1 /*
<lambda>null2 * Copyright 2022 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 android.annotation.SuppressLint
20 import android.appwidget.AppWidgetManager
21 import android.appwidget.AppWidgetProviderInfo
22 import android.content.Context
23 import android.os.Build
24 import android.os.Bundle
25 import android.os.Trace
26 import android.util.DisplayMetrics
27 import android.util.Log
28 import android.util.SizeF
29 import android.widget.RemoteViews
30 import androidx.annotation.RequiresApi
31 import androidx.annotation.RestrictTo
32 import androidx.compose.runtime.Composable
33 import androidx.compose.ui.unit.DpSize
34 import androidx.compose.ui.unit.dp
35 import androidx.compose.ui.unit.max
36 import androidx.glance.GlanceComposable
37 import androidx.glance.GlanceId
38 import java.util.concurrent.atomic.AtomicBoolean
39 import java.util.concurrent.atomic.AtomicReference
40 import kotlin.coroutines.CoroutineContext
41 import kotlin.math.ceil
42 import kotlin.math.min
43 import kotlin.random.Random
44 import kotlin.random.nextInt
45 import kotlinx.coroutines.CancellableContinuation
46 import kotlinx.coroutines.CancellationException
47 import kotlinx.coroutines.flow.Flow
48 import kotlinx.coroutines.flow.channelFlow
49 import kotlinx.coroutines.suspendCancellableCoroutine
50 import kotlinx.coroutines.withContext
51
52 /**
53 * Maximum depth for a composition. Although there is no hard limit, this should avoid deep
54 * recursions, which would create [RemoteViews] too large to be sent.
55 */
56 internal const val MaxComposeTreeDepth = 50
57
58 // Retrieves the minimum size of an App Widget, as configured by the App Widget provider.
59 internal fun appWidgetMinSize(
60 displayMetrics: DisplayMetrics,
61 appWidgetManager: AppWidgetManager,
62 appWidgetId: Int
63 ): DpSize {
64 val info = appWidgetManager.getAppWidgetInfo(appWidgetId) ?: return DpSize.Zero
65 return info.getMinSize(displayMetrics)
66 }
67
68 // Extract the sizes from the bundle
69 @SuppressLint("PrimitiveInCollection", "ListIterator")
70 @Suppress("DEPRECATION")
extractAllSizesnull71 internal fun Bundle.extractAllSizes(
72 @SuppressLint("PrimitiveInLambda") minSize: () -> DpSize
73 ): List<DpSize> {
74 val sizes = getParcelableArrayList<SizeF>(AppWidgetManager.OPTION_APPWIDGET_SIZES)
75 return if (sizes.isNullOrEmpty()) {
76 estimateSizes(minSize)
77 } else {
78 sizes.map { DpSize(it.width.dp, it.height.dp) }
79 }
80 }
81
82 // If the list of sizes is not available, estimate it from the min/max width and height.
83 // We can assume that the min width and max height correspond to the portrait mode and the max
84 // width / min height to the landscape mode.
85 @SuppressLint("PrimitiveInCollection")
Bundlenull86 private fun Bundle.estimateSizes(
87 @SuppressLint("PrimitiveInLambda") minSize: () -> DpSize
88 ): List<DpSize> {
89 val minHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 0)
90 val maxHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0)
91 val minWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 0)
92 val maxWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 0)
93 // If the min / max widths and heights are not specified, fall back to the unique mode,
94 // giving the minimum size the app widget may have.
95 if (minHeight == 0 || maxHeight == 0 || minWidth == 0 || maxWidth == 0) {
96 return listOf(minSize())
97 }
98 return listOf(DpSize(minWidth.dp, maxHeight.dp), DpSize(maxWidth.dp, minHeight.dp))
99 }
100
101 // Landscape is min height / max width
Bundlenull102 private fun Bundle.extractLandscapeSize(): DpSize? {
103 val minHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 0)
104 val maxWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 0)
105 return if (minHeight == 0 || maxWidth == 0) null else DpSize(maxWidth.dp, minHeight.dp)
106 }
107
108 // Portrait is max height / min width
extractPortraitSizenull109 private fun Bundle.extractPortraitSize(): DpSize? {
110 val maxHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0)
111 val minWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 0)
112 return if (maxHeight == 0 || minWidth == 0) null else DpSize(minWidth.dp, maxHeight.dp)
113 }
114
115 @SuppressLint("PrimitiveInCollection")
extractOrientationSizesnull116 internal fun Bundle.extractOrientationSizes() =
117 listOfNotNull(extractLandscapeSize(), extractPortraitSize())
118
119 // True if the object fits in the given size.
120 private infix fun DpSize.fitsIn(other: DpSize) =
121 (ceil(other.width.value) + 1 > width.value) && (ceil(other.height.value) + 1 > height.value)
122
123 internal fun DpSize.toSizeF(): SizeF = SizeF(width.value, height.value)
124
125 private fun squareDistance(widgetSize: DpSize, layoutSize: DpSize): Float {
126 val dw = widgetSize.width.value - layoutSize.width.value
127 val dh = widgetSize.height.value - layoutSize.height.value
128 return dw * dw + dh * dh
129 }
130
131 // Find the best size that fits in the available [widgetSize] or null if no layout fits.
132 @SuppressLint("ListIterator")
findBestSizenull133 internal fun findBestSize(widgetSize: DpSize, layoutSizes: Collection<DpSize>): DpSize? =
134 layoutSizes
135 .mapNotNull { layoutSize ->
136 if (layoutSize fitsIn widgetSize) {
137 layoutSize to squareDistance(widgetSize, layoutSize)
138 } else {
139 null
140 }
141 }
<lambda>null142 .minByOrNull { it.second }
143 ?.first
144
145 /** @return the minimum size as configured by the App Widget provider. */
getMinSizenull146 internal fun AppWidgetProviderInfo.getMinSize(displayMetrics: DisplayMetrics): DpSize {
147 val minWidth =
148 min(
149 minWidth,
150 if (resizeMode and AppWidgetProviderInfo.RESIZE_HORIZONTAL != 0) {
151 minResizeWidth
152 } else {
153 Int.MAX_VALUE
154 }
155 )
156 val minHeight =
157 min(
158 minHeight,
159 if (resizeMode and AppWidgetProviderInfo.RESIZE_VERTICAL != 0) {
160 minResizeHeight
161 } else {
162 Int.MAX_VALUE
163 }
164 )
165 return DpSize(minWidth.pixelsToDp(displayMetrics), minHeight.pixelsToDp(displayMetrics))
166 }
167
168 @SuppressLint("PrimitiveInCollection")
sortedBySizenull169 internal fun Collection<DpSize>.sortedBySize() =
170 sortedWith(compareBy({ it.width.value * it.height.value }, { it.width.value }))
171
logExceptionnull172 internal fun logException(throwable: Throwable) {
173 Log.e(GlanceAppWidgetTag, "Error in Glance App Widget", throwable)
174 }
175
176 /** [Tracing] contains methods for tracing sections of GlanceAppWidget. */
177 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
178 object Tracing {
179 val enabled = AtomicBoolean(false)
180
beginGlanceAppWidgetUpdatenull181 fun beginGlanceAppWidgetUpdate() {
182 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && enabled.get()) {
183 TracingApi29Impl.beginAsyncSection("GlanceAppWidget::update", 0)
184 }
185 }
186
endGlanceAppWidgetUpdatenull187 fun endGlanceAppWidgetUpdate() {
188 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && enabled.get()) {
189 TracingApi29Impl.endAsyncSection("GlanceAppWidget::update", 0)
190 }
191 }
192 }
193
194 @RequiresApi(Build.VERSION_CODES.Q)
195 internal object TracingApi29Impl {
beginAsyncSectionnull196 fun beginAsyncSection(
197 methodName: String,
198 cookie: Int,
199 ) = Trace.beginAsyncSection(methodName, cookie)
200
201 fun endAsyncSection(
202 methodName: String,
203 cookie: Int,
204 ) = Trace.endAsyncSection(methodName, cookie)
205 }
206
207 internal val Context.appWidgetManager: AppWidgetManager
208 get() = this.getSystemService(Context.APPWIDGET_SERVICE) as AppWidgetManager
209
210 internal fun createUniqueRemoteUiName(appWidgetId: Int) = "appWidget-$appWidgetId"
211
212 internal fun AppWidgetId.toSessionKey() = createUniqueRemoteUiName(appWidgetId)
213
214 internal fun interface ContentReceiver : CoroutineContext.Element {
215 /**
216 * Provide [content] to the Glance session, suspending until the session is shut down.
217 *
218 * If this function is called concurrently with itself, the previous call will throw
219 * [CancellationException] and the new content will replace it.
220 */
221 suspend fun provideContent(content: @Composable @GlanceComposable () -> Unit): Nothing
222
223 override val key: CoroutineContext.Key<*>
224 get() = Key
225
226 companion object Key : CoroutineContext.Key<ContentReceiver>
227 }
228
runGlancenull229 internal fun GlanceAppWidget.runGlance(
230 context: Context,
231 id: GlanceId,
232 ): Flow<(@GlanceComposable @Composable () -> Unit)?> = channelFlow {
233 val contentCoroutine: AtomicReference<CancellableContinuation<Nothing>?> = AtomicReference(null)
234 val receiver = ContentReceiver { content ->
235 suspendCancellableCoroutine {
236 it.invokeOnCancellation { trySend(null) }
237 contentCoroutine.getAndSet(it)?.cancel()
238 trySend(content)
239 }
240 }
241 withContext(receiver) { provideGlance(context, id) }
242 }
243
toArrayListnull244 internal inline fun <reified T> Collection<T>.toArrayList() = ArrayList<T>(this)
245
246 @SuppressLint("ListIterator")
247 internal fun optionsBundleOf(@SuppressLint("PrimitiveInCollection") sizes: List<DpSize>): Bundle {
248 require(sizes.isNotEmpty()) { "There must be at least one size" }
249 val (minSize, maxSize) =
250 sizes.fold(sizes[0] to sizes[0]) { acc, s ->
251 DpSize(
252 androidx.compose.ui.unit.min(acc.first.width, s.width),
253 androidx.compose.ui.unit.min(acc.first.height, s.height)
254 ) to DpSize(max(acc.second.width, s.width), max(acc.second.height, s.height))
255 }
256 return Bundle().apply {
257 putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, minSize.width.value.toInt())
258 putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, minSize.height.value.toInt())
259 putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxSize.width.value.toInt())
260 putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, maxSize.height.value.toInt())
261 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
262 val sizeList = sizes.map { it.toSizeF() }.toArrayList()
263 putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizeList)
264 }
265 }
266 }
267
268 // Create a fake ID that we can use for sessions that do not publish to a bound app widget.
createFakeAppWidgetIdnull269 internal fun createFakeAppWidgetId(): AppWidgetId = AppWidgetId(Random.nextInt(Int.MIN_VALUE..-2))
270
271 internal val AppWidgetId.isFakeId
272 get() = appWidgetId in Int.MIN_VALUE..-2
273
274 // Added as a convenience for better readability.
275 internal val AppWidgetId.isRealId
276 get() = !isFakeId
277