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