1 /*
<lambda>null2  * Copyright (C) 2020 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.systemui.controls.controller
18 
19 import android.app.backup.BackupManager
20 import android.content.ComponentName
21 import android.util.AtomicFile
22 import android.util.Log
23 import android.util.Xml
24 import com.android.systemui.backup.BackupHelper
25 import libcore.io.IoUtils
26 import org.xmlpull.v1.XmlPullParser
27 import org.xmlpull.v1.XmlPullParserException
28 import java.io.BufferedInputStream
29 import java.io.File
30 import java.io.FileInputStream
31 import java.io.FileNotFoundException
32 import java.io.IOException
33 import java.util.concurrent.Executor
34 
35 /**
36  * Manages persistence of favorite controls.
37  *
38  * This class uses an [AtomicFile] to serialize the favorite controls to an xml.
39  * @property file a file location for storing/reading the favorites.
40  * @property executor an executor in which to execute storing the favorites.
41  */
42 class ControlsFavoritePersistenceWrapper(
43     private var file: File,
44     private val executor: Executor,
45     private var backupManager: BackupManager? = null
46 ) {
47 
48     companion object {
49         private const val TAG = "ControlsFavoritePersistenceWrapper"
50         const val FILE_NAME = "controls_favorites.xml"
51         private const val TAG_CONTROLS = "controls"
52         private const val TAG_STRUCTURES = "structures"
53         private const val TAG_STRUCTURE = "structure"
54         private const val TAG_CONTROL = "control"
55         private const val TAG_COMPONENT = "component"
56         private const val TAG_ID = "id"
57         private const val TAG_TITLE = "title"
58         private const val TAG_SUBTITLE = "subtitle"
59         private const val TAG_TYPE = "type"
60         private const val TAG_VERSION = "version"
61 
62         // must increment with every change to the XML structure
63         private const val VERSION = 1
64     }
65 
66     /**
67      * Change the file location for storing/reading the favorites and the [BackupManager]
68      *
69      * @param fileName new location
70      * @param newBackupManager new [BackupManager]. Pass null to not trigger backups.
71      */
72     fun changeFileAndBackupManager(fileName: File, newBackupManager: BackupManager?) {
73         file = fileName
74         backupManager = newBackupManager
75     }
76 
77     val fileExists: Boolean
78         get() = file.exists()
79 
80     fun deleteFile() {
81         file.delete()
82     }
83 
84     /**
85      * Stores the list of favorites in the corresponding file.
86      *
87      * @param list a list of favorite controls. The list will be stored in the same order.
88      */
89     fun storeFavorites(structures: List<StructureInfo>) {
90         if (structures.isEmpty() && !file.exists()) {
91             // Do not create a new file to store nothing
92             return
93         }
94         executor.execute {
95             Log.d(TAG, "Saving data to file: $file")
96             val atomicFile = AtomicFile(file)
97             val dataWritten = synchronized(BackupHelper.controlsDataLock) {
98                 val writer = try {
99                     atomicFile.startWrite()
100                 } catch (e: IOException) {
101                     Log.e(TAG, "Failed to start write file", e)
102                     return@execute
103                 }
104                 try {
105                     Xml.newSerializer().apply {
106                         setOutput(writer, "utf-8")
107                         setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
108                         startDocument(null, true)
109                         startTag(null, TAG_VERSION)
110                         text("$VERSION")
111                         endTag(null, TAG_VERSION)
112 
113                         startTag(null, TAG_STRUCTURES)
114                         structures.forEach { s ->
115                             startTag(null, TAG_STRUCTURE)
116                             attribute(null, TAG_COMPONENT, s.componentName.flattenToString())
117                             attribute(null, TAG_STRUCTURE, s.structure.toString())
118 
119                             startTag(null, TAG_CONTROLS)
120                             s.controls.forEach { c ->
121                                 startTag(null, TAG_CONTROL)
122                                 attribute(null, TAG_ID, c.controlId)
123                                 attribute(null, TAG_TITLE, c.controlTitle.toString())
124                                 attribute(null, TAG_SUBTITLE, c.controlSubtitle.toString())
125                                 attribute(null, TAG_TYPE, c.deviceType.toString())
126                                 endTag(null, TAG_CONTROL)
127                             }
128                             endTag(null, TAG_CONTROLS)
129                             endTag(null, TAG_STRUCTURE)
130                         }
131                         endTag(null, TAG_STRUCTURES)
132                         endDocument()
133                         atomicFile.finishWrite(writer)
134                     }
135                     true
136                 } catch (t: Throwable) {
137                     Log.e(TAG, "Failed to write file, reverting to previous version")
138                     atomicFile.failWrite(writer)
139                     false
140                 } finally {
141                     IoUtils.closeQuietly(writer)
142                 }
143             }
144             if (dataWritten) backupManager?.dataChanged()
145         }
146     }
147 
148     /**
149      * Stores the list of favorites in the corresponding file.
150      *
151      * @return a list of stored favorite controls. Return an empty list if the file is not found
152      * @throws [IllegalStateException] if there is an error while reading the file
153      */
154     fun readFavorites(): List<StructureInfo> {
155         if (!file.exists()) {
156             Log.d(TAG, "No favorites, returning empty list")
157             return emptyList()
158         }
159         val reader = try {
160             BufferedInputStream(FileInputStream(file))
161         } catch (fnfe: FileNotFoundException) {
162             Log.i(TAG, "No file found")
163             return emptyList()
164         }
165         try {
166             Log.d(TAG, "Reading data from file: $file")
167             synchronized(BackupHelper.controlsDataLock) {
168                 val parser = Xml.newPullParser()
169                 parser.setInput(reader, null)
170                 return parseXml(parser)
171             }
172         } catch (e: XmlPullParserException) {
173             throw IllegalStateException("Failed parsing favorites file: $file", e)
174         } catch (e: IOException) {
175             throw IllegalStateException("Failed parsing favorites file: $file", e)
176         } finally {
177             IoUtils.closeQuietly(reader)
178         }
179     }
180 
181     private fun parseXml(parser: XmlPullParser): List<StructureInfo> {
182         var type: Int
183         val infos = mutableListOf<StructureInfo>()
184 
185         var lastComponent: ComponentName? = null
186         var lastStructure: CharSequence? = null
187         var controls = mutableListOf<ControlInfo>()
188         while (parser.next().also { type = it } != XmlPullParser.END_DOCUMENT) {
189             val tagName = parser.name ?: ""
190             if (type == XmlPullParser.START_TAG && tagName == TAG_STRUCTURE) {
191                 lastComponent = ComponentName.unflattenFromString(
192                     parser.getAttributeValue(null, TAG_COMPONENT))
193                 lastStructure = parser.getAttributeValue(null, TAG_STRUCTURE) ?: ""
194             } else if (type == XmlPullParser.START_TAG && tagName == TAG_CONTROL) {
195                 val id = parser.getAttributeValue(null, TAG_ID)
196                 val title = parser.getAttributeValue(null, TAG_TITLE)
197                 val subtitle = parser.getAttributeValue(null, TAG_SUBTITLE) ?: ""
198                 val deviceType = parser.getAttributeValue(null, TAG_TYPE)?.toInt()
199                 if (id != null && title != null && deviceType != null) {
200                     controls.add(ControlInfo(id, title, subtitle, deviceType))
201                 }
202             } else if (type == XmlPullParser.END_TAG && tagName == TAG_STRUCTURE) {
203                 infos.add(StructureInfo(lastComponent!!, lastStructure!!, controls.toList()))
204                 controls.clear()
205             }
206         }
207 
208         return infos
209     }
210 }
211