1# Datastore library 2 3This library provides consistent API for data management (including backup, 4restore, and metrics logging) on Android platform. 5 6Notably, it is designed to be flexible and could be utilized for a wide range of 7data store besides the settings preferences. 8 9## Overview 10 11In the high-level design, a persistent datastore aims to support two key 12characteristics: 13 14- **observable**: triggers backup and metrics logging whenever data is 15 changed. 16- **transferable**: offers users with a seamless experience by backing up and 17 restoring data on to new devices. 18 19More specifically, Android framework supports 20[data backup](https://developer.android.com/guide/topics/data/backup) to 21preserve user experiences on a new device. And the 22[observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) allows to 23monitor data change. 24 25### Backup and restore 26 27Currently, the Android backup framework provides 28[BackupAgentHelper](https://developer.android.com/reference/android/app/backup/BackupAgentHelper) 29and 30[BackupHelper](https://developer.android.com/reference/android/app/backup/BackupHelper) 31to facilitate data backup. However, there are several caveats to consider when 32implementing `BackupHelper`: 33 34- *performBackup*: The data is updated incrementally but it is not well 35 documented. The `ParcelFileDescriptor` state parameters are normally ignored 36 and data is updated even there is no change. 37- *restoreEntity*: The implementation must take care not to seek or close the 38 underlying data source, nor read more than `size()` bytes from the stream 39 when restore (see 40 [BackupDataInputStream](https://developer.android.com/reference/android/app/backup/BackupDataInputStream)). 41 It is possible that a `BackupHelper` interferes with the restore process of 42 other `BackupHelper`s. 43- *writeNewStateDescription*: Existing implementations rarely notice that this 44 callback is invoked after *all* entities are restored. Instead, they check 45 if necessary data are all restored in the `restoreEntity` (e.g. 46 [BatteryBackupHelper](https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Settings/src/com/android/settings/fuelgauge/BatteryBackupHelper.java;l=144;drc=cca804e1ed504e2d477be1e3db00fb881ca32736)), 47 which is not robust sometimes. 48 49The datastore library will mitigate these problems by providing alternative 50APIs. For instance, library users make use of `InputStream` / `OutputStream` to 51back up and restore data directly. 52 53### Observer pattern 54 55In the current implementation, the Android backup framework requires a manual 56call to 57[BackupManager.dataChanged](https://developer.android.com/reference/android/app/backup/BackupManager#dataChanged\(\)). 58However, it's often observed that this API call is forgotten when using 59`SharedPreferences`. Additionally, there's a common need to log metrics when 60data changed. To address these limitations, datastore API employed the observer 61pattern. 62 63### API design and advantages 64 65Datastore must extend the `BackupRestoreStorage` class (subclass of 66[BackupHelper](https://developer.android.com/reference/android/app/backup/BackupHelper)). 67The data in a datastore is group by entity, which is represented by 68`BackupRestoreEntity`. Basically, a datastore implementation only needs to focus 69on the `BackupRestoreEntity`. 70 71If the datastore is key-value based (e.g. `SharedPreferences`), implements the 72`KeyedObservable` interface to offer fine-grained observer. Otherwise, 73implements `Observable`. There are builtin thread-safe implementations of the 74two interfaces (`KeyedDataObservable` / `DataObservable`). If it is Kotlin, use 75delegation to simplify the code. 76 77Keep in mind that the implementation should call `KeyedObservable.notifyChange` 78/ `Observable.notifyChange` whenever internal data is changed, so that the 79registered observer will be notified properly. 80 81For `SharedPreferences` use case, leverage the `SharedPreferencesStorage` 82directly. To back up other file based storage, extend the 83`BackupRestoreFileStorage` class. 84 85Here are some highlights of the library: 86 87- The restore `InputStream` will ensure bounded data are read, and close the 88 stream is no-op. That being said, all entities are isolated. 89- Data checksum is computed automatically, unchanged data will not be sent to 90 Android backup system. 91- Data compression is supported: 92 - ZIP best compression is enabled by default, no extra effort needs to be 93 taken. 94 - It is safe to switch between compression and no compression in future, 95 the backup data will add 1 byte header to recognize the codec. 96 - To support other compression algorithms, simply wrap over the 97 `InputStream` and `OutputStream`. Actually, the checksum is computed in 98 this way by 99 [CheckedInputStream](https://developer.android.com/reference/java/util/zip/CheckedInputStream) 100 and 101 [CheckedOutputStream](https://developer.android.com/reference/java/util/zip/CheckedOutputStream), 102 see `BackupRestoreStorage` implementation for more details. 103- Enhanced forward compatibility for file is enabled: If a backup includes 104 data that didn't exist in earlier versions of the app, the data can still be 105 successfully restored in those older versions. This is achieved by extending 106 the `BackupRestoreFileStorage` class, and `BackupRestoreFileArchiver` will 107 treat each file as an entity and do the backup / restore. 108- Manual `BackupManager.dataChanged` call is unnecessary now, the framework 109 will invoke the API automatically. 110 111## Usages 112 113This section provides [examples](example/ExampleStorage.kt) of datastore. 114 115Here is a datastore with a string data: 116 117```kotlin 118class ExampleStorage : ObservableBackupRestoreStorage() { 119 @Volatile // field is manipulated by multiple threads, synchronization might be needed 120 var data: String? = null 121 private set 122 123 @AnyThread 124 fun setData(data: String?) { 125 this.data = data 126 // call notifyChange to trigger backup and metrics logging whenever data is changed 127 if (data != null) { 128 notifyChange(ChangeReason.UPDATE) 129 } else { 130 notifyChange(ChangeReason.DELETE) 131 } 132 } 133 134 override val name: String 135 get() = "ExampleStorage" 136 137 override fun createBackupRestoreEntities(): List<BackupRestoreEntity> = 138 listOf(StringEntity("data")) 139 140 override fun enableRestore(): Boolean { 141 return true // check condition like flag, environment, etc. 142 } 143 144 override fun enableBackup(backupContext: BackupContext): Boolean { 145 return true // check condition like flag, environment, etc. 146 } 147 148 @BinderThread 149 private inner class StringEntity(override val key: String) : BackupRestoreEntity { 150 override fun backup(backupContext: BackupContext, outputStream: OutputStream) = 151 if (data != null) { 152 outputStream.write(data!!.toByteArray(UTF_8)) 153 EntityBackupResult.UPDATE 154 } else { 155 EntityBackupResult.DELETE // delete existing backup data 156 } 157 158 override fun restore(restoreContext: RestoreContext, inputStream: InputStream) { 159 // DO NOT call setData API here, which will trigger notifyChange unexpectedly. 160 // Under the hood, the datastore library will call notifyChange(ChangeReason.RESTORE) 161 // later to notify observers. 162 data = String(inputStream.readBytes(), UTF_8) 163 // Handle restored data in onRestoreFinished() callback 164 } 165 } 166 167 override fun onRestoreFinished() { 168 // TODO: Update state with the restored data. Use this callback instead of "restore()" in 169 // case the restore action involves several entities. 170 // NOTE: The library will call notifyChange(ChangeReason.RESTORE) for you 171 } 172} 173``` 174 175And this is a datastore with key value data: 176 177```kotlin 178class ExampleKeyValueStorage : 179 BackupRestoreStorage(), KeyedObservable<String> by KeyedDataObservable() { 180 // thread safe data structure 181 private val map = ConcurrentHashMap<String, String>() 182 183 override val name: String 184 get() = "ExampleKeyValueStorage" 185 186 fun updateData(key: String, value: String?) { 187 if (value != null) { 188 map[key] = value 189 notifyChange(ChangeReason.UPDATE) 190 } else { 191 map.remove(key) 192 notifyChange(ChangeReason.DELETE) 193 } 194 } 195 196 override fun createBackupRestoreEntities(): List<BackupRestoreEntity> = 197 listOf(createMapBackupRestoreEntity()) 198 199 private fun createMapBackupRestoreEntity() = 200 object : BackupRestoreEntity { 201 override val key: String 202 get() = "map" 203 204 override fun backup( 205 backupContext: BackupContext, 206 outputStream: OutputStream, 207 ): EntityBackupResult { 208 // Use TreeMap to achieve predictable and stable order, so that data will not be 209 // updated to Android backup backend if there is only order change. 210 val copy = TreeMap(map) 211 if (copy.isEmpty()) return EntityBackupResult.DELETE 212 val dataOutputStream = DataOutputStream(outputStream) 213 dataOutputStream.writeInt(copy.size) 214 for ((key, value) in copy) { 215 dataOutputStream.writeUTF(key) 216 dataOutputStream.writeUTF(value) 217 } 218 return EntityBackupResult.UPDATE 219 } 220 221 override fun restore(restoreContext: RestoreContext, inputStream: InputStream) { 222 val dataInputString = DataInputStream(inputStream) 223 repeat(dataInputString.readInt()) { 224 val key = dataInputString.readUTF() 225 val value = dataInputString.readUTF() 226 map[key] = value 227 } 228 } 229 } 230} 231``` 232 233All the datastore should be added in the application class: 234 235```kotlin 236class ExampleApplication : Application() { 237 override fun onCreate() { 238 super.onCreate() 239 BackupRestoreStorageManager.getInstance(this) 240 .add(ExampleStorage(), ExampleKeyValueStorage()) 241 } 242} 243``` 244 245Additionally, inject datastore to the custom `BackupAgentHelper` class: 246 247```kotlin 248class ExampleBackupAgent : BackupAgentHelper() { 249 override fun onCreate() { 250 super.onCreate() 251 BackupRestoreStorageManager.getInstance(this).addBackupAgentHelpers(this) 252 } 253 254 override fun onRestoreFinished() { 255 BackupRestoreStorageManager.getInstance(this).onRestoreFinished() 256 } 257} 258``` 259 260## Development 261 262Please preserve the code coverage ratio during development. The current line 263coverage is **100% (444/444)** and branch coverage is **93.6% (176/188)**. 264