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