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.utils.Utils 30 import kotlinx.coroutines.Dispatchers 31 import kotlinx.coroutines.GlobalScope 32 import kotlinx.coroutines.launch 33 import org.xmlpull.v1.XmlPullParser 34 import org.xmlpull.v1.XmlPullParserException 35 import java.io.IOException 36 import java.io.InputStream 37 import java.io.OutputStream 38 import java.nio.charset.StandardCharsets 39 import java.text.ParseException 40 import java.text.SimpleDateFormat 41 import java.util.Date 42 import java.util.Locale 43 import java.util.concurrent.TimeUnit 44 45 /** 46 * Implementation of [BasePermissionEventStorage] for storing [PermissionDecision] events. 47 */ 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 /** 58 * The format for how dates are stored. 59 */ 60 private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) 61 62 companion object { 63 private const val LOG_TAG = "PermissionDecisionStorageImpl" 64 65 private const val DB_VERSION = 1 66 67 /** 68 * Config store file name for general shared store file. 69 */ 70 private const val STORE_FILE_NAME = "recent_permission_decisions.xml" 71 72 private const val TAG_RECENT_PERMISSION_DECISIONS = "recent-permission-decisions" 73 private const val TAG_PERMISSION_DECISION = "permission-decision" 74 private const val ATTR_VERSION = "version" 75 private const val ATTR_PACKAGE_NAME = "package-name" 76 private const val ATTR_PERMISSION_GROUP = "permission-group-name" 77 private const val ATTR_DECISION_TIME = "decision-time" 78 private const val ATTR_IS_GRANTED = "is-granted" 79 80 private val DEFAULT_MAX_DATA_AGE_MS = TimeUnit.DAYS.toMillis(7) 81 82 @Volatile 83 private var INSTANCE: PermissionEventStorage<PermissionDecision>? = null 84 getInstancenull85 fun getInstance(): PermissionEventStorage<PermissionDecision> = 86 INSTANCE ?: synchronized(this) { 87 INSTANCE ?: createInstance().also { INSTANCE = it } 88 } 89 createInstancenull90 private fun createInstance(): PermissionEventStorage<PermissionDecision> { 91 return PermissionDecisionStorageImpl(PermissionControllerApplication.get()) 92 } 93 recordPermissionDecisionnull94 fun recordPermissionDecision( 95 context: Context, 96 packageName: String, 97 permGroupName: String, 98 isGranted: Boolean 99 ) { 100 if (isRecordPermissionsSupported(context)) { 101 GlobalScope.launch(Dispatchers.IO) { 102 getInstance().storeEvent( 103 PermissionDecision(packageName, System.currentTimeMillis(), permGroupName, 104 isGranted)) 105 } 106 } 107 } 108 isRecordPermissionsSupportednull109 fun isRecordPermissionsSupported(context: Context): Boolean { 110 return DeviceUtils.isAuto(context) 111 } 112 } 113 serializenull114 override fun serialize(stream: OutputStream, events: List<PermissionDecision>) { 115 val out = Xml.newSerializer() 116 out.setOutput(stream, StandardCharsets.UTF_8.name()) 117 out.startDocument(/* encoding= */ null, /* standalone= */ true) 118 out.startTag(ns, TAG_RECENT_PERMISSION_DECISIONS) 119 out.attribute(/* namespace= */ null, ATTR_VERSION, DB_VERSION.toString()) 120 for (decision in events) { 121 out.startTag(ns, TAG_PERMISSION_DECISION) 122 out.attribute(ns, ATTR_PACKAGE_NAME, decision.packageName) 123 out.attribute(ns, ATTR_PERMISSION_GROUP, decision.permissionGroupName) 124 val date = dateFormat.format(Date(decision.eventTime)) 125 out.attribute(ns, ATTR_DECISION_TIME, date) 126 out.attribute(ns, ATTR_IS_GRANTED, decision.isGranted.toString()) 127 out.endTag(ns, TAG_PERMISSION_DECISION) 128 } 129 out.endTag(/* namespace= */ null, TAG_RECENT_PERMISSION_DECISIONS) 130 out.endDocument() 131 } 132 parsenull133 override fun parse(inputStream: InputStream): List<PermissionDecision> { 134 inputStream.use { 135 val parser: XmlPullParser = Xml.newPullParser() 136 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) 137 parser.setInput(inputStream, /* inputEncoding= */ null) 138 parser.nextTag() 139 return readRecentDecisions(parser) 140 } 141 } 142 143 @Throws(XmlPullParserException::class, IOException::class) readRecentDecisionsnull144 private fun readRecentDecisions(parser: XmlPullParser): List<PermissionDecision> { 145 val entries = mutableListOf<PermissionDecision>() 146 147 parser.require(XmlPullParser.START_TAG, ns, TAG_RECENT_PERMISSION_DECISIONS) 148 while (parser.next() != XmlPullParser.END_TAG) { 149 readPermissionDecision(parser)?.let { 150 entries.add(it) 151 } 152 } 153 return entries 154 } 155 156 @Throws(XmlPullParserException::class, IOException::class) readPermissionDecisionnull157 private fun readPermissionDecision(parser: XmlPullParser): PermissionDecision? { 158 var decision: PermissionDecision? = null 159 parser.require(XmlPullParser.START_TAG, ns, TAG_PERMISSION_DECISION) 160 try { 161 val packageName = parser.getAttributeValueNullSafe(ns, ATTR_PACKAGE_NAME) 162 val permissionGroup = parser.getAttributeValueNullSafe(ns, ATTR_PERMISSION_GROUP) 163 val decisionDate = parser.getAttributeValueNullSafe(ns, ATTR_DECISION_TIME) 164 val decisionTime = dateFormat.parse(decisionDate)?.time 165 ?: throw IllegalArgumentException( 166 "Could not parse date $decisionDate on package $packageName") 167 val isGranted = parser.getAttributeValueNullSafe(ns, ATTR_IS_GRANTED).toBoolean() 168 decision = PermissionDecision(packageName, decisionTime, permissionGroup, isGranted) 169 } catch (e: XmlPullParserException) { 170 Log.e(LOG_TAG, "Unable to parse permission decision", e) 171 } catch (e: ParseException) { 172 Log.e(LOG_TAG, "Unable to parse permission decision", e) 173 } catch (e: IllegalArgumentException) { 174 Log.e(LOG_TAG, "Unable to parse permission decision", e) 175 } finally { 176 parser.nextTag() 177 parser.require(XmlPullParser.END_TAG, ns, TAG_PERMISSION_DECISION) 178 } 179 return decision 180 } 181 182 @Throws(XmlPullParserException::class) getAttributeValueNullSafenull183 private fun XmlPullParser.getAttributeValueNullSafe(namespace: String?, name: String): String { 184 return this.getAttributeValue(namespace, name) 185 ?: throw XmlPullParserException( 186 "Could not find attribute: namespace $namespace, name $name") 187 } 188 getDatabaseFileNamenull189 override fun getDatabaseFileName(): String { 190 return STORE_FILE_NAME 191 } 192 getMaxDataAgeMsnull193 override fun getMaxDataAgeMs(): Long { 194 return DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, 195 Utils.PROPERTY_PERMISSION_DECISIONS_MAX_DATA_AGE_MILLIS, 196 DEFAULT_MAX_DATA_AGE_MS) 197 } 198 hasTheSamePrimaryKeynull199 override fun hasTheSamePrimaryKey( 200 first: PermissionDecision, 201 second: PermissionDecision 202 ): Boolean { 203 return first.packageName == second.packageName && 204 first.permissionGroupName == second.permissionGroupName 205 } 206 copyWithTimeDeltanull207 override fun PermissionDecision.copyWithTimeDelta(timeDelta: Long): PermissionDecision { 208 return this.copy(eventTime = this.eventTime + timeDelta) 209 } 210 } 211