• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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