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

Commit 3455f82a authored by Jacky Wang's avatar Jacky Wang
Browse files

[DataStore] Support backup data state computation

Entity data is computed with CRC32 checksum and saved to state file automatically.

Bug: 325144964
Test: Manual tests
Change-Id: Ib74178fa87c1a11c39a703f6be409b33da00a00e
parent 4303555e
Loading
Loading
Loading
Loading
+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)
+9 −5
Original line number Diff line number Diff line
@@ -18,11 +18,11 @@ 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
import java.util.zip.CheckedInputStream

/**
 * File archiver to handle backup and restore for all the [BackupRestoreFileStorage] subclasses.
@@ -62,8 +62,10 @@ internal class BackupRestoreFileArchiver(
            }
        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(inputStream.read().toByte())
            val codec = BackupCodec.fromId(checkedInputStream.read().toByte())
            if (fileStorage != null && fileStorage.defaultCodec().id != codec.id) {
                Log.i(
                    LOG_TAG,
@@ -71,17 +73,19 @@ internal class BackupRestoreFileArchiver(
                )
            }
            file.parentFile?.mkdirs() // ensure parent folders are created
            val wrappedInputStream = codec.decode(inputStream)
            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 writeNewStateDescription(newState: ParcelFileDescriptor) =
        fileStorages.forEach { it.writeNewStateDescription(newState) }
    override fun onRestoreFinished() {
        fileStorages.forEach { it.onRestoreFinished() }
    }
}

private fun BackupRestoreFileStorage.toBackupRestoreEntity() =
+110 −12
Original line number Diff line number Diff line
@@ -22,11 +22,20 @@ import android.app.backup.BackupDataOutput
import android.app.backup.BackupHelper
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.collection.MutableScatterMap
import com.google.common.io.ByteStreams
import java.io.ByteArrayOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.EOFException
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.FilterInputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.CRC32
import java.util.zip.CheckedInputStream
import java.util.zip.CheckedOutputStream

internal const val LOG_TAG = "BackupRestoreStorage"

@@ -47,6 +56,20 @@ abstract class BackupRestoreStorage : BackupHelper {

    private val entities: List<BackupRestoreEntity> by lazy { createBackupRestoreEntities() }

    /**
     * Checksum of the data.
     *
     * Always call [java.util.zip.Checksum.reset] before using it.
     */
    protected val checksum = CRC32()

    /**
     * Entity states represented by checksum.
     *
     * Map key is the entity key, map value is the checksum of backup data.
     */
    protected val entityStates = MutableScatterMap<String, Long>()

    /** Entities to back up and restore. */
    abstract fun createBackupRestoreEntities(): List<BackupRestoreEntity>

