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 @file:OptIn(ExperimentalGlanceApi::class)
18 
19 package androidx.glance.appwidget.samples
20 
21 import android.appwidget.AppWidgetManager
22 import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD
23 import android.content.Context
24 import androidx.annotation.Sampled
25 import androidx.compose.runtime.Composable
26 import androidx.compose.runtime.collectAsState
27 import androidx.compose.runtime.getValue
28 import androidx.compose.runtime.rememberCoroutineScope
29 import androidx.datastore.core.DataStore
30 import androidx.datastore.preferences.core.Preferences
31 import androidx.datastore.preferences.core.intPreferencesKey
32 import androidx.datastore.preferences.core.stringPreferencesKey
33 import androidx.datastore.preferences.preferencesDataStore
34 import androidx.glance.ExperimentalGlanceApi
35 import androidx.glance.GlanceId
36 import androidx.glance.GlanceModifier
37 import androidx.glance.action.clickable
38 import androidx.glance.appwidget.GlanceAppWidget
39 import androidx.glance.appwidget.LocalAppWidgetOptions
40 import androidx.glance.appwidget.provideContent
41 import androidx.glance.appwidget.updateAll
42 import androidx.glance.text.Text
43 import androidx.work.CoroutineWorker
44 import androidx.work.ExistingPeriodicWorkPolicy
45 import androidx.work.PeriodicWorkRequest
46 import androidx.work.WorkManager
47 import androidx.work.WorkerParameters
48 import kotlin.random.Random
49 import kotlin.time.Duration.Companion.minutes
50 import kotlin.time.toJavaDuration
51 import kotlinx.coroutines.coroutineScope
52 import kotlinx.coroutines.flow.first
53 import kotlinx.coroutines.flow.map
54 import kotlinx.coroutines.flow.stateIn
55 import kotlinx.coroutines.launch
56 
57 /** This sample demonstrates how to do create a simple [GlanceAppWidget] and update the widget. */
58 @Sampled
59 fun provideGlanceSample() {
60     class MyWidget : GlanceAppWidget() {
61 
62         val Context.myWidgetStore by preferencesDataStore("MyWidget")
63         val Name = stringPreferencesKey("name")
64 
65         override suspend fun provideGlance(context: Context, id: GlanceId) {
66             // Load initial data needed to render the AppWidget here. Prefer doing heavy work before
67             // provideContent, as the provideGlance function will timeout shortly after
68             // provideContent is called.
69             val store = context.myWidgetStore
70             val initial = store.data.first()
71 
72             provideContent {
73                 // Observe your sources of data, and declare your @Composable layout.
74                 val data by store.data.collectAsState(initial)
75                 val scope = rememberCoroutineScope()
76                 Text(
77                     text = "Hello ${data[Name]}",
78                     modifier =
79                         GlanceModifier.clickable("changeName") {
80                             scope.launch {
81                                 store.updateData {
82                                     it.toMutablePreferences().apply { set(Name, "Changed") }
83                                 }
84                             }
85                         }
86                 )
87             }
88         }
89 
90         // Updating the widget from elsewhere in the app:
91         suspend fun changeWidgetName(context: Context, newName: String) {
92             context.myWidgetStore.updateData {
93                 it.toMutablePreferences().apply { set(Name, newName) }
94             }
95             // Call update/updateAll in case a Worker for the widget is not currently running. This
96             // is not necessary when updating data from inside of the composition using lambdas,
97             // since a Worker will be started to run lambda actions.
98             MyWidget().updateAll(context)
99         }
100     }
101 }
102 
103 // Without this declaration, the class reference to WeatherWidgetWorker::class.java below does not
104 // work because it is defined in that function after it is referenced. This will not show up in the
105 // sample.
106 class WeatherWidgetWorker(appContext: Context, params: WorkerParameters) :
107     CoroutineWorker(appContext, params) {
doWorknull108     override suspend fun doWork() = Result.success()
109 }
110 
111 /**
112  * This sample demonstrates how to do periodic updates using a unique periodic [CoroutineWorker].
113  */
114 @Sampled
115 fun provideGlancePeriodicWorkSample() {
116     class WeatherWidget : GlanceAppWidget() {
117 
118         val Context.weatherWidgetStore by preferencesDataStore("WeatherWidget")
119         val CurrentDegrees = intPreferencesKey("currentDegrees")
120 
121         suspend fun DataStore<Preferences>.loadWeather() {
122             updateData { prefs ->
123                 prefs.toMutablePreferences().apply {
124                     this[CurrentDegrees] = Random.Default.nextInt()
125                 }
126             }
127         }
128 
129         override suspend fun provideGlance(context: Context, id: GlanceId) {
130             coroutineScope {
131                 val store = context.weatherWidgetStore
132                 val currentDegrees =
133                     store.data.map { prefs -> prefs[CurrentDegrees] }.stateIn(this@coroutineScope)
134 
135                 // Load the current weather if there is not a current value present.
136                 if (currentDegrees.value == null) store.loadWeather()
137 
138                 // Create unique periodic work to keep this widget updated at a regular interval.
139                 WorkManager.getInstance(context)
140                     .enqueueUniquePeriodicWork(
141                         "weatherWidgetWorker",
142                         ExistingPeriodicWorkPolicy.KEEP,
143                         PeriodicWorkRequest.Builder(
144                                 WeatherWidgetWorker::class.java,
145                                 15.minutes.toJavaDuration()
146                             )
147                             .setInitialDelay(15.minutes.toJavaDuration())
148                             .build()
149                     )
150 
151                 // Note: you can also set `android:updatePeriodMillis` to control how often the
152                 // launcher requests an update, but this does not support periods less than
153                 // 30 minutes.
154 
155                 provideContent {
156                     val degrees by currentDegrees.collectAsState()
157                     Text("Current weather: $degrees °F")
158                 }
159             }
160         }
161     }
162 
163     class WeatherWidgetWorker(appContext: Context, params: WorkerParameters) :
164         CoroutineWorker(appContext, params) {
165         override suspend fun doWork(): Result {
166             WeatherWidget().apply {
167                 applicationContext.weatherWidgetStore.loadWeather()
168                 // Call update/updateAll in case a Worker for the widget is not currently running.
169                 updateAll(applicationContext)
170             }
171             return Result.success()
172         }
173     }
174 }
175 
176 /** This sample demonstrates how to implement [GlanceAppWidget.providePreview], */
177 @Sampled
providePreviewSamplenull178 fun providePreviewSample() {
179     class MyWidgetWithPreview : GlanceAppWidget() {
180         override suspend fun provideGlance(context: Context, id: GlanceId) {
181             provideContent {
182                 val widgetCategory =
183                     LocalAppWidgetOptions.current.getInt(
184                         AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY
185                     )
186                 Content(isPreview = false, widgetCategory)
187             }
188         }
189 
190         override suspend fun providePreview(context: Context, widgetCategory: Int) {
191             provideContent { Content(isPreview = true, widgetCategory) }
192         }
193 
194         @Composable
195         fun Content(
196             isPreview: Boolean,
197             widgetCategory: Int,
198         ) {
199             val text = if (isPreview) "preview" else "bound widget"
200             Text("This is a $text.")
201             // Avoid showing personal information if this preview or widget is showing on the
202             // lockscreen/keyguard.
203             val isKeyguardWidget = widgetCategory.and(WIDGET_CATEGORY_KEYGUARD) != 0
204             if (!isKeyguardWidget) {
205                 Text("Some personal info.")
206             }
207         }
208     }
209 }
210