1 /* 2 * Copyright 2021 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.glance.state 18 19 import android.content.Context 20 import androidx.annotation.RestrictTo 21 import androidx.datastore.core.DataStore 22 import androidx.datastore.preferences.core.PreferenceDataStoreFactory 23 import androidx.datastore.preferences.core.Preferences 24 import androidx.datastore.preferences.preferencesDataStoreFile 25 import java.io.File 26 import kotlinx.coroutines.CoroutineScope 27 import kotlinx.coroutines.flow.first 28 import kotlinx.coroutines.sync.Mutex 29 import kotlinx.coroutines.sync.withLock 30 31 /** 32 * Configuration definition for [GlanceState]. This defines where the data is stored and how the 33 * underlying data store is created. Use a unique [GlanceStateDefinition] to get a [GlanceState], 34 * once defined, the data should be updated using the state directly, this definition should not 35 * change. 36 */ 37 interface GlanceStateDefinition<T> { 38 39 /** 40 * This file indicates the location of the persisted data. 41 * 42 * @param context The context used to create the file directory 43 * @param fileKey The unique string key used to name and identify the data file corresponding to 44 * a remote UI. Each remote UI has a unique UI key, used to key the data for that UI. 45 */ getLocationnull46 fun getLocation(context: Context, fileKey: String): File 47 48 /** 49 * Creates the underlying data store. 50 * 51 * @param context The context used to create locate the file directory 52 * @param fileKey The unique string key used to name and identify the data file corresponding to 53 * a remote UI. Each remote UI has a unique UI key, used to key the data for that UI. 54 */ 55 suspend fun getDataStore(context: Context, fileKey: String): DataStore<T> 56 } 57 58 /** 59 * Interface for an object that manages configuration for glanceables using the given 60 * GlanceStateDefinition. 61 */ 62 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 63 interface ConfigManager { 64 /** 65 * Returns the stored data associated with the given UI key string. 66 * 67 * @param definition the configuration that defines this state. 68 * @param fileKey identifies the data file associated with the store, must be unique for any 69 * remote UI in the app. 70 */ 71 suspend fun <T> getValue( 72 context: Context, 73 definition: GlanceStateDefinition<T>, 74 fileKey: String 75 ): T 76 77 /** 78 * Updates the underlying data by applying the provided update block. 79 * 80 * @param definition the configuration that defines this state. 81 * @param fileKey identifies the date file associated with the store, must be unique for any 82 * remote UI in the app. 83 */ 84 suspend fun <T> updateValue( 85 context: Context, 86 definition: GlanceStateDefinition<T>, 87 fileKey: String, 88 updateBlock: suspend (T) -> T 89 ): T 90 91 /** 92 * Delete the file underlying the [DataStore] and remove local references to the [DataStore]. 93 */ 94 suspend fun deleteStore(context: Context, definition: GlanceStateDefinition<*>, fileKey: String) 95 } 96 97 /** 98 * Data store for data specific to the glanceable view. Stored data should include information 99 * relevant to the representation of views, but not surface specific view data. For example, the 100 * month displayed on a calendar rather than actual calendar entries. 101 */ 102 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 103 object GlanceState : ConfigManager { getValuenull104 override suspend fun <T> getValue( 105 context: Context, 106 definition: GlanceStateDefinition<T>, 107 fileKey: String 108 ): T = getDataStore(context, definition, fileKey).data.first() 109 110 override suspend fun <T> updateValue( 111 context: Context, 112 definition: GlanceStateDefinition<T>, 113 fileKey: String, 114 updateBlock: suspend (T) -> T 115 ): T = getDataStore(context, definition, fileKey).updateData(updateBlock) 116 117 override suspend fun deleteStore( 118 context: Context, 119 definition: GlanceStateDefinition<*>, 120 fileKey: String 121 ) { 122 mutex.withLock { 123 dataStores.remove(fileKey) 124 val location = definition.getLocation(context, fileKey) 125 location.delete() 126 } 127 } 128 129 @Suppress("UNCHECKED_CAST") getDataStorenull130 private suspend fun <T> getDataStore( 131 context: Context, 132 definition: GlanceStateDefinition<T>, 133 fileKey: String 134 ): DataStore<T> = 135 mutex.withLock { 136 dataStores.getOrPut(fileKey) { definition.getDataStore(context, fileKey) } 137 as DataStore<T> 138 } 139 140 private val mutex = Mutex() 141 142 // TODO: Move to a single, global source to manage the data lifecycle 143 private val dataStores: MutableMap<String, DataStore<*>> = mutableMapOf() 144 } 145 146 /** Base class helping the creation of a state using DataStore's [Preferences]. */ 147 object PreferencesGlanceStateDefinition : GlanceStateDefinition<Preferences> { 148 private var coroutineScope: CoroutineScope? = null 149 getLocationnull150 override fun getLocation(context: Context, fileKey: String): File = 151 context.preferencesDataStoreFile(fileKey) 152 153 override suspend fun getDataStore(context: Context, fileKey: String): DataStore<Preferences> { 154 return coroutineScope?.let { 155 PreferenceDataStoreFactory.create(scope = it) { 156 context.preferencesDataStoreFile(fileKey) 157 } 158 } ?: PreferenceDataStoreFactory.create { context.preferencesDataStoreFile(fileKey) } 159 } 160 161 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) setCoroutineScopenull162 fun setCoroutineScope(scope: CoroutineScope) { 163 coroutineScope = scope 164 } 165 } 166