[DataStore] Provide BackupRestoreFileStorage
Bug: 325144964
Test: Manual tests
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:50784fe9c08b868ec04562f7ae3df751515c9ee3)
Merged-In: I9ad13305321d0c9f5f6dccc46c266443fd45f73e
Change-Id: I9ad13305321d0c9f5f6dccc46c266443fd45f73e
diff --git a/packages/SettingsLib/DataStore/Android.bp b/packages/SettingsLib/DataStore/Android.bp
index 868a4a5..1806544 100644
--- a/packages/SettingsLib/DataStore/Android.bp
+++ b/packages/SettingsLib/DataStore/Android.bp
@@ -11,6 +11,7 @@
static_libs: [
"androidx.annotation_annotation",
"androidx.collection_collection-ktx",
+ "androidx.core_core-ktx",
"guava",
],
}
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreFileArchiver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreFileArchiver.kt
new file mode 100644
index 0000000..cb1b072
--- /dev/null
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreFileArchiver.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.datastore
+
+import android.app.backup.BackupDataInputStream
+import android.content.Context
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+
+/**
+ * File archiver to handle backup and restore for all the [BackupRestoreFileStorage] subclasses.
+ *
+ * Compared with [android.app.backup.FileBackupHelper], this class supports forward-compatibility
+ * like the [com.google.android.libraries.backup.PersistentBackupAgentHelper]: the app does not need
+ * to know the list of files in advance at restore time.
+ */
+internal class BackupRestoreFileArchiver(
+ private val context: Context,
+ private val fileStorages: List<BackupRestoreFileStorage>,
+) : BackupRestoreStorage() {
+ override val name: String
+ get() = "file_archiver"
+
+ override fun createBackupRestoreEntities(): List<BackupRestoreEntity> =
+ fileStorages.map { it.toBackupRestoreEntity() }
+
+ override fun restoreEntity(data: BackupDataInputStream) {
+ val key = data.key
+ val fileStorage = fileStorages.firstOrNull { it.storageFilePath == key }
+ val file =
+ if (fileStorage != null) {
+ if (!fileStorage.enableRestore()) {
+ Log.i(LOG_TAG, "[$name] $key restore disabled")
+ return
+ }
+ fileStorage.restoreFile
+ } else { // forward-compatibility
+ Log.i(LOG_TAG, "Restore unknown file $key")
+ File(context.dataDirCompat, key)
+ }
+ Log.i(LOG_TAG, "[$name] Restore ${data.size()} bytes for $key to $file")
+ try {
+ file.parentFile?.mkdirs() // ensure parent folders are created
+ val wrappedInputStream = wrapRestoreInputStream(data)
+ file.outputStream().use { wrappedInputStream.copyTo(it) }
+ Log.i(LOG_TAG, "[$name] $key restored")
+ fileStorage?.onRestoreFinished(file)
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "[$name] Fail to restore $key", e)
+ }
+ }
+
+ override fun writeNewStateDescription(newState: ParcelFileDescriptor) =
+ fileStorages.forEach { it.writeNewStateDescription(newState) }
+}
+
+private fun BackupRestoreFileStorage.toBackupRestoreEntity() =
+ object : BackupRestoreEntity {
+ override val key: String
+ get() = storageFilePath
+
+ override fun backup(
+ backupContext: BackupContext,
+ outputStream: OutputStream
+ ): EntityBackupResult {
+ if (!enableBackup(backupContext)) {
+ Log.i(LOG_TAG, "[$name] $key backup disabled")
+ return EntityBackupResult.INTACT
+ }
+ val file = backupFile
+ prepareBackup(file)
+ if (!file.exists()) {
+ Log.i(LOG_TAG, "[$name] $key not exist")
+ return EntityBackupResult.DELETE
+ }
+ val wrappedOutputStream = wrapBackupOutputStream(outputStream)
+ file.inputStream().use { it.copyTo(wrappedOutputStream) }
+ onBackupFinished(file)
+ return EntityBackupResult.UPDATE
+ }
+
+ override fun restore(restoreContext: RestoreContext, inputStream: InputStream) {
+ // no-op, BackupRestoreFileArchiver#restoreEntity will restore files
+ }
+ }
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreFileStorage.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreFileStorage.kt
new file mode 100644
index 0000000..b531bd1
--- /dev/null
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreFileStorage.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.datastore
+
+import android.content.Context
+import androidx.core.content.ContextCompat
+import java.io.File
+
+/**
+ * A file-based storage with backup and restore support.
+ *
+ * [BackupRestoreFileArchiver] will handle the backup and restore based on file path for all
+ * subclasses.
+ *
+ * @param context Context to retrieve data dir
+ * @param storageFilePath Storage file path, which MUST be relative to the [Context.getDataDir]
+ * folder. This is used as the entity name for backup and restore.
+ */
+abstract class BackupRestoreFileStorage(
+ val context: Context,
+ val storageFilePath: String,
+) : BackupRestoreStorage() {
+
+ /** The absolute path of the file to backup. */
+ open val backupFile: File
+ get() = File(context.dataDirCompat, storageFilePath)
+
+ /** The absolute path of the file to restore. */
+ open val restoreFile: File
+ get() = backupFile
+
+ fun checkFilePaths() {
+ if (storageFilePath.isEmpty() || storageFilePath[0] == File.separatorChar) {
+ throw IllegalArgumentException("$storageFilePath is not valid path")
+ }
+ if (!backupFile.isAbsolute) {
+ throw IllegalArgumentException("backupFile is not absolute")
+ }
+ if (!restoreFile.isAbsolute) {
+ throw IllegalArgumentException("restoreFile is not absolute")
+ }
+ }
+
+ /**
+ * Callback before [backupFile] is backed up.
+ *
+ * @param file equals to [backupFile]
+ */
+ open fun prepareBackup(file: File) {}
+
+ /**
+ * Callback when [backupFile] is restored.
+ *
+ * @param file equals to [backupFile]
+ */
+ open fun onBackupFinished(file: File) {}
+
+ /**
+ * Callback when [restoreFile] is restored.
+ *
+ * @param file equals to [restoreFile]
+ */
+ open fun onRestoreFinished(file: File) {}
+
+ final override fun createBackupRestoreEntities(): List<BackupRestoreEntity> = listOf()
+}
+
+internal val Context.dataDirCompat: File
+ get() = ContextCompat.getDataDir(this)!!
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
index 221e2e8..0e39493 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorageManager.kt
@@ -46,12 +46,22 @@
/**
* Adds all the registered [BackupRestoreStorage] as the helpers of given [BackupAgentHelper].
*
+ * All [BackupRestoreFileStorage]s will be wrapped as a single [BackupRestoreFileArchiver].
+ *
* @see BackupAgentHelper.addHelper
*/
fun addBackupAgentHelpers(backupAgentHelper: BackupAgentHelper) {
+ val fileStorages = mutableListOf<BackupRestoreFileStorage>()
for ((keyPrefix, storage) in storages) {
- backupAgentHelper.addHelper(keyPrefix, storage)
+ if (storage is BackupRestoreFileStorage) {
+ fileStorages.add(storage)
+ } else {
+ backupAgentHelper.addHelper(keyPrefix, storage)
+ }
}
+ // Always add file archiver even fileStorages is empty to handle forward compatibility
+ val fileArchiver = BackupRestoreFileArchiver(application, fileStorages)
+ backupAgentHelper.addHelper(fileArchiver.name, fileArchiver)
}
/**
@@ -87,6 +97,7 @@
* The storage MUST implement [KeyedObservable] or [Observable].
*/
fun add(storage: BackupRestoreStorage) {
+ if (storage is BackupRestoreFileStorage) storage.checkFilePaths()
val name = storage.name
val oldStorage = storages.put(name, storage)
if (oldStorage != null) {