| # Datastore library | |
| This library provides consistent API for data management (including backup, | |
| restore, and metrics logging) on Android platform. | |
| Notably, it is designed to be flexible and could be utilized for a wide range of | |
| data store besides the settings preferences. | |
| ## Overview | |
| In the high-level design, a persistent datastore aims to support two key | |
| characteristics: | |
| - **observable**: triggers backup and metrics logging whenever data is | |
| changed. | |
| - **transferable**: offers users with a seamless experience by backing up and | |
| restoring data on to new devices. | |
| More specifically, Android framework supports | |
| [data backup](https://developer.android.com/guide/topics/data/backup) to | |
| preserve user experiences on a new device. And the | |
| [observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) allows to | |
| monitor data change. | |
| ### Backup and restore | |
| Currently, the Android backup framework provides | |
| [BackupAgentHelper](https://developer.android.com/reference/android/app/backup/BackupAgentHelper) | |
| and | |
| [BackupHelper](https://developer.android.com/reference/android/app/backup/BackupHelper) | |
| to facilitate data backup. However, there are several caveats to consider when | |
| implementing `BackupHelper`: | |
| - *performBackup*: The data is updated incrementally but it is not well | |
| documented. The `ParcelFileDescriptor` state parameters are normally ignored | |
| and data is updated even there is no change. | |
| - *restoreEntity*: The implementation must take care not to seek or close the | |
| underlying data source, nor read more than `size()` bytes from the stream | |
| when restore (see | |
| [BackupDataInputStream](https://developer.android.com/reference/android/app/backup/BackupDataInputStream)). | |
| It is possible that a `BackupHelper` interferes with the restore process of | |
| other `BackupHelper`s. | |
| - *writeNewStateDescription*: Existing implementations rarely notice that this | |
| callback is invoked after *all* entities are restored. Instead, they check | |
| if necessary data are all restored in the `restoreEntity` (e.g. | |
| [BatteryBackupHelper](https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Settings/src/com/android/settings/fuelgauge/BatteryBackupHelper.java;l=144;drc=cca804e1ed504e2d477be1e3db00fb881ca32736)), | |
| which is not robust sometimes. | |
| The datastore library will mitigate these problems by providing alternative | |
| APIs. For instance, library users make use of `InputStream` / `OutputStream` to | |
| back up and restore data directly. | |
| ### Observer pattern | |
| In the current implementation, the Android backup framework requires a manual | |
| call to | |
| [BackupManager.dataChanged](https://developer.android.com/reference/android/app/backup/BackupManager#dataChanged\(\)). | |
| However, it's often observed that this API call is forgotten when using | |
| `SharedPreferences`. Additionally, there's a common need to log metrics when | |
| data changed. To address these limitations, datastore API employed the observer | |
| pattern. | |
| ### API design and advantages | |
| Datastore must extend the `BackupRestoreStorage` class (subclass of | |
| [BackupHelper](https://developer.android.com/reference/android/app/backup/BackupHelper)). | |
| The data in a datastore is group by entity, which is represented by | |
| `BackupRestoreEntity`. Basically, a datastore implementation only needs to focus | |
| on the `BackupRestoreEntity`. | |
| If the datastore is key-value based (e.g. `SharedPreferences`), implements the | |
| `KeyedObservable` interface to offer fine-grained observer. Otherwise, | |
| implements `Observable`. There are builtin thread-safe implementations of the | |
| two interfaces (`KeyedDataObservable` / `DataObservable`). If it is Kotlin, use | |
| delegation to simplify the code. | |
| Keep in mind that the implementation should call `KeyedObservable.notifyChange` | |
| / `Observable.notifyChange` whenever internal data is changed, so that the | |
| registered observer will be notified properly. | |
| For `SharedPreferences` use case, leverage the `SharedPreferencesStorage` | |
| directly. To back up other file based storage, extend the | |
| `BackupRestoreFileStorage` class. | |
| Here are some highlights of the library: | |
| - The restore `InputStream` will ensure bounded data are read, and close the | |
| stream is no-op. That being said, all entities are isolated. | |
| - Data checksum is computed automatically, unchanged data will not be sent to | |
| Android backup system. | |
| - Data compression is supported: | |
| - ZIP best compression is enabled by default, no extra effort needs to be | |
| taken. | |
| - It is safe to switch between compression and no compression in future, | |
| the backup data will add 1 byte header to recognize the codec. | |
| - To support other compression algorithms, simply wrap over the | |
| `InputStream` and `OutputStream`. Actually, the checksum is computed in | |
| this way by | |
| [CheckedInputStream](https://developer.android.com/reference/java/util/zip/CheckedInputStream) | |
| and | |
| [CheckedOutputStream](https://developer.android.com/reference/java/util/zip/CheckedOutputStream), | |
| see `BackupRestoreStorage` implementation for more details. | |
| - Enhanced forward compatibility for file is enabled: If a backup includes | |
| data that didn't exist in earlier versions of the app, the data can still be | |
| successfully restored in those older versions. This is achieved by extending | |
| the `BackupRestoreFileStorage` class, and `BackupRestoreFileArchiver` will | |
| treat each file as an entity and do the backup / restore. | |
| - Manual `BackupManager.dataChanged` call is unnecessary now, the framework | |
| will invoke the API automatically. | |
| ## Usages | |
| This section provides [examples](example/ExampleStorage.kt) of datastore. | |
| Here is a datastore with a string data: | |
| ```kotlin | |
| class ExampleStorage : ObservableBackupRestoreStorage() { | |
| @Volatile // field is manipulated by multiple threads, synchronization might be needed | |
| var data: String? = null | |
| private set | |
| @AnyThread | |
| fun setData(data: String?) { | |
| this.data = data | |
| // call notifyChange to trigger backup and metrics logging whenever data is changed | |
| if (data != null) { | |
| notifyChange(ChangeReason.UPDATE) | |
| } else { | |
| notifyChange(ChangeReason.DELETE) | |
| } | |
| } | |
| override val name: String | |
| get() = "ExampleStorage" | |
| override fun createBackupRestoreEntities(): List<BackupRestoreEntity> = | |
| listOf(StringEntity("data")) | |
| override fun enableRestore(): Boolean { | |
| return true // check condition like flag, environment, etc. | |
| } | |
| override fun enableBackup(backupContext: BackupContext): Boolean { | |
| return true // check condition like flag, environment, etc. | |
| } | |
| @BinderThread | |
| private inner class StringEntity(override val key: String) : BackupRestoreEntity { | |
| override fun backup(backupContext: BackupContext, outputStream: OutputStream) = | |
| if (data != null) { | |
| outputStream.write(data!!.toByteArray(UTF_8)) | |
| EntityBackupResult.UPDATE | |
| } else { | |
| EntityBackupResult.DELETE // delete existing backup data | |
| } | |
| override fun restore(restoreContext: RestoreContext, inputStream: InputStream) { | |
| // DO NOT call setData API here, which will trigger notifyChange unexpectedly. | |
| // Under the hood, the datastore library will call notifyChange(ChangeReason.RESTORE) | |
| // later to notify observers. | |
| data = String(inputStream.readBytes(), UTF_8) | |
| // Handle restored data in onRestoreFinished() callback | |
| } | |
| } | |
| override fun onRestoreFinished() { | |
| // TODO: Update state with the restored data. Use this callback instead of "restore()" in | |
| // case the restore action involves several entities. | |
| // NOTE: The library will call notifyChange(ChangeReason.RESTORE) for you | |
| } | |
| } | |
| ``` | |
| And this is a datastore with key value data: | |
| ```kotlin | |
| class ExampleKeyValueStorage : | |
| BackupRestoreStorage(), KeyedObservable<String> by KeyedDataObservable() { | |
| // thread safe data structure | |
| private val map = ConcurrentHashMap<String, String>() | |
| override val name: String | |
| get() = "ExampleKeyValueStorage" | |
| fun updateData(key: String, value: String?) { | |
| if (value != null) { | |
| map[key] = value | |
| notifyChange(ChangeReason.UPDATE) | |
| } else { | |
| map.remove(key) | |
| notifyChange(ChangeReason.DELETE) | |
| } | |
| } | |
| override fun createBackupRestoreEntities(): List<BackupRestoreEntity> = | |
| listOf(createMapBackupRestoreEntity()) | |
| private fun createMapBackupRestoreEntity() = | |
| object : BackupRestoreEntity { | |
| override val key: String | |
| get() = "map" | |
| override fun backup( | |
| backupContext: BackupContext, | |
| outputStream: OutputStream, | |
| ): EntityBackupResult { | |
| // Use TreeMap to achieve predictable and stable order, so that data will not be | |
| // updated to Android backup backend if there is only order change. | |
| val copy = TreeMap(map) | |
| if (copy.isEmpty()) return EntityBackupResult.DELETE | |
| val dataOutputStream = DataOutputStream(outputStream) | |
| dataOutputStream.writeInt(copy.size) | |
| for ((key, value) in copy) { | |
| dataOutputStream.writeUTF(key) | |
| dataOutputStream.writeUTF(value) | |
| } | |
| return EntityBackupResult.UPDATE | |
| } | |
| override fun restore(restoreContext: RestoreContext, inputStream: InputStream) { | |
| val dataInputString = DataInputStream(inputStream) | |
| repeat(dataInputString.readInt()) { | |
| val key = dataInputString.readUTF() | |
| val value = dataInputString.readUTF() | |
| map[key] = value | |
| } | |
| } | |
| } | |
| } | |
| ``` | |
| All the datastore should be added in the application class: | |
| ```kotlin | |
| class ExampleApplication : Application() { | |
| override fun onCreate() { | |
| super.onCreate() | |
| BackupRestoreStorageManager.getInstance(this) | |
| .add(ExampleStorage(), ExampleKeyValueStorage()) | |
| } | |
| } | |
| ``` | |
| Additionally, inject datastore to the custom `BackupAgentHelper` class: | |
| ```kotlin | |
| class ExampleBackupAgent : BackupAgentHelper() { | |
| override fun onCreate() { | |
| super.onCreate() | |
| BackupRestoreStorageManager.getInstance(this).addBackupAgentHelpers(this) | |
| } | |
| override fun onRestoreFinished() { | |
| BackupRestoreStorageManager.getInstance(this).onRestoreFinished() | |
| } | |
| } | |
| ``` | |
| ## Development | |
| Please preserve the code coverage ratio during development. The current line | |
| coverage is **100% (444/444)** and branch coverage is **93.6% (176/188)**. |