diff options
4 files changed, 282 insertions, 0 deletions
diff --git a/packages/SettingsLib/DataStore/Android.bp b/packages/SettingsLib/DataStore/Android.bp index c5957c600934..868a4a50577e 100644 --- a/packages/SettingsLib/DataStore/Android.bp +++ b/packages/SettingsLib/DataStore/Android.bp @@ -11,5 +11,6 @@ android_library { 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 000000000000..c6d6f772c5df --- /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 000000000000..6a7ef5a35ac8 --- /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 000000000000..88d9dd6400ee --- /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 + } +} |