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