@@ -58,7 +81,8 @@ abstract class BackupRestoreStorage : BackupHelper {
        data: BackupDataOutput,
        newState: ParcelFileDescriptor,
    ) {
        val backupContext = BackupContext(oldState, data, newState)
        oldState.readEntityStates(entityStates)
        val backupContext = BackupContext(data)
        if (!enableBackup(backupContext)) {
            Log.i(LOG_TAG, "[$name] Backup disabled")
            return
@@ -67,34 +91,50 @@ abstract class BackupRestoreStorage : BackupHelper {
        for (entity in entities) {
            val key = entity.key
            val outputStream = ByteArrayOutputStream()
            checksum.reset()
            val checkedOutputStream = CheckedOutputStream(outputStream, checksum)
            val codec = entity.codec() ?: defaultCodec()
            val result =
                try {
                    entity.backup(backupContext, wrapBackupOutputStream(codec, outputStream))
                    entity.backup(backupContext, wrapBackupOutputStream(codec, checkedOutputStream))
                } catch (exception: Exception) {
                    Log.e(LOG_TAG, "[$name] Fail to backup entity $key", exception)
                    continue
                }
            when (result) {
                EntityBackupResult.UPDATE -> {
                    if (updateEntityState(key)) {
                        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")
                    } else {
                        Log.i(
                            LOG_TAG,
                            "[$name] Backup entity $key unchanged: ${outputStream.size()} bytes"
                        )
                    }
                }
                EntityBackupResult.INTACT -> {
                    Log.i(LOG_TAG, "[$name] Backup entity $key intact")
                }
                EntityBackupResult.DELETE -> {
                    entityStates.remove(key)
                    data.writeEntityHeader(key, -1)
                    Log.i(LOG_TAG, "[$name] Backup entity $key deleted")
                }
            }
        }
        newState.writeEntityStates(entityStates)
        Log.i(LOG_TAG, "[$name] Backup end")
    }

    private fun updateEntityState(key: String): Boolean {
        val value = checksum.value
        return entityStates.put(key, value) != value
    }

    /** Returns if backup is enabled. */
    open fun enableBackup(backupContext: BackupContext): Boolean = true

@@ -118,11 +158,12 @@ abstract class BackupRestoreStorage : BackupHelper {
        Log.i(LOG_TAG, "[$name] Restore $key: ${data.size()} bytes")
        val restoreContext = RestoreContext(key)
        val codec = entity.codec() ?: defaultCodec()
        val inputStream = LimitedNoCloseInputStream(data)
        checksum.reset()
        val checkedInputStream = CheckedInputStream(inputStream, checksum)
        try {
            entity.restore(
                restoreContext,
                wrapRestoreInputStream(codec, LimitedNoCloseInputStream(data))
            )
            entity.restore(restoreContext, wrapRestoreInputStream(codec, checkedInputStream))
            entityStates[key] = checksum.value
        } catch (exception: Exception) {
            Log.e(LOG_TAG, "[$name] Fail to restore entity $key", exception)
        }
@@ -143,7 +184,64 @@ abstract class BackupRestoreStorage : BackupHelper {
        return BackupCodec.fromId(id.toByte()).decode(inputStream)
    }

    override fun writeNewStateDescription(newState: ParcelFileDescriptor) {}
    final override fun writeNewStateDescription(newState: ParcelFileDescriptor) {
        newState.writeEntityStates(entityStates)
        onRestoreFinished()
    }

    /** Callbacks when restore finished. */
    open fun onRestoreFinished() {}

    private fun ParcelFileDescriptor?.readEntityStates(state: MutableScatterMap<String, Long>) {
        state.clear()
        if (this == null) return
        // do not close the streams
        val fileInputStream = FileInputStream(fileDescriptor)
        val dataInputStream = DataInputStream(fileInputStream)
        try {
            val version = dataInputStream.readByte()
            if (version != STATE_VERSION) {
                Log.w(
                    LOG_TAG,
                    "[$name] Unexpected state version, read:$version, expected:$STATE_VERSION"
                )
                return
            }
            var count = dataInputStream.readInt()
            while (count-- > 0) {
                val key = dataInputStream.readUTF()
                val checksum = dataInputStream.readLong()
                state[key] = checksum
            }
        } catch (exception: Exception) {
            if (exception is EOFException) {
                Log.d(LOG_TAG, "[$name] Hit EOF when read state file")
            } else {
                Log.e(LOG_TAG, "[$name] Fail to read state file", exception)
            }
            state.clear()
        }
    }

    private fun ParcelFileDescriptor.writeEntityStates(state: MutableScatterMap<String, Long>) {
        // do not close the streams
        val fileOutputStream = FileOutputStream(fileDescriptor)
        val dataOutputStream = DataOutputStream(fileOutputStream)
        try {
            dataOutputStream.writeByte(STATE_VERSION.toInt())
            dataOutputStream.writeInt(state.size)
            state.forEach { key, value ->
                dataOutputStream.writeUTF(key)
                dataOutputStream.writeLong(value)
            }
        } catch (exception: Exception) {
            Log.e(LOG_TAG, "[$name] Fail to write state file", exception)
        }
    }

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

/**