1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.permissioncontroller.permission.service.v33 18 19 import android.app.job.JobScheduler 20 import android.content.Context 21 import android.os.Build 22 import android.provider.DeviceConfig 23 import android.util.Log 24 import android.util.Xml 25 import androidx.annotation.RequiresApi 26 import com.android.permissioncontroller.DeviceUtils 27 import com.android.permissioncontroller.PermissionControllerApplication 28 import com.android.permissioncontroller.permission.data.v33.PermissionDecision 29 import com.android.permissioncontroller.permission.service.BasePermissionEventStorage 30 import com.android.permissioncontroller.permission.service.PermissionEventStorage 31 import com.android.permissioncontroller.permission.utils.Utils 32 import java.io.IOException 33 import java.io.InputStream 34 import java.io.OutputStream 35 import java.nio.charset.StandardCharsets 36 import java.text.ParseException 37 import java.text.SimpleDateFormat 38 import java.util.Date 39 import java.util.Locale 40 import java.util.concurrent.TimeUnit 41 import kotlinx.coroutines.Dispatchers 42 import kotlinx.coroutines.GlobalScope 43 import kotlinx.coroutines.launch 44 import org.xmlpull.v1.XmlPullParser 45 import org.xmlpull.v1.XmlPullParserException 46 47 /** Implementation of [BasePermissionEventStorage] for storing [PermissionDecision] events. */ 48 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 49 class PermissionDecisionStorageImpl( 50 context: Context, 51 jobScheduler: JobScheduler = context.getSystemService(JobScheduler::class.java)!! 52 ) : BasePermissionEventStorage<PermissionDecision>(context, jobScheduler) { 53 54 // We don't use namespaces 55 private val ns: String? = null 56 57 /** The format for how dates are stored. */ 58 private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) 59 60 companion object { 61 private const val LOG_TAG = "PermissionDecisionStorageImpl" 62 63 private const val DB_VERSION = 1 64 65 /** Config store file name for general shared store file. */ 66 private const val STORE_FILE_NAME = "recent_permission_decisions.xml" 67 68 private const val TAG_RECENT_PERMISSION_DECISIONS = "recent-permission-decisions" 69 private const val TAG_PERMISSION_DECISION = "permission-decision" 70 private const val ATTR_VERSION = "version" 71 private const val ATTR_PACKAGE_NAME = "package-name" 72 private const val ATTR_PERMISSION_GROUP = "permission-group-name" 73 private const val ATTR_DECISION_TIME = "decision-time" 74 private const val ATTR_IS_GRANTED = "is-granted" 75 76 private val DEFAULT_MAX_DATA_AGE_MS = TimeUnit.DAYS.toMillis(7) 77 78 @Volatile private var INSTANCE: PermissionEventStorage<PermissionDecision>? = null 79 getInstancenull80 fun getInstance(): PermissionEventStorage<PermissionDecision> = 81 INSTANCE ?: synchronized(this) { INSTANCE ?: createInstance().also { INSTANCE = it } } 82 createInstancenull83 private fun createInstance(): PermissionEventStorage<PermissionDecision> { 84 return PermissionDecisionStorageImpl(PermissionControllerApplication.get()) 85 } 86 recordPermissionDecisionnull87 fun recordPermissionDecision( 88 context: Context, 89 packageName: String, 90 permGroupName: String, 91 isGranted: Boolean 92 ) { 93 if (isRecordPermissionsSupported(context)) { 94 GlobalScope.launch(Dispatchers.IO) { 95 getInstance() 96 .storeEvent( 97 PermissionDecision( 98 packageName, 99 System.currentTimeMillis(), 100 permGroupName, 101 isGranted 102 ) 103 ) 104 } 105 } 106 } 107 isRecordPermissionsSupportednull108 fun isRecordPermissionsSupported(context: Context): Boolean { 109 return DeviceUtils.isAuto(context) 110 } 111 } 112 serializenull113 override fun serialize(stream: OutputStream, events: List<PermissionDecision>) { 114 val out = Xml.newSerializer() 115 out.setOutput(stream, StandardCharsets.UTF_8.name()) 116 out.startDocument(/* encoding= */ null, /* standalone= */ true) 117 out.startTag(ns, TAG_RECENT_PERMISSION_DECISIONS) 118 out.attribute(/* namespace= */ null, ATTR_VERSION, DB_VERSION.toString()) 119 for (decision in events) { 120 out.startTag(ns, TAG_PERMISSION_DECISION) 121 out.attribute(ns, ATTR_PACKAGE_NAME, decision.packageName) 122 out.attribute(ns, ATTR_PERMISSION_GROUP, decision.permissionGroupName) 123 val date = dateFormat.format(Date(decision.eventTime)) 124 out.attribute(ns, ATTR_DECISION_TIME, date) 125 out.attribute(ns, ATTR_IS_GRANTED, decision.isGranted.toString()) 126 out.endTag(ns, TAG_PERMISSION_DECISION) 127 } 128 out.endTag(/* namespace= */ null, TAG_RECENT_PERMISSION_DECISIONS) 129 out.endDocument() 130 } 131 parsenull132 override fun parse(inputStream: InputStream): List<PermissionDecision> { 133 inputStream.use { 134 val parser: XmlPullParser = Xml.newPullParser() 135 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) 136 parser.setInput(inputStream, /* inputEncoding= */ null) 137 parser.nextTag() 138 return readRecentDecisions(parser) 139 } 140 } 141 142 @Throws(XmlPullParserException::class, IOException::class) readRecentDecisionsnull143 private fun readRecentDecisions(parser: XmlPullParser): List<PermissionDecision> { 144 val entries = mutableListOf<PermissionDecision>() 145 146 parser.require(XmlPullParser.START_TAG, ns, TAG_RECENT_PERMISSION_DECISIONS) 147 while (parser.next() != XmlPullParser.END_TAG) { 148 readPermissionDecision(parser)?.let { entries.add(it) } 149 } 150 return entries 151 } 152 153 @Throws(XmlPullParserException::class, IOException::class) readPermissionDecisionnull154 private fun readPermissionDecision(parser: XmlPullParser): PermissionDecision? { 155 var decision: PermissionDecision? = null 156 parser.require(XmlPullParser.START_TAG, ns, TAG_PERMISSION_DECISION) 157 try { 158 val packageName = parser.getAttributeValueNullSafe(ns, ATTR_PACKAGE_NAME) 159 val permissionGroup = parser.getAttributeValueNullSafe(ns, ATTR_PERMISSION_GROUP) 160 val decisionDate = parser.getAttributeValueNullSafe(ns, ATTR_DECISION_TIME) 161 val decisionTime = 162 dateFormat.parse(decisionDate)?.time 163 ?: throw IllegalArgumentException( 164 "Could not parse date $decisionDate on package $packageName" 165 ) 166 val isGranted = parser.getAttributeValueNullSafe(ns, ATTR_IS_GRANTED).toBoolean() 167 decision = PermissionDecision(packageName, decisionTime, permissionGroup, isGranted) 168 } catch (e: XmlPullParserException) { 169 Log.e(LOG_TAG, "Unable to parse permission decision", e) 170 } catch (e: ParseException) { 171 Log.e(LOG_TAG, "Unable to parse permission decision", e) 172 } catch (e: IllegalArgumentException) { 173 Log.e(LOG_TAG, "Unable to parse permission decision", e) 174 } finally { 175 parser.nextTag() 176 parser.require(XmlPullParser.END_TAG, ns, TAG_PERMISSION_DECISION) 177 } 178 return decision 179 } 180 181 @Throws(XmlPullParserException::class) getAttributeValueNullSafenull182 private fun XmlPullParser.getAttributeValueNullSafe(namespace: String?, name: String): String { 183 return this.getAttributeValue(namespace, name) 184 ?: throw XmlPullParserException( 185 "Could not find attribute: namespace $namespace, name $name" 186 ) 187 } 188 getDatabaseFileNamenull189 override fun getDatabaseFileName(): String { 190 return STORE_FILE_NAME 191 } 192 getMaxDataAgeMsnull193 override fun getMaxDataAgeMs(): Long { 194 return DeviceConfig.getLong( 195 DeviceConfig.NAMESPACE_PERMISSIONS, 196 Utils.PROPERTY_PERMISSION_DECISIONS_MAX_DATA_AGE_MILLIS, 197 DEFAULT_MAX_DATA_AGE_MS 198 ) 199 } 200 hasTheSamePrimaryKeynull201 override fun hasTheSamePrimaryKey( 202 first: PermissionDecision, 203 second: PermissionDecision 204 ): Boolean { 205 return first.packageName == second.packageName && 206 first.permissionGroupName == second.permissionGroupName 207 } 208 copyWithTimeDeltanull209 override fun PermissionDecision.copyWithTimeDelta(timeDelta: Long): PermissionDecision { 210 return this.copy(eventTime = this.eventTime + timeDelta) 211 } 212 } 213