1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.datastore.rxjava3
18 
19 import android.annotation.SuppressLint
20 import android.content.Context
21 import androidx.datastore.core.DataMigration
22 import androidx.datastore.core.DataStoreFactory
23 import androidx.datastore.core.Serializer
24 import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
25 import androidx.datastore.dataStoreFile
26 import io.reactivex.rxjava3.core.Scheduler
27 import io.reactivex.rxjava3.core.Single
28 import io.reactivex.rxjava3.schedulers.Schedulers
29 import java.io.File
30 import java.util.concurrent.Callable
31 import kotlinx.coroutines.CoroutineScope
32 import kotlinx.coroutines.Job
33 import kotlinx.coroutines.rx3.asCoroutineDispatcher
34 import kotlinx.coroutines.rx3.await
35 
36 /** Builder class for an RxDataStore that works on a single process. */
37 @SuppressLint("TopLevelBuilder")
38 public class RxDataStoreBuilder<T : Any> {
39 
40     /**
41      * Create a RxDataStoreBuilder with the callable which returns the File that DataStore acts on.
42      * The user is responsible for ensuring that there is never more than one DataStore acting on a
43      * file at a time.
44      *
45      * @param produceFile Function which returns the file that the new DataStore will act on. The
46      *   function must return the same path every time. No two instances of DataStore should act on
47      *   the same file at the same time.
48      * @param serializer the serializer for the type that this DataStore acts on.
49      */
50     public constructor(produceFile: Callable<File>, serializer: Serializer<T>) {
51         this.produceFile = produceFile
52         this.serializer = serializer
53     }
54 
55     /**
56      * Create a RxDataStoreBuilder with the Context and name from which to derive the DataStore
57      * file. The file is generated by File(this.filesDir, "datastore/$fileName"). The user is
58      * responsible for ensuring that there is never more than one DataStore acting on a file at a
59      * time.
60      *
61      * @param context the context from which we retrieve files directory.
62      * @param fileName the filename relative to Context.applicationContext.filesDir that DataStore
63      *   acts on. The File is obtained from [dataStoreFile]. It is created in the "/datastore"
64      *   subdirectory.
65      * @param serializer the serializer for the type that this DataStore acts on.
66      */
67     public constructor(context: Context, fileName: String, serializer: Serializer<T>) {
68         this.context = context
69         this.name = fileName
70         this.serializer = serializer
71     }
72 
73     // Either produceFile or context & name must be set, but not both. This is enforced by the
74     // two constructors.
75     private var produceFile: Callable<File>? = null
76 
77     private var context: Context? = null
78     private var name: String? = null
79 
80     // Required. This is enforced by the constructors.
81     private var serializer: Serializer<T>? = null
82 
83     // Optional
84     private var ioScheduler: Scheduler = Schedulers.io()
85     private var corruptionHandler: ReplaceFileCorruptionHandler<T>? = null
86     private val dataMigrations: MutableList<DataMigration<T>> = mutableListOf()
87 
88     /**
89      * Set the Scheduler on which to perform IO and transform operations. This is converted into a
90      * CoroutineDispatcher before being added to DataStore.
91      *
92      * This parameter is optional and defaults to Schedulers.io().
93      *
94      * @param ioScheduler the scheduler on which IO and transform operations are run
95      * @return this
96      */
97     @Suppress("MissingGetterMatchingBuilder")
<lambda>null98     public fun setIoScheduler(ioScheduler: Scheduler): RxDataStoreBuilder<T> = apply {
99         this.ioScheduler = ioScheduler
100     }
101 
102     /**
103      * Sets the corruption handler to install into the DataStore.
104      *
105      * This parameter is optional and defaults to no corruption handler.
106      *
107      * @param corruptionHandler
108      * @return this
109      */
110     @Suppress("MissingGetterMatchingBuilder")
setCorruptionHandlernull111     public fun setCorruptionHandler(
112         corruptionHandler: ReplaceFileCorruptionHandler<T>
113     ): RxDataStoreBuilder<T> = apply { this.corruptionHandler = corruptionHandler }
114 
115     /**
116      * Add an RxDataMigration to the DataStore. Migrations are run in the order they are added.
117      *
118      * @param rxDataMigration the migration to add.
119      * @return this
120      */
121     @Suppress("MissingGetterMatchingBuilder")
addRxDataMigrationnull122     public fun addRxDataMigration(rxDataMigration: RxDataMigration<T>): RxDataStoreBuilder<T> =
123         apply {
124             this.dataMigrations.add(DataMigrationFromRxDataMigration(rxDataMigration))
125         }
126 
127     /**
128      * Add a DataMigration to the Datastore. Migrations are run in the order they are added.
129      *
130      * @param dataMigration the migration to add
131      * @return this
132      */
133     @Suppress("MissingGetterMatchingBuilder")
<lambda>null134     public fun addDataMigration(dataMigration: DataMigration<T>): RxDataStoreBuilder<T> = apply {
135         this.dataMigrations.add(dataMigration)
136     }
137 
138     /**
139      * Build the DataStore.
140      *
141      * @return the DataStore with the provided parameters
142      */
buildnull143     public fun build(): RxDataStore<T> {
144         val scope = CoroutineScope(ioScheduler.asCoroutineDispatcher() + Job())
145 
146         val delegateDs =
147             if (produceFile != null) {
148                 DataStoreFactory.create(
149                     produceFile = { produceFile!!.call() },
150                     serializer = serializer!!,
151                     scope = scope,
152                     corruptionHandler = corruptionHandler,
153                     migrations = dataMigrations
154                 )
155             } else if (context != null && name != null) {
156                 DataStoreFactory.create(
157                     produceFile = { context!!.dataStoreFile(name!!) },
158                     serializer = serializer!!,
159                     scope = scope,
160                     corruptionHandler = corruptionHandler,
161                     migrations = dataMigrations
162                 )
163             } else {
164                 error(
165                     "Either produceFile or context and name must be set. This should never happen."
166                 )
167             }
168 
169         return RxDataStore.create(delegateDs, scope)
170     }
171 }
172 
173 internal class DataMigrationFromRxDataMigration<T>(private val migration: RxDataMigration<T>) :
174     DataMigration<T> {
shouldMigratenull175     override suspend fun shouldMigrate(currentData: T): Boolean {
176         return migration.shouldMigrate(currentData).await()
177     }
178 
migratenull179     override suspend fun migrate(currentData: T): T {
180         @Suppress("UNCHECKED_CAST")
181         return (migration.migrate(currentData) as Single<T & Any>).await()
182     }
183 
cleanUpnull184     override suspend fun cleanUp() {
185         migration.cleanUp().await()
186     }
187 }
188