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

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

Merge "[DataStore] Add more test cases" into main

parents 6fe6643f 28eebac3
Loading
Loading
Loading
Loading
+6 −1
Original line number Original line Diff line number Diff line
@@ -2,12 +2,17 @@ package {
    default_applicable_licenses: ["frameworks_base_license"],
    default_applicable_licenses: ["frameworks_base_license"],
}
}


filegroup {
    name: "SettingsLibDataStore-srcs",
    srcs: ["src/**/*"],
}

android_library {
android_library {
    name: "SettingsLibDataStore",
    name: "SettingsLibDataStore",
    defaults: [
    defaults: [
        "SettingsLintDefaults",
        "SettingsLintDefaults",
    ],
    ],
    srcs: ["src/**/*"],
    srcs: [":SettingsLibDataStore-srcs"],
    static_libs: [
    static_libs: [
        "androidx.annotation_annotation",
        "androidx.annotation_annotation",
        "androidx.collection_collection-ktx",
        "androidx.collection_collection-ktx",
+6 −6
Original line number Original line Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.settingslib.datastore
import android.app.backup.BackupDataInputStream
import android.app.backup.BackupDataInputStream
import android.content.Context
import android.content.Context
import android.util.Log
import android.util.Log
import androidx.annotation.VisibleForTesting
import java.io.File
import java.io.File
import java.io.InputStream
import java.io.InputStream
import java.io.OutputStream
import java.io.OutputStream
@@ -33,11 +34,9 @@ import java.util.zip.CheckedInputStream
 */
 */
internal class BackupRestoreFileArchiver(
internal class BackupRestoreFileArchiver(
    private val context: Context,
    private val context: Context,
    private val fileStorages: List<BackupRestoreFileStorage>,
    @get:VisibleForTesting internal val fileStorages: List<BackupRestoreFileStorage>,
    override val name: String,
) : BackupRestoreStorage() {
) : BackupRestoreStorage() {
    override val name: String
        get() = "file_archiver"

    override fun createBackupRestoreEntities(): List<BackupRestoreEntity> =
    override fun createBackupRestoreEntities(): List<BackupRestoreEntity> =
        fileStorages.map { it.toBackupRestoreEntity() }
        fileStorages.map { it.toBackupRestoreEntity() }


@@ -88,7 +87,8 @@ internal class BackupRestoreFileArchiver(
    }
    }
}
}


private fun BackupRestoreFileStorage.toBackupRestoreEntity() =
@VisibleForTesting
internal fun BackupRestoreFileStorage.toBackupRestoreEntity() =
    object : BackupRestoreEntity {
    object : BackupRestoreEntity {
        override val key: String
        override val key: String
            get() = storageFilePath
            get() = storageFilePath
@@ -107,7 +107,7 @@ private fun BackupRestoreFileStorage.toBackupRestoreEntity() =
                Log.i(LOG_TAG, "[$name] $key not exist")
                Log.i(LOG_TAG, "[$name] $key not exist")
                return EntityBackupResult.DELETE
                return EntityBackupResult.DELETE
            }
            }
            val codec = codec() ?: defaultCodec()
            val codec = defaultCodec()
            // MUST close to flush the data
            // MUST close to flush the data
            wrapBackupOutputStream(codec, outputStream).use { stream ->
            wrapBackupOutputStream(codec, outputStream).use { stream ->
                val bytesCopied = file.inputStream().use { it.copyTo(stream) }
                val bytesCopied = file.inputStream().use { it.copyTo(stream) }
+17 −7
Original line number Original line Diff line number Diff line
@@ -22,6 +22,7 @@ import android.app.backup.BackupDataOutput
import android.app.backup.BackupHelper
import android.app.backup.BackupHelper
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor
import android.util.Log
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.collection.MutableScatterMap
import androidx.collection.MutableScatterMap
import com.google.common.io.ByteStreams
import com.google.common.io.ByteStreams
import java.io.ByteArrayOutputStream
import java.io.ByteArrayOutputStream
@@ -60,10 +61,11 @@ abstract class BackupRestoreStorage : BackupHelper {
     *
     *
     * Map key is the entity key, map value is the checksum of backup data.
     * Map key is the entity key, map value is the checksum of backup data.
     */
     */
    protected val entityStates = MutableScatterMap<String, Long>()
    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
    val entityStates = MutableScatterMap<String, Long>()


    /** Entities created by [createBackupRestoreEntities]. This field is for restore only. */
    /** Entities created by [createBackupRestoreEntities]. This field is for restore only. */
    private var entities: List<BackupRestoreEntity>? = null
    @VisibleForTesting internal var entities: List<BackupRestoreEntity>? = null


    /** Entities to back up and restore. */
    /** Entities to back up and restore. */
    abstract fun createBackupRestoreEntities(): List<BackupRestoreEntity>
    abstract fun createBackupRestoreEntities(): List<BackupRestoreEntity>
@@ -76,7 +78,7 @@ abstract class BackupRestoreStorage : BackupHelper {
        data: BackupDataOutput,
        data: BackupDataOutput,
        newState: ParcelFileDescriptor,
        newState: ParcelFileDescriptor,
    ) {
    ) {
        oldState.readEntityStates(entityStates)
        readEntityStates(oldState, entityStates)
        val backupContext = BackupContext(data)
        val backupContext = BackupContext(data)
        if (!enableBackup(backupContext)) {
        if (!enableBackup(backupContext)) {
            Log.i(LOG_TAG, "[$name] Backup disabled")
            Log.i(LOG_TAG, "[$name] Backup disabled")
@@ -94,7 +96,10 @@ abstract class BackupRestoreStorage : BackupHelper {
            val codec = entity.codec() ?: defaultCodec()
            val codec = entity.codec() ?: defaultCodec()
            val result =
            val result =
                try {
                try {
                    entity.backup(backupContext, wrapBackupOutputStream(codec, checkedOutputStream))
                    // MUST close to flush all data
                    wrapBackupOutputStream(codec, checkedOutputStream).use {
                        entity.backup(backupContext, it)
                    }
                } catch (exception: Exception) {
                } catch (exception: Exception) {
                    Log.e(LOG_TAG, "[$name] Fail to backup entity $key", exception)
                    Log.e(LOG_TAG, "[$name] Fail to backup entity $key", exception)
                    continue
                    continue
@@ -191,9 +196,13 @@ abstract class BackupRestoreStorage : BackupHelper {
    /** Callbacks when restore finished. */
    /** Callbacks when restore finished. */
    open fun onRestoreFinished() {}
    open fun onRestoreFinished() {}


    private fun ParcelFileDescriptor?.readEntityStates(state: MutableScatterMap<String, Long>) {
    @VisibleForTesting
    internal fun readEntityStates(
        parcelFileDescriptor: ParcelFileDescriptor?,
        state: MutableScatterMap<String, Long>,
    ) {
        state.clear()
        state.clear()
        if (this == null) return
        val fileDescriptor = parcelFileDescriptor?.fileDescriptor ?: return
        // do not close the streams
        // do not close the streams
        val fileInputStream = FileInputStream(fileDescriptor)
        val fileInputStream = FileInputStream(fileDescriptor)
        val dataInputStream = DataInputStream(fileInputStream)
        val dataInputStream = DataInputStream(fileInputStream)
@@ -233,6 +242,7 @@ abstract class BackupRestoreStorage : BackupHelper {
                dataOutputStream.writeUTF(key)
                dataOutputStream.writeUTF(key)
                dataOutputStream.writeLong(value)
                dataOutputStream.writeLong(value)
            }
            }
            dataOutputStream.flush()
        } catch (exception: Exception) {
        } catch (exception: Exception) {
            Log.e(LOG_TAG, "[$name] Fail to write state file", exception)
            Log.e(LOG_TAG, "[$name] Fail to write state file", exception)
        }
        }
@@ -241,7 +251,7 @@ abstract class BackupRestoreStorage : BackupHelper {
    }
    }


    companion object {
    companion object {
        private const val STATE_VERSION: Byte = 0
        internal const val STATE_VERSION: Byte = 0


        /** Checksum for entity backup data. */
        /** Checksum for entity backup data. */
        fun createChecksum(): Checksum = CRC32()
        fun createChecksum(): Checksum = CRC32()
+16 −6
Original line number Original line Diff line number Diff line
@@ -21,23 +21,32 @@ import android.app.backup.BackupAgentHelper
import android.app.backup.BackupManager
import android.app.backup.BackupManager
import android.content.Context
import android.content.Context
import android.util.Log
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.google.common.util.concurrent.MoreExecutors
import com.google.common.util.concurrent.MoreExecutors
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap


/** Manager of [BackupRestoreStorage]. */
/** Manager of [BackupRestoreStorage]. */
class BackupRestoreStorageManager private constructor(private val application: Application) {
class BackupRestoreStorageManager private constructor(private val application: Application) {
    private val storageWrappers = ConcurrentHashMap<String, StorageWrapper>()
    @VisibleForTesting internal val storageWrappers = ConcurrentHashMap<String, StorageWrapper>()


    private val executor = MoreExecutors.directExecutor()
    private val executor = MoreExecutors.directExecutor()


    /**
    /**
     * Adds all the registered [BackupRestoreStorage] as the helpers of given [BackupAgentHelper].
     * Adds all the registered [BackupRestoreStorage] as the helpers of given [BackupAgentHelper].
     *
     *
     * All [BackupRestoreFileStorage]s will be wrapped as a single [BackupRestoreFileArchiver].
     * All [BackupRestoreFileStorage]s will be wrapped as a single [BackupRestoreFileArchiver],
     * specify [fileArchiverName] to avoid key prefix conflict if needed.
     *
     *
     * @param backupAgentHelper backup agent helper to add helpers
     * @param fileArchiverName key prefix of the [BackupRestoreFileArchiver], the value must not be
     *   changed in future
     * @see BackupAgentHelper.addHelper
     * @see BackupAgentHelper.addHelper
     */
     */
    fun addBackupAgentHelpers(backupAgentHelper: BackupAgentHelper) {
    @JvmOverloads
    fun addBackupAgentHelpers(
        backupAgentHelper: BackupAgentHelper,
        fileArchiverName: String = "file_archiver",
    ) {
        val fileStorages = mutableListOf<BackupRestoreFileStorage>()
        val fileStorages = mutableListOf<BackupRestoreFileStorage>()
        for ((keyPrefix, storageWrapper) in storageWrappers) {
        for ((keyPrefix, storageWrapper) in storageWrappers) {
            val storage = storageWrapper.storage
            val storage = storageWrapper.storage
@@ -48,7 +57,7 @@ class BackupRestoreStorageManager private constructor(private val application: A
            }
            }
        }
        }
        // Always add file archiver even fileStorages is empty to handle forward compatibility
        // Always add file archiver even fileStorages is empty to handle forward compatibility
        val fileArchiver = BackupRestoreFileArchiver(application, fileStorages)
        val fileArchiver = BackupRestoreFileArchiver(application, fileStorages, fileArchiverName)
        backupAgentHelper.addHelper(fileArchiver.name, fileArchiver)
        backupAgentHelper.addHelper(fileArchiver.name, fileArchiver)
    }
    }


@@ -106,7 +115,8 @@ class BackupRestoreStorageManager private constructor(private val application: A
    /** Returns storage with given name, exception is raised if not found. */
    /** Returns storage with given name, exception is raised if not found. */
    fun getOrThrow(name: String): BackupRestoreStorage = storageWrappers[name]!!.storage
    fun getOrThrow(name: String): BackupRestoreStorage = storageWrappers[name]!!.storage


    private inner class StorageWrapper(val storage: BackupRestoreStorage) :
    @VisibleForTesting
    internal inner class StorageWrapper(val storage: BackupRestoreStorage) :
        Observer, KeyedObserver<Any?> {
        Observer, KeyedObserver<Any?> {
        init {
        init {
            when (storage) {
            when (storage) {
@@ -139,7 +149,7 @@ class BackupRestoreStorageManager private constructor(private val application: A
                LOG_TAG,
                LOG_TAG,
                "Notify BackupManager dataChanged: storage=$name key=$key reason=$reason"
                "Notify BackupManager dataChanged: storage=$name key=$key reason=$reason"
            )
            )
            BackupManager.dataChanged(application.packageName)
            BackupManager(application).dataChanged()
        }
        }


        fun removeObserver() {
        fun removeObserver() {
+32 −14
Original line number Original line Diff line number Diff line
@@ -20,9 +20,11 @@ import android.content.Context
import android.content.SharedPreferences
import android.content.SharedPreferences
import android.os.Build
import android.os.Build
import android.util.Log
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.annotation.VisibleForTesting
import java.io.File
import java.io.File


private fun defaultVerbose() = Build.TYPE == "eng"

/**
/**
 * [SharedPreferences] based storage.
 * [SharedPreferences] based storage.
 *
 *
@@ -43,24 +45,35 @@ import java.io.File
 * @param verbose Verbose logging on key/value pairs during backup/restore. Enable for dev only!
 * @param verbose Verbose logging on key/value pairs during backup/restore. Enable for dev only!
 * @param filter Filter of key/value pairs for backup and restore.
 * @param filter Filter of key/value pairs for backup and restore.
 */
 */
class SharedPreferencesStorage
open class SharedPreferencesStorage
@JvmOverloads
@JvmOverloads
constructor(
constructor(
    context: Context,
    context: Context,
    override val name: String,
    override val name: String,
    mode: Int,
    @get:VisibleForTesting internal val sharedPreferences: SharedPreferences,
    private val verbose: Boolean = (Build.TYPE == "eng"),
    private val codec: BackupCodec? = null,
    private val verbose: Boolean = defaultVerbose(),
    private val filter: (String, Any?) -> Boolean = { _, _ -> true },
    private val filter: (String, Any?) -> Boolean = { _, _ -> true },
) :
) :
    BackupRestoreFileStorage(context, context.getSharedPreferencesFilePath(name)),
    BackupRestoreFileStorage(context, context.getSharedPreferencesFilePath(name)),
    KeyedObservable<String> by KeyedDataObservable() {
    KeyedObservable<String> by KeyedDataObservable() {


    private val sharedPreferences = context.getSharedPreferences(name, mode)
    @JvmOverloads
    constructor(
        context: Context,
        name: String,
        mode: Int,
        codec: BackupCodec? = null,
        verbose: Boolean = defaultVerbose(),
        filter: (String, Any?) -> Boolean = { _, _ -> true },
    ) : this(context, name, context.getSharedPreferences(name, mode), codec, verbose, filter)


    /** Name of the intermediate SharedPreferences. */
    /** Name of the intermediate SharedPreferences. */
    private val intermediateName: String
    @VisibleForTesting
    internal val intermediateName: String
        get() = "_br_$name"
        get() = "_br_$name"


    @Suppress("DEPRECATION")
    private val intermediateSharedPreferences: SharedPreferences
    private val intermediateSharedPreferences: SharedPreferences
        get() {
        get() {
            // use MODE_MULTI_PROCESS to ensure a reload
            // use MODE_MULTI_PROCESS to ensure a reload
@@ -82,12 +95,15 @@ constructor(
        sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
        sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
    }
    }


    override fun defaultCodec() = codec ?: super.defaultCodec()

    override val backupFile: File
    override val backupFile: File
        // use a different file to avoid multi-thread file write
        // use a different file to avoid multi-thread file write
        get() = context.getSharedPreferencesFile(intermediateName)
        get() = context.getSharedPreferencesFile(intermediateName)


    override fun prepareBackup(file: File) {
    override fun prepareBackup(file: File) {
        val editor = intermediateSharedPreferences.merge(sharedPreferences.all, "Backup")
        val editor =
            mergeSharedPreferences(intermediateSharedPreferences, sharedPreferences.all, "Backup")
        // commit to ensure data is write to disk synchronously
        // commit to ensure data is write to disk synchronously
        if (!editor.commit()) {
        if (!editor.commit()) {
            Log.w(LOG_TAG, "[$name] fail to commit")
            Log.w(LOG_TAG, "[$name] fail to commit")
@@ -104,8 +120,8 @@ constructor(
        // observers consistently once restore finished.
        // observers consistently once restore finished.
        sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
        sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener)
        val restored = intermediateSharedPreferences
        val restored = intermediateSharedPreferences
        val editor = sharedPreferences.merge(restored.all, "Restore")
        val editor = mergeSharedPreferences(sharedPreferences, restored.all, "Restore")
        editor.apply() // apply to avoid blocking
        editor.commit() // commit to avoid race condition
        sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
        sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
        // clear the intermediate SharedPreferences
        // clear the intermediate SharedPreferences
        restored.delete(intermediateName)
        restored.delete(intermediateName)
@@ -115,7 +131,7 @@ constructor(
        if (deleteSharedPreferences(name)) {
        if (deleteSharedPreferences(name)) {
            Log.i(LOG_TAG, "SharedPreferences $name deleted")
            Log.i(LOG_TAG, "SharedPreferences $name deleted")
        } else {
        } else {
            edit().clear().apply()
            edit().clear().commit() // commit to avoid potential race condition
        }
        }
    }
    }


@@ -126,11 +142,13 @@ constructor(
            false
            false
        }
        }


    private fun SharedPreferences.merge(
    @VisibleForTesting
    internal open fun mergeSharedPreferences(
        sharedPreferences: SharedPreferences,
        entries: Map<String, Any?>,
        entries: Map<String, Any?>,
        operation: String
        operation: String,
    ): SharedPreferences.Editor {
    ): SharedPreferences.Editor {
        val editor = edit()
        val editor = sharedPreferences.edit()
        for ((key, value) in entries) {
        for ((key, value) in entries) {
            if (!filter.invoke(key, value)) {
            if (!filter.invoke(key, value)) {
                if (verbose) Log.v(LOG_TAG, "[$name] $operation skips $key=$value")
                if (verbose) Log.v(LOG_TAG, "[$name] $operation skips $key=$value")
@@ -184,7 +202,7 @@ constructor(
    companion object {
    companion object {
        private fun Context.getSharedPreferencesFilePath(name: String): String {
        private fun Context.getSharedPreferencesFilePath(name: String): String {
            val file = getSharedPreferencesFile(name)
            val file = getSharedPreferencesFile(name)
            return file.relativeTo(ContextCompat.getDataDir(this)!!).toString()
            return file.relativeTo(dataDirCompat).toString()
        }
        }


        /** Returns the absolute path of shared preferences file. */
        /** Returns the absolute path of shared preferences file. */
Loading