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