1 /*
<lambda>null2  * Copyright 2023 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.appwidget.demos
18 
19 import android.app.Activity
20 import android.appwidget.AppWidgetManager
21 import android.content.Context
22 import android.content.Intent
23 import android.content.SharedPreferences
24 import android.os.Bundle
25 import androidx.activity.ComponentActivity
26 import androidx.activity.compose.setContent
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.Column
29 import androidx.compose.foundation.layout.Row
30 import androidx.compose.foundation.layout.Spacer
31 import androidx.compose.foundation.layout.padding
32 import androidx.compose.foundation.layout.size
33 import androidx.compose.material3.Button
34 import androidx.compose.material3.RadioButton
35 import androidx.compose.material3.Text
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.LaunchedEffect
38 import androidx.compose.runtime.MutableState
39 import androidx.compose.runtime.mutableStateOf
40 import androidx.compose.runtime.remember
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.unit.dp
43 import androidx.core.content.edit
44 import androidx.glance.GlanceId
45 import androidx.glance.appwidget.GlanceAppWidgetManager
46 import kotlinx.coroutines.DelicateCoroutinesApi
47 import kotlinx.coroutines.GlobalScope
48 import kotlinx.coroutines.launch
49 
50 enum class OnErrorBehavior {
51     Default,
52     Custom,
53     Ignore
54 }
55 
56 class ErrorUiAppWidgetConfigurationActivity : ComponentActivity() {
57 
58     private var repo: ErrorUiAppWidgetConfigurationRepo? = null
59 
onCreatenull60     override fun onCreate(savedInstanceState: Bundle?) {
61         super.onCreate(savedInstanceState)
62 
63         val appWidgetId: Int =
64             intent
65                 ?.extras
66                 ?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
67                 ?: AppWidgetManager.INVALID_APPWIDGET_ID
68 
69         val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
70         setResult(Activity.RESULT_CANCELED, resultValue)
71 
72         if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
73             finish()
74             return
75         }
76 
77         val glanceId: GlanceId = GlanceAppWidgetManager(this).getGlanceIdBy(appWidgetId)
78         val repo = ErrorUiAppWidgetConfigurationRepo(context = this, glanceId = glanceId)
79         this.repo = repo
80 
81         setContent { ConfigurationUi(repo, onSaveAndFinish = { saveAndFinish(appWidgetId) }) }
82     }
83 
84     @OptIn(DelicateCoroutinesApi::class)
saveAndFinishnull85     private fun saveAndFinish(appWidgetId: Int) {
86         val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
87         setResult(Activity.RESULT_OK, resultValue)
88         finish()
89 
90         GlobalScope.launch {
91             val context: Context = this@ErrorUiAppWidgetConfigurationActivity
92             val glanceId: GlanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
93             ErrorUiAppWidget().update(context = context, id = glanceId)
94         }
95     }
96 
onStartnull97     override fun onStart() {
98         super.onStart()
99         this.repo = repo
100     }
101 
onStopnull102     override fun onStop() {
103         super.onStop()
104         repo?.unregisterObserver()
105     }
106 
onDestroynull107     override fun onDestroy() {
108         super.onDestroy()
109         repo = null
110     }
111 }
112 
113 class ErrorUiAppWidgetConfigurationRepo(val context: Context, glanceId: GlanceId) {
114     private val prefsFile = "androidx.glance.appwidget.demos.ErrorUiAppWidgetConfigurationRepo"
115     private val prefsKey = "onErrorBehavior-$glanceId"
116     private val sharedPreferences = context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE)
117 
118     private var listener: SharedPreferences.OnSharedPreferenceChangeListener? = null
119 
updatenull120     fun update(behavior: OnErrorBehavior) {
121         // todo
122         sharedPreferences.edit { putInt(prefsKey, behavior.ordinal) }
123     }
124 
getOnErrorBehaviornull125     fun getOnErrorBehavior(): OnErrorBehavior = getOnErrorBehavior(sharedPreferences)
126 
127     private fun getOnErrorBehavior(prefs: SharedPreferences): OnErrorBehavior {
128         val ordinal = prefs.getInt(prefsKey, OnErrorBehavior.Default.ordinal)
129         return OnErrorBehavior.values()[ordinal]
130     }
131 
observeOnErrorBehaviornull132     fun observeOnErrorBehavior(onChange: (OnErrorBehavior) -> Unit) {
133         val listener =
134             SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
135                 if (key != prefsKey) {
136                     return@OnSharedPreferenceChangeListener
137                 } else {
138                     onChange(getOnErrorBehavior(prefs))
139                 }
140             }
141 
142         this.listener = listener
143         sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
144     }
145 
unregisterObservernull146     fun unregisterObserver() {
147         listener?.let { sharedPreferences.unregisterOnSharedPreferenceChangeListener(it) }
148     }
149 }
150 
151 @Composable
ConfigurationUinull152 private fun ConfigurationUi(repo: ErrorUiAppWidgetConfigurationRepo, onSaveAndFinish: () -> Unit) {
153     val selected: MutableState<OnErrorBehavior> = remember {
154         mutableStateOf(repo.getOnErrorBehavior())
155     }
156     LaunchedEffect(repo) { repo.observeOnErrorBehavior { newState -> selected.value = newState } }
157 
158     Box(Modifier.padding(16.dp)) {
159         Column(Modifier.padding(24.dp)) {
160             Text(text = "OnError behavior")
161             Spacer(Modifier.size(16.dp))
162             LabeledRadioButton(
163                 "Default Behavior",
164                 myBehavior = OnErrorBehavior.Default,
165                 selectedBehavior = selected.value,
166                 onClick = repo::update
167             )
168             LabeledRadioButton(
169                 "Custom Error UI",
170                 myBehavior = OnErrorBehavior.Custom,
171                 selectedBehavior = selected.value,
172                 onClick = repo::update
173             )
174             LabeledRadioButton(
175                 "Ignore Error, No UI Change",
176                 myBehavior = OnErrorBehavior.Ignore,
177                 selectedBehavior = selected.value,
178                 onClick = repo::update
179             )
180 
181             Spacer(Modifier.size(32.dp))
182             Button(onClick = onSaveAndFinish) { Text("Done") }
183         }
184     }
185 }
186 
187 @Composable
LabeledRadioButtonnull188 private fun LabeledRadioButton(
189     text: String,
190     myBehavior: OnErrorBehavior,
191     selectedBehavior: OnErrorBehavior,
192     onClick: (OnErrorBehavior) -> Unit
193 ) {
194     Row {
195         RadioButton(selected = selectedBehavior == myBehavior, onClick = { onClick(myBehavior) })
196         Spacer(Modifier.size(8.dp))
197         Text(text)
198     }
199 }
200