blob: eac97fed192d5885867aabccb09b86ddf450db9e [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.permissioncontroller.permission.service
import android.app.job.JobScheduler
import android.content.Context
import android.provider.DeviceConfig
import android.util.Log
import android.util.Xml
import com.android.permissioncontroller.PermissionControllerApplication
import com.android.permissioncontroller.hibernation.getUnusedThresholdMs
import com.android.permissioncontroller.permission.data.PermissionChange
import com.android.permissioncontroller.permission.utils.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.StandardCharsets
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Implementation of [BasePermissionEventStorage] for storing [PermissionChange] events for long
* periods of time.
*/
class PermissionChangeStorageImpl(
context: Context,
jobScheduler: JobScheduler = context.getSystemService(JobScheduler::class.java)!!
) : BasePermissionEventStorage<PermissionChange>(context, jobScheduler) {
// We don't use namespaces
private val ns: String? = null
/**
* The format for how dates are stored.
*/
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
/**
* Exact format if [PROPERTY_PERMISSION_CHANGES_STORE_EXACT_TIME] is true
*/
private val exactTimeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
companion object {
private const val LOG_TAG = "PermissionChangeStorageImpl"
private const val DB_VERSION = 1
/**
* Config store file name for general shared store file.
*/
private const val STORE_FILE_NAME = "permission_changes.xml"
private const val TAG_PERMISSION_CHANGES = "permission-changes"
private const val TAG_PERMISSION_CHANGE = "permission-change"
private const val ATTR_VERSION = "version"
private const val ATTR_STORE_EXACT_TIME = "store-exact-time"
private const val ATTR_PACKAGE_NAME = "package-name"
private const val ATTR_EVENT_TIME = "event-time"
@Volatile
private var INSTANCE: PermissionEventStorage<PermissionChange>? = null
fun getInstance(): PermissionEventStorage<PermissionChange> =
INSTANCE ?: synchronized(this) {
INSTANCE ?: createInstance().also { INSTANCE = it }
}
private fun createInstance(): PermissionEventStorage<PermissionChange> {
return PermissionChangeStorageImpl(PermissionControllerApplication.get())
}
fun recordPermissionChange(packageName: String) {
GlobalScope.launch(Dispatchers.IO) {
getInstance().storeEvent(PermissionChange(packageName, System.currentTimeMillis()))
}
}
}
override fun serialize(stream: OutputStream, events: List<PermissionChange>) {
val out = Xml.newSerializer()
out.setOutput(stream, StandardCharsets.UTF_8.name())
out.startDocument(/* encoding= */ null, /* standalone= */ true)
out.startTag(ns, TAG_PERMISSION_CHANGES)
out.attribute(ns, ATTR_VERSION, DB_VERSION.toString())
val storesExactTime = storesExactTime()
out.attribute(ns, ATTR_STORE_EXACT_TIME, storesExactTime.toString())
val format = if (storesExactTime) exactTimeFormat else dateFormat
for (change in events) {
out.startTag(ns, TAG_PERMISSION_CHANGE)
out.attribute(ns, ATTR_PACKAGE_NAME, change.packageName)
val date = format.format(Date(change.eventTime))
out.attribute(ns, ATTR_EVENT_TIME, date)
out.endTag(ns, TAG_PERMISSION_CHANGE)
}
out.endTag(ns, TAG_PERMISSION_CHANGES)
out.endDocument()
}
override fun parse(inputStream: InputStream): List<PermissionChange> {
inputStream.use {
val parser: XmlPullParser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, /* state= */ false)
parser.setInput(inputStream, /* inputEncoding= */ null)
parser.nextTag()
return readPermissionChanges(parser)
}
}
@Throws(XmlPullParserException::class, IOException::class)
private fun readPermissionChanges(parser: XmlPullParser): List<PermissionChange> {
val entries = mutableListOf<PermissionChange>()
parser.require(XmlPullParser.START_TAG, ns, TAG_PERMISSION_CHANGES)
// Parse using whatever format was previously used no matter what current device config
// value is but truncate if we switched from exact granularity to day granularity
val didStoreExactTime =
parser.getAttributeValueNullSafe(ns, ATTR_STORE_EXACT_TIME).toBoolean()
val format = if (didStoreExactTime) exactTimeFormat else dateFormat
val storesExactTime = storesExactTime()
val truncateToDay = didStoreExactTime != storesExactTime && !storesExactTime
while (parser.next() != XmlPullParser.END_TAG) {
readPermissionChange(parser, format, truncateToDay)?.let {
entries.add(it)
}
}
return entries
}
@Throws(XmlPullParserException::class, IOException::class)
private fun readPermissionChange(
parser: XmlPullParser,
format: SimpleDateFormat,
truncateToDay: Boolean
): PermissionChange? {
var change: PermissionChange? = null
parser.require(XmlPullParser.START_TAG, ns, TAG_PERMISSION_CHANGE)
try {
val packageName = parser.getAttributeValueNullSafe(ns, ATTR_PACKAGE_NAME)
val changeDate = parser.getAttributeValueNullSafe(ns, ATTR_EVENT_TIME)
var changeTime = format.parse(changeDate)?.time
?: throw IllegalArgumentException(
"Could not parse date $changeDate on package $packageName")
if (truncateToDay) {
changeTime = dateFormat.parse(dateFormat.format(Date(changeTime))).time
}
change = PermissionChange(packageName, changeTime)
} catch (e: XmlPullParserException) {
Log.e(LOG_TAG, "Unable to parse permission change", e)
} catch (e: ParseException) {
Log.e(LOG_TAG, "Unable to parse permission change", e)
} catch (e: IllegalArgumentException) {
Log.e(LOG_TAG, "Unable to parse permission change", e)
} finally {
parser.nextTag()
parser.require(XmlPullParser.END_TAG, ns, TAG_PERMISSION_CHANGE)
}
return change
}
@Throws(XmlPullParserException::class)
private fun XmlPullParser.getAttributeValueNullSafe(namespace: String?, name: String): String {
return this.getAttributeValue(namespace, name)
?: throw XmlPullParserException(
"Could not find attribute: namespace $namespace, name $name")
}
override fun getDatabaseFileName(): String {
return STORE_FILE_NAME
}
override fun getMaxDataAgeMs(): Long {
// Only retain data up to the threshold needed for auto-revoke to trigger
return getUnusedThresholdMs()
}
override fun hasTheSamePrimaryKey(first: PermissionChange, second: PermissionChange): Boolean {
return first.packageName == second.packageName
}
override fun PermissionChange.copyWithTimeDelta(timeDelta: Long): PermissionChange {
return this.copy(eventTime = this.eventTime + timeDelta)
}
/**
* Should only be true in tests and never true in prod.
*/
private fun storesExactTime(): Boolean {
return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PERMISSIONS,
Utils.PROPERTY_PERMISSION_CHANGES_STORE_EXACT_TIME, /* defaultValue= */ false)
}
}