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