1 /* <lambda>null2 * 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 com.example.datastoresampleapp 18 19 import android.content.Context 20 import android.os.Bundle 21 import android.util.Log 22 import android.view.View 23 import androidx.appcompat.app.AppCompatActivity 24 import androidx.datastore.core.CorruptionException 25 import androidx.datastore.core.DataStore 26 import androidx.datastore.core.DataStoreFactory 27 import androidx.datastore.core.Serializer 28 import androidx.lifecycle.Lifecycle 29 import androidx.lifecycle.lifecycleScope 30 import androidx.lifecycle.repeatOnLifecycle 31 import androidx.preference.Preference 32 import androidx.preference.PreferenceFragmentCompat 33 import androidx.preference.SwitchPreference 34 import androidx.preference.TwoStatePreference 35 import com.google.protobuf.InvalidProtocolBufferException 36 import java.io.File 37 import java.io.IOException 38 import java.io.InputStream 39 import java.io.OutputStream 40 import kotlinx.coroutines.ExperimentalCoroutinesApi 41 import kotlinx.coroutines.channels.awaitClose 42 import kotlinx.coroutines.flow.Flow 43 import kotlinx.coroutines.flow.callbackFlow 44 import kotlinx.coroutines.flow.collect 45 import kotlinx.coroutines.flow.first 46 import kotlinx.coroutines.flow.flatMapLatest 47 import kotlinx.coroutines.launch 48 49 private val TAG = "SettingsActivity" 50 51 class SettingsFragmentActivity() : AppCompatActivity() { 52 override fun onCreate(savedInstanceState: Bundle?) { 53 super.onCreate(savedInstanceState) 54 supportFragmentManager 55 .beginTransaction() 56 .replace(android.R.id.content, SettingsFragment()) 57 .commit() 58 } 59 } 60 61 /** 62 * Toggle States: 63 * 1) Value not read from disk. Toggle is disabled in default position. 64 * 2) Value read from disk and no pending updates. Toggle is enabled in latest persisted position. 65 * 3) Value read from disk but with pending updates. Toggle is disabled in pending position. 66 */ 67 class SettingsFragment() : PreferenceFragmentCompat() { <lambda>null68 private val fooToggle: TwoStatePreference by lazy { 69 createFooPreference(preferenceManager.context) 70 } 71 72 private val PROTO_STORE_FILE_NAME = "datastore_test_app.pb" 73 <lambda>null74 private val settingsStore: DataStore<Settings> by lazy { 75 DataStoreFactory.create(serializer = SettingsSerializer) { 76 File(requireActivity().applicationContext.filesDir, PROTO_STORE_FILE_NAME) 77 } 78 } 79 onCreatePreferencesnull80 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 81 val preferences = preferenceManager.createPreferenceScreen(preferenceManager.context) 82 preferences.addPreference(fooToggle) 83 preferenceScreen = preferences 84 } 85 86 @Suppress("OPT_IN_MARKER_ON_OVERRIDE_WARNING") 87 @ExperimentalCoroutinesApi onViewCreatednull88 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 89 super.onViewCreated(view, savedInstanceState) 90 91 viewLifecycleOwner.lifecycleScope.launch { 92 viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 93 // Read the initial value from disk 94 val settings: Settings = 95 try { 96 settingsStore.data.first() 97 } catch (ex: IOException) { 98 Log.e(TAG, "Could not read settings.", ex) 99 // Show error to user here, or try re-reading. 100 return@repeatOnLifecycle 101 } 102 103 // Set the toggle to the value read from disk and enable the toggle. 104 fooToggle.isChecked = settings.foo 105 fooToggle.isEnabled = true 106 107 fooToggle.changeFlow 108 .flatMapLatest { (_: Preference?, newValue: Any?) -> 109 val isChecked = newValue as Boolean 110 111 fooToggle.isEnabled = 112 false // Disable the toggle until the write is completed 113 fooToggle.isChecked = 114 isChecked // Set the disabled toggle to the pending value 115 116 try { 117 settingsStore.setFoo(isChecked) 118 } catch (ex: IOException) { // setFoo can only throw IOExceptions 119 Log.e(TAG, "Could not write settings", ex) 120 // Show error to user here 121 } 122 settingsStore.data // Switch to data flow since it is the source of truth. 123 } 124 .collect { 125 // We update the toggle to the latest persisted value - whether or not the 126 // update succeeded. If the write failed, this will reset to original state. 127 fooToggle.isChecked = it.foo 128 fooToggle.isEnabled = true 129 } 130 } 131 } 132 } 133 <lambda>null134 private suspend fun DataStore<Settings>.setFoo(foo: Boolean) = updateData { 135 it.toBuilder().setFoo(foo).build() 136 } 137 createFooPreferencenull138 private fun createFooPreference(context: Context) = 139 SwitchPreference(context).apply { 140 isEnabled = false // Start out disabled 141 isPersistent = false // Disable SharedPreferences 142 title = "Foo title" 143 summary = "Summary of Foo toggle" 144 } 145 } 146 147 @ExperimentalCoroutinesApi 148 private val Preference.changeFlow: Flow<Pair<Preference?, Any?>> <lambda>null149 get() = callbackFlow { 150 this@changeFlow.setOnPreferenceChangeListener { preference: Preference?, newValue: Any? -> 151 this@callbackFlow.launch { send(Pair(preference, newValue)) } 152 false // Do not update the state of the toggle. 153 } 154 155 awaitClose { this@changeFlow.onPreferenceChangeListener = null } 156 } 157 158 private object SettingsSerializer : Serializer<Settings> { 159 override val defaultValue: Settings = Settings.getDefaultInstance() 160 readFromnull161 override suspend fun readFrom(input: InputStream): Settings { 162 try { 163 return Settings.parseFrom(input) 164 } catch (ipbe: InvalidProtocolBufferException) { 165 throw CorruptionException("Cannot read proto.", ipbe) 166 } 167 } 168 writeTonull169 override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output) 170 } 171