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