Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 50784fe9 authored by Jacky Wang's avatar Jacky Wang
Browse files

[DataStore] Provide BackupRestoreFileStorage

Bug: 325144964
Test: Manual tests
Change-Id: I9ad13305321d0c9f5f6dccc46c266443fd45f73e
parent fb91f165
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ android_library {
    static_libs: [
        "androidx.annotation_annotation",
        "androidx.collection_collection-ktx",
        "androidx.core_core-ktx",
        "guava",
    ],
}
+102 −0
Original line number Diff line number Diff line
/*
 * 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
        }
    }
+83 −0
Original line number Diff line number Diff line
/*
 * 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)!!
+12 −1
Original line number Diff line number Diff line
@@ -46,13 +46,23 @@ class BackupRestoreStorageManager private constructor(private val application: A
    /**
     * 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) {
            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)
    }

    /**
     * Callback when restore finished.
@@ -87,6 +97,7 @@ class BackupRestoreStorageManager private constructor(private val application: A
     * 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) {