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 18 19 import android.app.job.JobScheduler 20 import android.content.Context 21 import android.provider.DeviceConfig 22 import android.util.Log 23 import android.util.Xml 24 import com.android.permissioncontroller.PermissionControllerApplication 25 import com.android.permissioncontroller.hibernation.getUnusedThresholdMs 26 import com.android.permissioncontroller.permission.data.PermissionChange 27 import com.android.permissioncontroller.permission.utils.Utils 28 import kotlinx.coroutines.DelicateCoroutinesApi 29 import kotlinx.coroutines.Dispatchers 30 import kotlinx.coroutines.GlobalScope 31 import kotlinx.coroutines.launch 32 import org.xmlpull.v1.XmlPullParser 33 import org.xmlpull.v1.XmlPullParserException 34 import java.io.IOException 35 import java.io.InputStream 36 import java.io.OutputStream 37 import java.nio.charset.StandardCharsets 38 import java.text.ParseException 39 import java.text.SimpleDateFormat 40 import java.util.Date 41 import java.util.Locale 42 43 /** 44 * Implementation of [BasePermissionEventStorage] for storing [PermissionChange] events for long 45 * periods of time. 46 */ 47 class PermissionChangeStorageImpl( 48 context: Context, 49 jobScheduler: JobScheduler = context.getSystemService(JobScheduler::class.java)!! 50 ) : BasePermissionEventStorage<PermissionChange>(context, jobScheduler) { 51 52 // We don't use namespaces 53 private val ns: String? = null 54 55 /** 56 * The format for how dates are stored. 57 */ 58 private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) 59 60 /** 61 * Exact format if [PROPERTY_PERMISSION_CHANGES_STORE_EXACT_TIME] is true 62 */ 63 private val exactTimeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) 64 65 companion object { 66 private const val LOG_TAG = "PermissionChangeStorageImpl" 67 68 private const val DB_VERSION = 1 69 70 /** 71 * Config store file name for general shared store file. 72 */ 73 private const val STORE_FILE_NAME = "permission_changes.xml" 74 75 private const val TAG_PERMISSION_CHANGES = "permission-changes" 76 private const val TAG_PERMISSION_CHANGE = "permission-change" 77 private const val ATTR_VERSION = "version" 78 private const val ATTR_STORE_EXACT_TIME = "store-exact-time" 79 private const val ATTR_PACKAGE_NAME = "package-name" 80 private const val ATTR_EVENT_TIME = "event-time" 81 82 @Volatile 83 private var INSTANCE: PermissionEventStorage<PermissionChange>? = null 84 getInstancenull85 fun getInstance(): PermissionEventStorage<PermissionChange> = 86 INSTANCE ?: synchronized(this) { 87 INSTANCE ?: createInstance().also { INSTANCE = it } 88 } 89 createInstancenull90 private fun createInstance(): PermissionEventStorage<PermissionChange> { 91 return PermissionChangeStorageImpl(PermissionControllerApplication.get()) 92 } 93 94 @OptIn(DelicateCoroutinesApi::class) recordPermissionChangenull95 fun recordPermissionChange(packageName: String) { 96 GlobalScope.launch(Dispatchers.IO) { 97 getInstance().storeEvent(PermissionChange(packageName, System.currentTimeMillis())) 98 } 99 } 100 } 101 serializenull102 override fun serialize(stream: OutputStream, events: List<PermissionChange>) { 103 val out = Xml.newSerializer() 104 out.setOutput(stream, StandardCharsets.UTF_8.name()) 105 out.startDocument(/* encoding= */ null, /* standalone= */ true) 106 out.startTag(ns, TAG_PERMISSION_CHANGES) 107 out.attribute(ns, ATTR_VERSION, DB_VERSION.toString()) 108 val storesExactTime = storesExactTime() 109 out.attribute(ns, ATTR_STORE_EXACT_TIME, storesExactTime.toString()) 110 val format = if (storesExactTime) exactTimeFormat else dateFormat 111 for (change in events) { 112 out.startTag(ns, TAG_PERMISSION_CHANGE) 113 out.attribute(ns, ATTR_PACKAGE_NAME, change.packageName) 114 val date = format.format(Date(change.eventTime)) 115 out.attribute(ns, ATTR_EVENT_TIME, date) 116 out.endTag(ns, TAG_PERMISSION_CHANGE) 117 } 118 out.endTag(ns, TAG_PERMISSION_CHANGES) 119 out.endDocument() 120 } 121 parsenull122 override fun parse(inputStream: InputStream): List<PermissionChange> { 123 inputStream.use { 124 val parser: XmlPullParser = Xml.newPullParser() 125 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, /* state= */ false) 126 parser.setInput(inputStream, /* inputEncoding= */ null) 127 parser.nextTag() 128 return readPermissionChanges(parser) 129 } 130 } 131 132 @Throws(XmlPullParserException::class, IOException::class) readPermissionChangesnull133 private fun readPermissionChanges(parser: XmlPullParser): List<PermissionChange> { 134 val entries = mutableListOf<PermissionChange>() 135 136 parser.require(XmlPullParser.START_TAG, ns, TAG_PERMISSION_CHANGES) 137 // Parse using whatever format was previously used no matter what current device config 138 // value is but truncate if we switched from exact granularity to day granularity 139 val didStoreExactTime = 140 parser.getAttributeValueNullSafe(ns, ATTR_STORE_EXACT_TIME).toBoolean() 141 val format = if (didStoreExactTime) exactTimeFormat else dateFormat 142 val storesExactTime = storesExactTime() 143 val truncateToDay = didStoreExactTime != storesExactTime && !storesExactTime 144 while (parser.next() != XmlPullParser.END_TAG) { 145 readPermissionChange(parser, format, truncateToDay)?.let { 146 entries.add(it) 147 } 148 } 149 return entries 150 } 151 152 @Throws(XmlPullParserException::class, IOException::class) readPermissionChangenull153 private fun readPermissionChange( 154 parser: XmlPullParser, 155 format: SimpleDateFormat, 156 truncateToDay: Boolean 157 ): PermissionChange? { 158 var change: PermissionChange? = null 159 parser.require(XmlPullParser.START_TAG, ns, TAG_PERMISSION_CHANGE) 160 try { 161 val packageName = parser.getAttributeValueNullSafe(ns, ATTR_PACKAGE_NAME) 162 val changeDate = parser.getAttributeValueNullSafe(ns, ATTR_EVENT_TIME) 163 var changeTime = format.parse(changeDate)?.time 164 ?: throw IllegalArgumentException( 165 "Could not parse date $changeDate on package $packageName") 166 if (truncateToDay) { 167 changeTime = dateFormat.parse(dateFormat.format(Date(changeTime)))!!.time 168 } 169 change = PermissionChange(packageName, changeTime) 170 } catch (e: XmlPullParserException) { 171 Log.e(LOG_TAG, "Unable to parse permission change", e) 172 } catch (e: ParseException) { 173 Log.e(LOG_TAG, "Unable to parse permission change", e) 174 } catch (e: IllegalArgumentException) { 175 Log.e(LOG_TAG, "Unable to parse permission change", e) 176 } finally { 177 parser.nextTag() 178 parser.require(XmlPullParser.END_TAG, ns, TAG_PERMISSION_CHANGE) 179 } 180 return change 181 } 182 183 @Throws(XmlPullParserException::class) getAttributeValueNullSafenull184 private fun XmlPullParser.getAttributeValueNullSafe(namespace: String?, name: String): String { 185 return this.getAttributeValue(namespace, name) 186 ?: throw XmlPullParserException( 187 "Could not find attribute: namespace $namespace, name $name") 188 } 189 getDatabaseFileNamenull190 override fun getDatabaseFileName(): String { 191 return STORE_FILE_NAME 192 } 193 getMaxDataAgeMsnull194 override fun getMaxDataAgeMs(): Long { 195 // Only retain data up to the threshold needed for auto-revoke to trigger 196 return getUnusedThresholdMs() 197 } 198 hasTheSamePrimaryKeynull199 override fun hasTheSamePrimaryKey(first: PermissionChange, second: PermissionChange): Boolean { 200 return first.packageName == second.packageName 201 } 202 copyWithTimeDeltanull203 override fun PermissionChange.copyWithTimeDelta(timeDelta: Long): PermissionChange { 204 return this.copy(eventTime = this.eventTime + timeDelta) 205 } 206 207 /** 208 * Should only be true in tests and never true in prod. 209 */ storesExactTimenull210 private fun storesExactTime(): Boolean { 211 return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PERMISSIONS, 212 Utils.PROPERTY_PERMISSION_CHANGES_STORE_EXACT_TIME, /* defaultValue= */ false) 213 } 214 } 215