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