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