1 /*
<lambda>null2  * Copyright 2021 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.appwidget.AppWidgetManager
20 import android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN
21 import android.content.ComponentName
22 import android.content.Context
23 import android.os.Build
24 import android.os.Bundle
25 import android.util.Log
26 import androidx.activity.ComponentActivity
27 import androidx.activity.compose.setContent
28 import androidx.annotation.RequiresApi
29 import androidx.compose.foundation.background
30 import androidx.compose.foundation.clickable
31 import androidx.compose.foundation.layout.Arrangement
32 import androidx.compose.foundation.layout.Column
33 import androidx.compose.foundation.layout.Row
34 import androidx.compose.foundation.layout.Spacer
35 import androidx.compose.foundation.layout.fillMaxSize
36 import androidx.compose.foundation.layout.fillMaxWidth
37 import androidx.compose.foundation.layout.height
38 import androidx.compose.foundation.layout.padding
39 import androidx.compose.foundation.lazy.LazyColumn
40 import androidx.compose.foundation.lazy.items
41 import androidx.compose.material.Divider
42 import androidx.compose.material.Text
43 import androidx.compose.runtime.Composable
44 import androidx.compose.runtime.rememberCoroutineScope
45 import androidx.compose.ui.Alignment
46 import androidx.compose.ui.Modifier
47 import androidx.compose.ui.graphics.Color
48 import androidx.compose.ui.text.font.FontWeight
49 import androidx.compose.ui.text.style.TextAlign
50 import androidx.compose.ui.unit.DpSize
51 import androidx.compose.ui.unit.dp
52 import androidx.compose.ui.unit.sp
53 import androidx.datastore.preferences.core.emptyPreferences
54 import androidx.glance.GlanceId
55 import androidx.glance.appwidget.GlanceAppWidget
56 import androidx.glance.appwidget.GlanceAppWidgetManager
57 import androidx.glance.appwidget.GlanceAppWidgetReceiver
58 import androidx.lifecycle.lifecycleScope
59 import kotlin.reflect.KClass
60 import kotlinx.coroutines.launch
61 
62 private const val TAG = "GlanceAppWidgetDemo"
63 
64 class GlanceAppWidgetDemoActivity : ComponentActivity() {
65 
66     lateinit var manager: GlanceAppWidgetManager
67 
68     override fun onCreate(savedInstanceState: Bundle?) {
69         super.onCreate(savedInstanceState)
70 
71         manager = GlanceAppWidgetManager(this)
72         updateView()
73     }
74 
75     override fun onResume() {
76         super.onResume()
77         updateWidgetPreviews()
78         updateView()
79     }
80 
81     private fun updateWidgetPreviews() =
82         lifecycleScope.launch {
83             if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
84                 return@launch
85             }
86             val previewClasses =
87                 listOf(
88                     ActionAppWidgetReceiver::class,
89                     BackgroundTintWidgetBroadcastReceiver::class,
90                     ButtonsWidgetBroadcastReceiver::class,
91                     CompoundButtonAppWidgetReceiver::class,
92                     ContentDescriptionAppWidgetReceiver::class,
93                     DefaultColorsAppWidgetReceiver::class,
94                     DefaultStateAppWidgetReceiver::class,
95                     ErrorUiAppWidgetReceiver::class,
96                     FontDemoAppWidgetReceiver::class,
97                     ImageAppWidgetReceiver::class,
98                     ProgressIndicatorAppWidgetReceiver::class,
99                     RemoteViewsWidgetReceiver::class,
100                     ResizingAppWidgetReceiver::class,
101                     ResponsiveAppWidgetReceiver::class,
102                     RippleAppWidgetReceiver::class,
103                     ScrollableAppWidgetReceiver::class,
104                     TitleBarWidgetBroadcastReceiver::class,
105                     TypographyDemoAppWidgetReceiver::class,
106                     VerticalGridAppWidgetReceiver::class,
107                 )
108             try {
109                 for (receiver in previewClasses) {
110                     if (
111                         receiver.hasPreviewForCategory(
112                             this@GlanceAppWidgetDemoActivity,
113                             WIDGET_CATEGORY_HOME_SCREEN
114                         )
115                     ) {
116                         Log.i(TAG, "Skipped updating previews for $receiver")
117                         continue
118                     }
119                     if (!manager.setWidgetPreviews(receiver)) {
120                         Log.e(TAG, "Failed to set previews for $receiver, probably rate limited")
121                     }
122                 }
123             } catch (e: Exception) {
124                 Log.e(TAG, "error thrown when calling setWidgetPreview", e)
125             }
126         }
127 
128     private fun updateView() {
129         lifecycleScope.launch {
130             // Discover the GlanceAppWidget
131             val appWidgetManager = AppWidgetManager.getInstance(this@GlanceAppWidgetDemoActivity)
132             val receivers =
133                 appWidgetManager.installedProviders
134                     .filter { it.provider.packageName == packageName }
135                     .map { it.provider.className }
136             val data =
137                 receivers.mapNotNull { receiverName ->
138                     val receiverClass = Class.forName(receiverName)
139                     if (!GlanceAppWidgetReceiver::class.java.isAssignableFrom(receiverClass)) {
140                         return@mapNotNull null
141                     }
142                     val receiver =
143                         receiverClass.getDeclaredConstructor().newInstance()
144                             as GlanceAppWidgetReceiver
145                     val provider = receiver.glanceAppWidget.javaClass
146                     ProviderData(
147                         provider = provider,
148                         receiver = receiver.javaClass,
149                         appWidgets =
150                             manager.getGlanceIds(provider).map { id ->
151                                 AppWidgetDesc(
152                                     appWidgetId = id,
153                                     sizes = manager.getAppWidgetSizes(id)
154                                 )
155                             }
156                     )
157                 }
158 
159             setContent {
160                 val scope = rememberCoroutineScope()
161                 Column(modifier = Modifier.fillMaxSize()) {
162                     Text(
163                         "Installed App Widgets",
164                         modifier = Modifier.fillMaxWidth(),
165                         fontSize = 18.sp,
166                         fontWeight = FontWeight.Bold,
167                         textAlign = TextAlign.Center,
168                     )
169                     Spacer(modifier = Modifier.height(5.dp))
170                     LazyColumn(
171                         modifier = Modifier.fillMaxWidth().weight(1f),
172                         verticalArrangement = Arrangement.spacedBy(8.dp),
173                     ) {
174                         items(data) {
175                             ShowProvider(it) {
176                                 scope.launch {
177                                     manager.requestPinGlanceAppWidget(
178                                         receiver = it.receiver,
179                                         preview =
180                                             it.provider.getDeclaredConstructor().newInstance(),
181                                         previewState = emptyPreferences()
182                                     )
183                                 }
184                             }
185                             Divider(color = Color.Black)
186                         }
187                     }
188                 }
189             }
190         }
191     }
192 }
193 
194 @Composable
ShowProvidernull195 fun ShowProvider(providerData: ProviderData, onProviderClicked: (ProviderData) -> Unit) {
196     Column(modifier = Modifier.fillMaxWidth().clickable { onProviderClicked(providerData) }) {
197         Text(providerData.provider.simpleName, fontWeight = FontWeight.Medium)
198         Column(
199             modifier = Modifier.fillMaxWidth().padding(start = 16.dp).background(Color.LightGray)
200         ) {
201             providerData.appWidgets.forEachIndexed { index, widget ->
202                 ShowAppWidget(index, widget)
203                 Divider(color = Color.DarkGray)
204             }
205         }
206     }
207 }
208 
209 @Composable
ShowAppWidgetnull210 fun ShowAppWidget(index: Int, widgetDesc: AppWidgetDesc) {
211     Row(
212         modifier = Modifier.fillMaxWidth(),
213         verticalAlignment = Alignment.CenterVertically,
214         horizontalArrangement = Arrangement.spacedBy(8.dp)
215     ) {
216         Text("Instance ${index + 1}")
217         Column(
218             modifier = Modifier.fillMaxWidth(),
219             verticalArrangement = Arrangement.spacedBy(4.dp)
220         ) {
221             widgetDesc.sizes
222                 .sortedBy { it.width.value * it.height.value }
223                 .forEachIndexed { index, size ->
224                     Text(
225                         String.format(
226                             "Size ${index + 1}: %.0f dp x %.0f dp",
227                             size.width.value,
228                             size.height.value
229                         )
230                     )
231                 }
232         }
233     }
234 }
235 
236 data class ProviderData(
237     val provider: Class<out GlanceAppWidget>,
238     val receiver: Class<out GlanceAppWidgetReceiver>,
239     val appWidgets: List<AppWidgetDesc>,
240 )
241 
242 data class AppWidgetDesc(
243     val appWidgetId: GlanceId,
244     val sizes: List<DpSize>,
245 )
246 
247 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
KClassnull248 private fun KClass<out GlanceAppWidgetReceiver>.hasPreviewForCategory(
249     context: Context,
250     widgetCategory: Int,
251 ): Boolean {
252     val manager = context.getSystemService(Context.APPWIDGET_SERVICE) as AppWidgetManager
253     val component = ComponentName(context, java)
254     val providerInfo =
255         manager.installedProviders.first { providerInfo -> providerInfo.provider == component }
256     return providerInfo.generatedPreviewCategories.and(widgetCategory) != 0
257 }
258