[DataStore] Add BackupRestoreStorage
Bug: 325144964
Test: Manual tests with follow up change
Change-Id: I0c5c285c6fbd1c7c7ab8d9c6f3ae09bf97bf53d0
diff --git a/packages/SettingsLib/DataStore/Android.bp b/packages/SettingsLib/DataStore/Android.bp
index c5957c6..868a4a5 100644
--- a/packages/SettingsLib/DataStore/Android.bp
+++ b/packages/SettingsLib/DataStore/Android.bp
@@ -11,5 +11,6 @@
static_libs: [
"androidx.annotation_annotation",
"androidx.collection_collection-ktx",
+ "guava",
],
}
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreContext.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreContext.kt
new file mode 100644
index 0000000..c6d6f77
--- /dev/null
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreContext.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.BackupAgent
+import android.app.backup.BackupDataOutput
+import android.app.backup.BackupHelper
+import android.os.Build
+import android.os.ParcelFileDescriptor
+import androidx.annotation.RequiresApi
+
+/**
+ * Context for backup.
+ *
+ * @see BackupHelper.performBackup
+ * @see BackupDataOutput
+ */
+class BackupContext
+internal constructor(
+ /**
+ * An open, read-only file descriptor pointing to the last backup state provided by the
+ * application. May be null, in which case no prior state is being provided and the application
+ * should perform a full backup.
+ *
+ * TODO: the state should support marshall/unmarshall for incremental back up.
+ */
+ val oldState: ParcelFileDescriptor?,
+
+ /** An open, read/write BackupDataOutput pointing to the backup data destination. */
+ private val data: BackupDataOutput,
+
+ /**
+ * An open, read/write file descriptor pointing to an empty file. The application should record
+ * the final backup.
+ */
+ val newState: ParcelFileDescriptor,
+) {
+ /**
+ * The quota in bytes for the application's current backup operation.
+ *
+ * @see [BackupDataOutput.getQuota]
+ */
+ val quota: Long
+ @RequiresApi(Build.VERSION_CODES.O) get() = data.quota
+
+ /**
+ * Additional information about the backup transport.
+ *
+ * See [BackupAgent] for supported flags.
+ *
+ * @see [BackupDataOutput.getTransportFlags]
+ */
+ val transportFlags: Int
+ @RequiresApi(Build.VERSION_CODES.P) get() = data.transportFlags
+}
+
+/** Context for restore. */
+class RestoreContext(val key: String)
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreEntity.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreEntity.kt
new file mode 100644
index 0000000..6a7ef5a
--- /dev/null
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreEntity.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.BackupDataOutput
+import android.app.backup.BackupHelper
+import androidx.annotation.BinderThread
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+/** Entity for back up and restore. */
+interface BackupRestoreEntity {
+ /**
+ * Key of the entity.
+ *
+ * The key string must be unique within the data set. Note that it is invalid if the first
+ * character is \uFF00 or higher.
+ *
+ * @see BackupDataOutput.writeEntityHeader
+ */
+ val key: String
+
+ /**
+ * Backs up the entity.
+ *
+ * @param backupContext context for backup
+ * @param outputStream output stream to back up data
+ * @return false if backup file is deleted, otherwise true
+ */
+ @BinderThread
+ @Throws(IOException::class)
+ fun backup(backupContext: BackupContext, outputStream: OutputStream): EntityBackupResult
+
+ /**
+ * Restores the entity.
+ *
+ * @param restoreContext context for restore
+ * @param inputStream An open input stream from which the backup data can be read.
+ * @see BackupHelper.restoreEntity
+ */
+ @BinderThread
+ @Throws(IOException::class)
+ fun restore(restoreContext: RestoreContext, inputStream: InputStream)
+}
+
+/** Result of the backup operation. */
+enum class EntityBackupResult {
+ /** Update the entity. */
+ UPDATE,
+ /** Leave the entity intact. */
+ INTACT,
+ /** Delete the entity. */
+ DELETE,
+}
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorage.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorage.kt
new file mode 100644
index 0000000..88d9dd6
--- /dev/null
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/BackupRestoreStorage.kt
@@ -0,0 +1,140 @@
+/*
+ * 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.BackupAgentHelper
+import android.app.backup.BackupDataInputStream
+import android.app.backup.BackupDataOutput
+import android.app.backup.BackupHelper
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import com.google.common.io.ByteStreams
+import java.io.ByteArrayOutputStream
+import java.io.FilterInputStream
+import java.io.InputStream
+import java.io.OutputStream
+
+internal const val LOG_TAG = "BackupRestoreStorage"
+
+/**
+ * Storage with backup and restore support. Subclass must implement either [Observable] or
+ * [KeyedObservable] interface.
+ *
+ * The storage is identified by a unique string [name] and data set is split into entities
+ * ([BackupRestoreEntity]).
+ */
+abstract class BackupRestoreStorage : BackupHelper {
+ /**
+ * A unique string used to disambiguate the various storages within backup agent.
+ *
+ * It will be used as the `keyPrefix` of [BackupAgentHelper.addHelper].
+ */
+ abstract val name: String
+
+ private val entities: List<BackupRestoreEntity> by lazy { createBackupRestoreEntities() }
+
+ /** Entities to back up and restore. */
+ abstract fun createBackupRestoreEntities(): List<BackupRestoreEntity>
+
+ override fun performBackup(
+ oldState: ParcelFileDescriptor?,
+ data: BackupDataOutput,
+ newState: ParcelFileDescriptor,
+ ) {
+ val backupContext = BackupContext(oldState, data, newState)
+ if (!enableBackup(backupContext)) {
+ Log.i(LOG_TAG, "[$name] Backup disabled")
+ return
+ }
+ Log.i(LOG_TAG, "[$name] Backup start")
+ for (entity in entities) {
+ val key = entity.key
+ val outputStream = ByteArrayOutputStream()
+ val result =
+ try {
+ entity.backup(backupContext, wrapBackupOutputStream(outputStream))
+ } catch (exception: Exception) {
+ Log.e(LOG_TAG, "[$name] Fail to backup entity $key", exception)
+ continue
+ }
+ when (result) {
+ EntityBackupResult.UPDATE -> {
+ val payload = outputStream.toByteArray()
+ val size = payload.size
+ data.writeEntityHeader(key, size)
+ data.writeEntityData(payload, size)
+ Log.i(LOG_TAG, "[$name] Backup entity $key: $size bytes")
+ }
+ EntityBackupResult.INTACT -> {
+ Log.i(LOG_TAG, "[$name] Backup entity $key intact")
+ }
+ EntityBackupResult.DELETE -> {
+ data.writeEntityHeader(key, -1)
+ Log.i(LOG_TAG, "[$name] Backup entity $key deleted")
+ }
+ }
+ }
+ Log.i(LOG_TAG, "[$name] Backup end")
+ }
+
+ /** Returns if backup is enabled. */
+ open fun enableBackup(backupContext: BackupContext): Boolean = true
+
+ fun wrapBackupOutputStream(outputStream: OutputStream): OutputStream {
+ return outputStream
+ }
+
+ override fun restoreEntity(data: BackupDataInputStream) {
+ val key = data.key
+ if (!enableRestore()) {
+ Log.i(LOG_TAG, "[$name] Restore disabled, ignore entity $key")
+ return
+ }
+ val entity = entities.firstOrNull { it.key == key }
+ if (entity == null) {
+ Log.w(LOG_TAG, "[$name] Cannot find handler for entity $key")
+ return
+ }
+ Log.i(LOG_TAG, "[$name] Restore $key: ${data.size()} bytes")
+ val restoreContext = RestoreContext(key)
+ try {
+ entity.restore(restoreContext, wrapRestoreInputStream(data))
+ } catch (exception: Exception) {
+ Log.e(LOG_TAG, "[$name] Fail to restore entity $key", exception)
+ }
+ }
+
+ /** Returns if restore is enabled. */
+ open fun enableRestore(): Boolean = true
+
+ fun wrapRestoreInputStream(inputStream: BackupDataInputStream): InputStream {
+ return LimitedNoCloseInputStream(inputStream)
+ }
+
+ override fun writeNewStateDescription(newState: ParcelFileDescriptor) {}
+}
+
+/**
+ * Wrapper of [BackupDataInputStream], limiting the number of bytes that can be read and make
+ * [close] no-op.
+ */
+class LimitedNoCloseInputStream(inputStream: BackupDataInputStream) :
+ FilterInputStream(ByteStreams.limit(inputStream, inputStream.size().toLong())) {
+ override fun close() {
+ // do not close original input stream
+ }
+}