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