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 java.io.IOException 29 import java.io.InputStream 30 import java.io.OutputStream 31 import java.nio.charset.StandardCharsets 32 import java.text.ParseException 33 import java.text.SimpleDateFormat 34 import java.util.Date 35 import java.util.Locale 36 import kotlinx.coroutines.DelicateCoroutinesApi 37 import kotlinx.coroutines.Dispatchers 38 import kotlinx.coroutines.GlobalScope 39 import kotlinx.coroutines.launch 40 import org.xmlpull.v1.XmlPullParser 41 import org.xmlpull.v1.XmlPullParserException 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 /** The format for how dates are stored. */ 56 private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) 57 58 /** Exact format if [PROPERTY_PERMISSION_CHANGES_STORE_EXACT_TIME] is true */ 59 private val exactTimeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) 60 61 companion object { 62 private const val LOG_TAG = "PermissionChangeStorageImpl" 63 64 private const val DB_VERSION = 1 65 66 /** Config store file name for general shared store file. */ 67 private const val STORE_FILE_NAME = "permission_changes.xml" 68 69 private const val TAG_PERMISSION_CHANGES = "permission-changes" 70 private const val TAG_PERMISSION_CHANGE = "permission-change" 71 private const val ATTR_VERSION = "version" 72 private const val ATTR_STORE_EXACT_TIME = "store-exact-time" 73 private const val ATTR_PACKAGE_NAME = "package-name" 74 private const val ATTR_EVENT_TIME = "event-time" 75 76 @Volatile private var INSTANCE: PermissionEventStorage<PermissionChange>? = null 77 getInstancenull78 fun getInstance(): PermissionEventStorage<PermissionChange> = 79 INSTANCE ?: synchronized(this) { INSTANCE ?: createInstance().also { INSTANCE = it } } 80 createInstancenull81 private fun createInstance(): PermissionEventStorage<PermissionChange> { 82 return PermissionChangeStorageImpl(PermissionControllerApplication.get()) 83 } 84 85 @OptIn(DelicateCoroutinesApi::class) recordPermissionChangenull86 fun recordPermissionChange(packageName: String) { 87 GlobalScope.launch(Dispatchers.IO) { 88 getInstance().storeEvent(PermissionChange(packageName, System.currentTimeMillis())) 89 } 90 } 91 } 92 serializenull93 override fun serialize(stream: OutputStream, events: List<PermissionChange>) { 94 val out = Xml.newSerializer() 95 out.setOutput(stream, StandardCharsets.UTF_8.name()) 96 out.startDocument(/* encoding= */ null, /* standalone= */ true) 97 out.startTag(ns, TAG_PERMISSION_CHANGES) 98 out.attribute(ns, ATTR_VERSION, DB_VERSION.toString()) 99 val storesExactTime = storesExactTime() 100 out.attribute(ns, ATTR_STORE_EXACT_TIME, storesExactTime.toString()) 101 val format = if (storesExactTime) exactTimeFormat else dateFormat 102 for (change in events) { 103 out.startTag(ns, TAG_PERMISSION_CHANGE) 104 out.attribute(ns, ATTR_PACKAGE_NAME, change.packageName) 105 val date = format.format(Date(change.eventTime)) 106 out.attribute(ns, ATTR_EVENT_TIME, date) 107 out.endTag(ns, TAG_PERMISSION_CHANGE) 108 } 109 out.endTag(ns, TAG_PERMISSION_CHANGES) 110 out.endDocument() 111 } 112 parsenull113 override fun parse(inputStream: InputStream): List<PermissionChange> { 114 inputStream.use { 115 val parser: XmlPullParser = Xml.newPullParser() 116 parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, /* state= */ false) 117 parser.setInput(inputStream, /* inputEncoding= */ null) 118 parser.nextTag() 119 return readPermissionChanges(parser) 120 } 121 } 122 123 @Throws(XmlPullParserException::class, IOException::class) readPermissionChangesnull124 private fun readPermissionChanges(parser: XmlPullParser): List<PermissionChange> { 125 val entries = mutableListOf<PermissionChange>() 126 127 parser.require(XmlPullParser.START_TAG, ns, TAG_PERMISSION_CHANGES) 128 // Parse using whatever format was previously used no matter what current device config 129 // value is but truncate if we switched from exact granularity to day granularity 130 val didStoreExactTime = 131 parser.getAttributeValueNullSafe(ns, ATTR_STORE_EXACT_TIME).toBoolean() 132 val format = if (didStoreExactTime) exactTimeFormat else dateFormat 133 val storesExactTime = storesExactTime() 134 val truncateToDay = didStoreExactTime != storesExactTime && !storesExactTime 135 while (parser.next() != XmlPullParser.END_TAG) { 136 readPermissionChange(parser, format, truncateToDay)?.let { entries.add(it) } 137 } 138 return entries 139 } 140 141 @Throws(XmlPullParserException::class, IOException::class) readPermissionChangenull142 private fun readPermissionChange( 143 parser: XmlPullParser, 144 format: SimpleDateFormat, 145 truncateToDay: Boolean 146 ): PermissionChange? { 147 var change: PermissionChange? = null 148 parser.require(XmlPullParser.START_TAG, ns, TAG_PERMISSION_CHANGE) 149 try { 150 val packageName = parser.getAttributeValueNullSafe(ns, ATTR_PACKAGE_NAME) 151 val changeDate = parser.getAttributeValueNullSafe(ns, ATTR_EVENT_TIME) 152 var changeTime = 153 format.parse(changeDate)?.time 154 ?: throw IllegalArgumentException( 155 "Could not parse date $changeDate on package $packageName" 156 ) 157 if (truncateToDay) { 158 changeTime = dateFormat.parse(dateFormat.format(Date(changeTime)))!!.time 159 } 160 change = PermissionChange(packageName, changeTime) 161 } catch (e: XmlPullParserException) { 162 Log.e(LOG_TAG, "Unable to parse permission change", e) 163 } catch (e: ParseException) { 164 Log.e(LOG_TAG, "Unable to parse permission change", e) 165 } catch (e: IllegalArgumentException) { 166 Log.e(LOG_TAG, "Unable to parse permission change", e) 167 } finally { 168 parser.nextTag() 169 parser.require(XmlPullParser.END_TAG, ns, TAG_PERMISSION_CHANGE) 170 } 171 return change 172 } 173 174 @Throws(XmlPullParserException::class) getAttributeValueNullSafenull175 private fun XmlPullParser.getAttributeValueNullSafe(namespace: String?, name: String): String { 176 return this.getAttributeValue(namespace, name) 177 ?: throw XmlPullParserException( 178 "Could not find attribute: namespace $namespace, name $name" 179 ) 180 } 181 getDatabaseFileNamenull182 override fun getDatabaseFileName(): String { 183 return STORE_FILE_NAME 184 } 185 getMaxDataAgeMsnull186 override fun getMaxDataAgeMs(): Long { 187 // Only retain data up to the threshold needed for auto-revoke to trigger 188 return getUnusedThresholdMs() 189 } 190 hasTheSamePrimaryKeynull191 override fun hasTheSamePrimaryKey(first: PermissionChange, second: PermissionChange): Boolean { 192 return first.packageName == second.packageName 193 } 194 copyWithTimeDeltanull195 override fun PermissionChange.copyWithTimeDelta(timeDelta: Long): PermissionChange { 196 return this.copy(eventTime = this.eventTime + timeDelta) 197 } 198 199 /** Should only be true in tests and never true in prod. */ storesExactTimenull200 private fun storesExactTime(): Boolean { 201 return DeviceConfig.getBoolean( 202 DeviceConfig.NAMESPACE_PERMISSIONS, 203 Utils.PROPERTY_PERMISSION_CHANGES_STORE_EXACT_TIME, 204 /* defaultValue= */ false 205 ) 206 } 207 } 208