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