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

Commit 27decd8e authored by Jacky Wang's avatar Jacky Wang Committed by Android (Google) Code Review
Browse files

Merge changes from topic "datastore-file" into main

* changes:
  [DataStore] Support backup data state computation
  [DataStore] Support backup data with compression
  [DataStore] Provide SharedPreferencesStorage
  [DataStore] Provide BackupRestoreFileStorage
parents 10fe5e6e 3455f82a
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -11,6 +11,8 @@ android_library {
    static_libs: [
        "androidx.annotation_annotation",
        "androidx.collection_collection-ktx",
        "androidx.core_core-ktx",
        "guava",
    ],
    kotlincflags: ["-Xjvm-default=all"],
}
+98 −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 androidx.annotation.IntDef
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.InflaterInputStream

/** Unique id of the codec. */
@Target(AnnotationTarget.TYPE)
@IntDef(
    BackupCodecId.NO_OP.toInt(),
    BackupCodecId.ZIP.toInt(),
)
@Retention(AnnotationRetention.SOURCE)
annotation class BackupCodecId {
    companion object {
        /** Unknown reason of the change. */
        const val NO_OP: Byte = 0
        /** Data is updated. */
        const val ZIP: Byte = 1
    }
}

/** How to encode/decode the backup data. */
interface BackupCodec {
    /** Unique id of the codec. */
    val id: @BackupCodecId Byte

    /** Name of the codec. */
    val name: String

    /** Encodes the backup data. */
    fun encode(outputStream: OutputStream): OutputStream

    /** Decodes the backup data. */
    fun decode(inputStream: InputStream): InputStream

    companion object {
        @JvmStatic
        fun fromId(id: @BackupCodecId Byte): BackupCodec =
            when (id) {
                BackupCodecId.NO_OP -> BackupNoOpCodec()
                BackupCodecId.ZIP -> BackupZipCodec.BEST_COMPRESSION
                else -> throw IllegalArgumentException("Unknown codec id $id")
            }
    }
}

/** Codec without any additional encoding/decoding. */
class BackupNoOpCodec : BackupCodec {
    override val id
        get() = BackupCodecId.NO_OP

    override val name
        get() = "N/A"

    override fun encode(outputStream: OutputStream) = outputStream

    override fun decode(inputStream: InputStream) = inputStream
}

/** Codec with ZIP compression. */
class BackupZipCodec(
    private val compressionLevel: Int,
    override val name: String,
) : BackupCodec {
    override val id
        get() = BackupCodecId.ZIP

    override fun encode(outputStream: OutputStream) =
        DeflaterOutputStream(outputStream, Deflater(compressionLevel))

    override fun decode(inputStream: InputStream) = InflaterInputStream(inputStream)

    companion object {
        val DEFAULT_COMPRESSION = BackupZipCodec(Deflater.DEFAULT_COMPRESSION, "ZipDefault")
        val BEST_COMPRESSION = BackupZipCodec(Deflater.BEST_COMPRESSION, "ZipBestCompression")
        val BEST_SPEED = BackupZipCodec(Deflater.BEST_SPEED, "ZipBestSpeed")
    }
}
+6 −18
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ 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

/**
@@ -31,23 +30,8 @@ import androidx.annotation.RequiresApi
 */
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.
@@ -68,5 +52,9 @@ internal constructor(
        @RequiresApi(Build.VERSION_CODES.P) get() = data.transportFlags
}

/** Context for restore. */
class RestoreContext(val key: String)
/**
 * Context for restore.
 *
 * @param key Entity key
 */
class RestoreContext internal constructor(val key: String)
+7 −0
Original line number Diff line number Diff line
@@ -35,6 +35,13 @@ interface BackupRestoreEntity {
     */
    val key: String

    /**
     * Codec used to encode/decode the backup data.
     *
     * When it is null, the [BackupRestoreStorage.defaultCodec] will be used.
     */
    fun codec(): BackupCodec? = null

    /**
     * Backs up the entity.
     *
+123 −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.util.Log
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.CheckedInputStream

/**
 * 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 wrapBackupOutputStream(codec: BackupCodec, outputStream: OutputStream) =
        outputStream

    override fun wrapRestoreInputStream(codec: BackupCodec, inputStream: InputStream) = inputStream

    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")
        val inputStream = LimitedNoCloseInputStream(data)
        checksum.reset()
        val checkedInputStream = CheckedInputStream(inputStream, checksum)
        try {
            val codec = BackupCodec.fromId(checkedInputStream.read().toByte())
            if (fileStorage != null && fileStorage.defaultCodec().id != codec.id) {
                Log.i(
                    LOG_TAG,
                    "[$name] $key different codec: ${codec.id}, ${fileStorage.defaultCodec().id}"
                )
            }
            file.parentFile?.mkdirs() // ensure parent folders are created
            val wrappedInputStream = codec.decode(checkedInputStream)
            val bytesCopied = file.outputStream().use { wrappedInputStream.copyTo(it) }
            Log.i(LOG_TAG, "[$name] $key restore $bytesCopied bytes with ${codec.name}")
            fileStorage?.onRestoreFinished(file)
            entityStates[key] = checksum.value
        } catch (e: Exception) {
            Log.e(LOG_TAG, "[$name] Fail to restore $key", e)
        }
    }

    override fun onRestoreFinished() {
        fileStorages.forEach { it.onRestoreFinished() }
    }
}

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 codec = codec() ?: defaultCodec()
            // MUST close to flush the data
            wrapBackupOutputStream(codec, outputStream).use { stream ->
                val bytesCopied = file.inputStream().use { it.copyTo(stream) }
                Log.i(LOG_TAG, "[$name] $key backup $bytesCopied bytes with ${codec.name}")
            }
            onBackupFinished(file)
            return EntityBackupResult.UPDATE
        }

        override fun restore(restoreContext: RestoreContext, inputStream: InputStream) {
            // no-op, BackupRestoreFileArchiver#restoreEntity will restore files
        }
    }
Loading