1 /* 2 * Copyright (C) 2021 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.util.AtomicFile 22 import android.util.Log 23 import com.android.permissioncontroller.DumpableLog 24 import com.android.permissioncontroller.permission.data.PermissionEvent 25 import org.xmlpull.v1.XmlPullParserException 26 import java.io.File 27 import java.io.FileOutputStream 28 import java.io.IOException 29 import java.io.InputStream 30 import java.io.OutputStream 31 32 /** 33 * Thread-safe implementation of [PermissionEventStorage] using an XML file as the 34 * database. 35 */ 36 abstract class BasePermissionEventStorage<T : PermissionEvent>( 37 private val context: Context, 38 jobScheduler: JobScheduler = context.getSystemService(JobScheduler::class.java)!! 39 ) : PermissionEventStorage<T> { 40 41 private val dbFile: AtomicFile = AtomicFile(File(context.filesDir, getDatabaseFileName())) 42 private val fileLock = Object() 43 44 companion object { 45 private const val LOG_TAG = "BasePermissionEventStorage" 46 } 47 48 init { 49 PermissionEventCleanupJobService.scheduleOldDataCleanupIfNecessary(context, jobScheduler) 50 } 51 storeEventnull52 override suspend fun storeEvent(event: T): Boolean { 53 synchronized(fileLock) { 54 val existingEvents = readData() 55 56 val newEvents = mutableListOf<T>() 57 // add new event first to keep the list ordered 58 newEvents.add(event) 59 for (existingEvent in existingEvents) { 60 // ignore any old events that violate the primary key uniqueness with the database 61 if (hasTheSamePrimaryKey(existingEvent, event)) { 62 continue 63 } 64 newEvents.add(existingEvent) 65 } 66 67 return writeData(newEvents) 68 } 69 } 70 loadEventsnull71 override suspend fun loadEvents(): List<T> { 72 synchronized(fileLock) { 73 return readData() 74 } 75 } 76 clearEventsnull77 override suspend fun clearEvents() { 78 synchronized(fileLock) { 79 dbFile.delete() 80 } 81 } 82 removeOldDatanull83 override suspend fun removeOldData(): Boolean { 84 synchronized(fileLock) { 85 val existingEvents = readData() 86 87 val originalCount = existingEvents.size 88 val newEvents = existingEvents.filter { 89 (System.currentTimeMillis() - it.eventTime) <= getMaxDataAgeMs() 90 } 91 92 DumpableLog.d(LOG_TAG, 93 "${originalCount - newEvents.size} old permission events removed") 94 95 return writeData(newEvents) 96 } 97 } 98 removeEventsForPackagenull99 override suspend fun removeEventsForPackage(packageName: String): Boolean { 100 synchronized(fileLock) { 101 val existingEvents = readData() 102 103 val newEvents = existingEvents.filter { it.packageName != packageName } 104 return writeData(newEvents) 105 } 106 } 107 updateEventsBySystemTimeDeltanull108 override suspend fun updateEventsBySystemTimeDelta(diffSystemTimeMillis: Long): Boolean { 109 synchronized(fileLock) { 110 val existingEvents = readData() 111 112 val newEvents = existingEvents.map { 113 it.copyWithTimeDelta(diffSystemTimeMillis) 114 } 115 return writeData(newEvents) 116 } 117 } 118 writeDatanull119 private fun writeData(events: List<T>): Boolean { 120 val stream: FileOutputStream = try { 121 dbFile.startWrite() 122 } catch (e: IOException) { 123 Log.e(LOG_TAG, "Failed to save db file", e) 124 return false 125 } 126 try { 127 serialize(stream, events) 128 dbFile.finishWrite(stream) 129 } catch (e: IOException) { 130 Log.e(LOG_TAG, "Failed to save db file, restoring backup", e) 131 dbFile.failWrite(stream) 132 return false 133 } 134 135 return true 136 } 137 readDatanull138 private fun readData(): List<T> { 139 if (!dbFile.baseFile.exists()) { 140 return emptyList() 141 } 142 return try { 143 parse(dbFile.openRead()) 144 } catch (e: IOException) { 145 Log.e(LOG_TAG, "Failed to read db file", e) 146 emptyList() 147 } catch (e: XmlPullParserException) { 148 Log.e(LOG_TAG, "Failed to read db file", e) 149 emptyList() 150 } 151 } 152 153 /** 154 * Serialize a list of permission events. 155 * 156 * @param stream output stream to serialize events to 157 * @param events list of permission events to serialize 158 */ serializenull159 abstract fun serialize(stream: OutputStream, events: List<T>) 160 161 /** 162 * Parse a list of permission events from the XML parser. 163 * 164 * @param inputStream input stream to parse events from 165 * @return the list of parsed permission events 166 */ 167 @Throws(XmlPullParserException::class, IOException::class) 168 abstract fun parse(inputStream: InputStream): List<T> 169 170 /** 171 * Returns file name for database. 172 */ 173 abstract fun getDatabaseFileName(): String 174 175 /** 176 * Returns max time that data should be persisted before being removed. 177 */ 178 abstract fun getMaxDataAgeMs(): Long 179 180 /** 181 * Returns true if the two events have the same primary key for the database store. 182 */ 183 abstract fun hasTheSamePrimaryKey(first: T, second: T): Boolean 184 185 /** 186 * Copies the event with the time delta applied to the [PermissionEvent.eventTime]. 187 */ 188 abstract fun T.copyWithTimeDelta(timeDelta: Long): T 189 } 190