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