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.appwidget.AppWidgetHostView
20 import android.appwidget.AppWidgetProviderInfo
21 import android.content.ComponentName
22 import android.content.pm.ActivityInfo
23 import android.os.Bundle
24 import androidx.activity.ComponentActivity
25 import androidx.activity.compose.setContent
26 import androidx.compose.foundation.clickable
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.Column
29 import androidx.compose.foundation.layout.fillMaxHeight
30 import androidx.compose.foundation.layout.fillMaxSize
31 import androidx.compose.foundation.layout.fillMaxWidth
32 import androidx.compose.foundation.layout.padding
33 import androidx.compose.foundation.lazy.LazyColumn
34 import androidx.compose.material.Text
35 import androidx.compose.runtime.Composable
36 import androidx.compose.runtime.collectAsState
37 import androidx.compose.runtime.getValue
38 import androidx.compose.runtime.mutableStateOf
39 import androidx.compose.runtime.remember
40 import androidx.compose.runtime.setValue
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.platform.LocalContext
43 import androidx.compose.ui.unit.DpSize
44 import androidx.compose.ui.unit.dp
45 import androidx.compose.ui.viewinterop.AndroidView
46 import androidx.glance.ExperimentalGlanceApi
47 import androidx.glance.appwidget.GlanceAppWidget
48 import androidx.glance.appwidget.UnmanagedSessionReceiver
49 import androidx.glance.appwidget.runComposition
50 import kotlinx.coroutines.Dispatchers
51 
52 /**
53  * Activity that displays Glance widgets using `GlanceAppWidget.runComposition` to output
54  * RemoteViews without having a bound widget.
55  */
56 class SimpleWidgetViewer : ComponentActivity() {
57     override fun onCreate(savedInstanceState: Bundle?) {
58         super.onCreate(savedInstanceState)
59         setContent {
60             val widgets =
61                 listOf(
62                     ActionAppWidget(),
63                     BackgroundTintWidget(),
64                     ButtonsWidget(),
65                     CompoundButtonAppWidget(),
66                     DefaultColorsAppWidget(),
67                     DefaultStateAppWidget(),
68                     ErrorUiAppWidget(),
69                     ExactAppWidget(),
70                     FontDemoWidget(),
71                     ImageAppWidget(),
72                     ProgressIndicatorAppWidget(),
73                     RemoteViewsWidget(),
74                     ResizingAppWidget(),
75                     ResponsiveAppWidget(),
76                     RippleAppWidget(),
77                     ScrollableAppWidget(),
78                     TypographyDemoAppWidget(),
79                     VerticalGridAppWidget(),
80                 )
81             var selectedWidget by remember { mutableStateOf(widgets.random()) }
82             Column(Modifier.fillMaxSize()) {
83                 LazyColumn(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.3F)) {
84                     widgets.forEach { widget ->
85                         item {
86                             Text(
87                                 text = widget::class.simpleName.orEmpty(),
88                                 modifier = Modifier.clickable { selectedWidget = widget },
89                             )
90                         }
91                     }
92                 }
93                 Box(Modifier.fillMaxWidth().fillMaxHeight(0.7F).padding(8.dp)) {
94                     WidgetView(selectedWidget, DpSize(500.dp, 500.dp))
95                 }
96             }
97         }
98     }
99 }
100 
101 @OptIn(ExperimentalGlanceApi::class)
102 @Composable
WidgetViewnull103 fun WidgetView(widget: GlanceAppWidget, size: DpSize = DpSize(200.dp, 200.dp)) {
104     val context = LocalContext.current
105     val remoteViews by
106         remember(widget, size) {
107                 widget.runComposition(
108                     context = context,
109                     sizes = listOf(size),
110                     lambdaReceiver =
111                         ComponentName(context, CustomUnmanagedSessionReceiver::class.java),
112                 )
113             }
114             .collectAsState(null, Dispatchers.Default)
115     AndroidView(
116         factory = {
117             // Using an AWHV is necessary for ListView support, and to properly select a RemoteViews
118             // from a multi-size RemoteViews. If the RemoteViews has only one size and does not
119             // contain lazy list items, a FrameLayout works fine.
120             AppWidgetHostView(context).apply { setFakeAppWidget() }
121         },
122         modifier = Modifier.fillMaxSize(),
123         update = { view -> view.updateAppWidget(remoteViews) },
124     )
125 }
126 
127 /**
128  * The hostView requires an AppWidgetProviderInfo to work in certain OS versions. This method uses
129  * reflection to set a fake provider info.
130  */
AppWidgetHostViewnull131 private fun AppWidgetHostView.setFakeAppWidget() {
132     val context = context
133     val info =
134         AppWidgetProviderInfo().apply {
135             initialLayout = androidx.glance.appwidget.R.layout.glance_default_loading_layout
136         }
137     try {
138         val activityInfo =
139             ActivityInfo().apply {
140                 applicationInfo = context.applicationInfo
141                 packageName = context.packageName
142                 labelRes = applicationInfo.labelRes
143             }
144 
145         info::class.java.getDeclaredField("providerInfo").run {
146             isAccessible = true
147             set(info, activityInfo)
148         }
149 
150         this::class.java.getDeclaredField("mInfo").apply {
151             isAccessible = true
152             set(this@setFakeAppWidget, info)
153         }
154     } catch (e: Exception) {
155         throw IllegalStateException("Couldn't set fake provider", e)
156     }
157 }
158 
159 class CustomUnmanagedSessionReceiver : UnmanagedSessionReceiver()
160