README.md
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