1 /*
2  * 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
18 
19 import androidx.compose.ui.unit.DpSize
20 import androidx.glance.LocalSize
21 import androidx.glance.appwidget.SizeMode.Exact
22 import androidx.glance.appwidget.SizeMode.Responsive
23 
24 /**
25  * Modes describing how the [GlanceAppWidget] should handle size specification.
26  *
27  * Note: Size modes that support multiple sizes ([Exact], [Responsive]) run the composable passed to
28  * to [provideContent] concurrently for each size. This has a number of important implications.
29  * Since an instance of the content is running for each size, all of the State objects in the
30  * content will have an instance for each size.
31  *
32  * For example, in Exact mode, let's say the AppWidgetHost asks for 2 sizes, portrait and landscape.
33  * In the code below, there will end up being two instances of the `count` variable, one for each
34  * size:
35  * ```
36  * provideContent {
37  *    var count by remember { mutableStateOf(0) }
38  *    Button(
39  *        text = "Increment count: $count",
40  *        onClick = { count++ }
41  *    )
42  * }
43  * ```
44  *
45  * If the button is clicked while the widget is displayed in portrait size, the `count` variable is
46  * updated for both sizes. This is so that, if the device orientation changes and the host displays
47  * the landscape layout, it will be consistent with the state in portrait. This works because
48  * lambdas that are at the same place in the composition will be mapped to the same default key in
49  * all sizes. So triggering one will trigger the corresponding lambdas in other sizes.
50  *
51  * This means that lambdas will be called multiple times when they are triggered, which can have
52  * unexpected effects if state is not handled correctly. In order to prevent some external action
53  * from being triggered multiple times at once, you should conflate requests so that only one is
54  * active at a time, e.g. using MutableStateFlow.
55  *
56  * To prevent this behavior, you can use the [androidx.compose.runtime.key] composable to set
57  * different default lambda keys for each size, so that they do not trigger each other:
58  * ```
59  * provideContent {
60  *    key(LocalSize.current) {
61  *        var count by remember { mutableStateOf(0) }
62  *        Button(
63  *            text = "Increment count: $count",
64  *            onClick = { count++ }
65  *        )
66  *    }
67  * }
68  * ```
69  *
70  * To disable this behavior on a per-lambda basis, use [androidx.glance.action.action] to set a
71  * custom lambda key based on the current size:
72  * ```
73  * provideContent {
74  *    var count by remember { mutableStateOf(0) }
75  *    Button(
76  *        text = "Increment count: $count",
77  *        onClick = action("incrementCount-${LocalSize.current}") { count++ }
78  *    )
79  * }
80  * ```
81  *
82  * In both of the last two examples, when the button is clicked, only the lambda for the currently
83  * visible size will be triggered.
84  *
85  * Note that the above does not work for effects, which will always be triggered for each size. Use
86  * effects to update state variables in the composition, otherwise be sure to handle any duplicate
87  * triggering that may occur.
88  */
89 sealed interface SizeMode {
90     /**
91      * The [GlanceAppWidget] provides a single UI.
92      *
93      * The [LocalSize] will be the minimum size the App Widget can be, as defined in the App Widget
94      * provider info (see [android.appwidget.AppWidgetManager.getAppWidgetInfo]).
95      */
96     object Single : SizeMode, PreviewSizeMode {
toStringnull97         override fun toString(): String = "SizeMode.Single"
98     }
99 
100     /**
101      * The [GlanceAppWidget] provides a UI for each size the App Widget may be displayed at. The
102      * list of sizes is provided by the options bundle (see
103      * [android.appwidget.AppWidgetManager.getAppWidgetOptions]).
104      *
105      * The composable will be run concurrently for each size. In each sub-composition, the
106      * [LocalSize] will be the one for which the UI is generated. See the note in [SizeMode] for
107      * more info.
108      */
109     object Exact : SizeMode {
110         override fun toString(): String = "SizeMode.Exact"
111     }
112 
113     /**
114      * The [GlanceAppWidget] provides a UI for a fixed set of sizes.
115      *
116      * On Android 12 and later, the composable will be run concurrently for each size provided and
117      * the mapping from size to view will be sent to the system. The framework will then decide
118      * which view to display based on the current size of the App Widget (see
119      * [android.widget.RemoteViews] for details)
120      *
121      * Before Android 12, the composable will be run concurrently for each size at which the app
122      * widget may be displayed (like for [Exact]). For each size, the best view will be chosen,
123      * which is the largest one that fits in the available space, or the smallest one if none fit.
124      *
125      * See the note in [SizeMode] for more info about handling concurrent runs for multiple sizes.
126      *
127      * @param sizes List of sizes to use, must not be empty.
128      */
129     class Responsive(val sizes: Set<DpSize>) : SizeMode, PreviewSizeMode {
130 
131         init {
<lambda>null132             require(sizes.isNotEmpty()) { "The set of sizes cannot be empty" }
133         }
134 
equalsnull135         override fun equals(other: Any?): Boolean {
136             if (this === other) return true
137             if (javaClass != other?.javaClass) return false
138 
139             other as Responsive
140 
141             if (sizes != other.sizes) return false
142 
143             return true
144         }
145 
hashCodenull146         override fun hashCode(): Int = sizes.hashCode()
147 
148         override fun toString(): String = "SizeMode.Responsive(sizes=$sizes)"
149     }
150 }
151 
152 /** This marker interface determines which [SizeMode]s can be used for preview compositions. */
153 sealed interface PreviewSizeMode : SizeMode
154