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
18 
19 import android.content.ComponentName
20 import android.content.Context
21 import android.os.Build
22 import android.util.Log
23 import android.util.SizeF
24 import android.view.Gravity
25 import android.view.View
26 import android.widget.RemoteViews
27 import androidx.annotation.LayoutRes
28 import androidx.annotation.RequiresApi
29 import androidx.annotation.VisibleForTesting
30 import androidx.compose.ui.unit.DpSize
31 import androidx.compose.ui.unit.dp
32 import androidx.compose.ui.unit.isSpecified
33 import androidx.core.widget.RemoteViewsCompat.setLinearLayoutGravity
34 import androidx.glance.Emittable
35 import androidx.glance.EmittableButton
36 import androidx.glance.EmittableImage
37 import androidx.glance.appwidget.lazy.EmittableLazyColumn
38 import androidx.glance.appwidget.lazy.EmittableLazyListItem
39 import androidx.glance.appwidget.lazy.EmittableLazyVerticalGrid
40 import androidx.glance.appwidget.lazy.EmittableLazyVerticalGridListItem
41 import androidx.glance.appwidget.translators.setText
42 import androidx.glance.appwidget.translators.translateEmittableCheckBox
43 import androidx.glance.appwidget.translators.translateEmittableCircularProgressIndicator
44 import androidx.glance.appwidget.translators.translateEmittableImage
45 import androidx.glance.appwidget.translators.translateEmittableLazyColumn
46 import androidx.glance.appwidget.translators.translateEmittableLazyListItem
47 import androidx.glance.appwidget.translators.translateEmittableLazyVerticalGrid
48 import androidx.glance.appwidget.translators.translateEmittableLazyVerticalGridListItem
49 import androidx.glance.appwidget.translators.translateEmittableLinearProgressIndicator
50 import androidx.glance.appwidget.translators.translateEmittableRadioButton
51 import androidx.glance.appwidget.translators.translateEmittableSwitch
52 import androidx.glance.appwidget.translators.translateEmittableText
53 import androidx.glance.findModifier
54 import androidx.glance.layout.Alignment
55 import androidx.glance.layout.EmittableBox
56 import androidx.glance.layout.EmittableColumn
57 import androidx.glance.layout.EmittableRow
58 import androidx.glance.layout.EmittableSpacer
59 import androidx.glance.layout.PaddingModifier
60 import androidx.glance.layout.padding
61 import androidx.glance.text.EmittableText
62 import java.util.concurrent.atomic.AtomicBoolean
63 import java.util.concurrent.atomic.AtomicInteger
64 
65 internal fun translateComposition(
66     context: Context,
67     appWidgetId: Int,
68     element: RemoteViewsRoot,
69     layoutConfiguration: LayoutConfiguration?,
70     rootViewIndex: Int,
71     layoutSize: DpSize,
72     actionBroadcastReceiver: ComponentName? = null,
73     glanceComponents: GlanceComponents = GlanceComponents.getDefault(context),
74 ) =
75     translateComposition(
76         TranslationContext(
77             context,
78             appWidgetId,
79             context.isRtl,
80             layoutConfiguration,
81             itemPosition = -1,
82             layoutSize = layoutSize,
83             actionBroadcastReceiver = actionBroadcastReceiver,
84             glanceComponents = glanceComponents,
85         ),
86         element.children,
87         rootViewIndex,
88     )
89 
90 @VisibleForTesting internal var forceRtl: Boolean? = null
91 
92 private val Context.isRtl: Boolean
93     get() = forceRtl ?: (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL)
94 
95 @RequiresApi(Build.VERSION_CODES.S)
96 private object Api31Impl {
97     fun createRemoteViews(sizeMap: Map<SizeF, RemoteViews>): RemoteViews = RemoteViews(sizeMap)
98 }
99 
translateCompositionnull100 internal fun translateComposition(
101     translationContext: TranslationContext,
102     children: List<Emittable>,
103     rootViewIndex: Int
104 ): RemoteViews {
105     if (children.all { it is EmittableSizeBox }) {
106         // If the children of root are all EmittableSizeBoxes, then we must translate each
107         // EmittableSizeBox into a distinct RemoteViews object. Then, we combine them into one
108         // multi-sized RemoteViews (a RemoteViews that contains either landscape & portrait RVs or
109         // multiple RVs mapped by size).
110         val sizeMode = (children.first() as EmittableSizeBox).sizeMode
111         val views =
112             children.map { child ->
113                 val size = (child as EmittableSizeBox).size
114                 val remoteViewsInfo =
115                     createRootView(translationContext, child.modifier, rootViewIndex)
116                 val rv =
117                     remoteViewsInfo.remoteViews.apply {
118                         translateChild(
119                             translationContext.forRootAndSize(root = remoteViewsInfo, size),
120                             child
121                         )
122                     }
123                 size.toSizeF() to rv
124             }
125         return when (sizeMode) {
126             is SizeMode.Single -> views.single().second
127             is SizeMode.Responsive,
128             SizeMode.Exact -> {
129                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
130                     Api31Impl.createRemoteViews(views.toMap())
131                 } else {
132                     require(views.size == 1 || views.size == 2) { "unsupported views size" }
133                     combineLandscapeAndPortrait(views.map { it.second })
134                 }
135             }
136         }
137     } else {
138         return children.single().let { child ->
139             val remoteViewsInfo = createRootView(translationContext, child.modifier, rootViewIndex)
140             remoteViewsInfo.remoteViews.apply {
141                 translateChild(translationContext.forRoot(root = remoteViewsInfo), child)
142             }
143         }
144     }
145 }
146 
combineLandscapeAndPortraitnull147 private fun combineLandscapeAndPortrait(views: List<RemoteViews>): RemoteViews =
148     when (views.size) {
149         2 -> RemoteViews(views[0], views[1])
150         1 -> views[0]
151         else -> throw IllegalArgumentException("There must be between 1 and 2 views.")
152     }
153 
154 private const val LAST_INVALID_VIEW_ID = -1
155 
156 internal data class TranslationContext(
157     val context: Context,
158     val appWidgetId: Int,
159     val isRtl: Boolean,
160     val layoutConfiguration: LayoutConfiguration?,
161     val itemPosition: Int,
162     val isLazyCollectionDescendant: Boolean = false,
163     val lastViewId: AtomicInteger = AtomicInteger(LAST_INVALID_VIEW_ID),
164     val parentContext: InsertedViewInfo = InsertedViewInfo(),
165     val isBackgroundSpecified: AtomicBoolean = AtomicBoolean(false),
166     val layoutSize: DpSize = DpSize.Zero,
167     val layoutCollectionViewId: Int = View.NO_ID,
168     val layoutCollectionItemId: Int = -1,
169     val canUseSelectableGroup: Boolean = false,
170     val actionTargetId: Int? = null,
171     val actionBroadcastReceiver: ComponentName? = null,
172     val glanceComponents: GlanceComponents,
173 ) {
nextViewIdnull174     fun nextViewId() =
175         lastViewId.incrementAndGet().let {
176             check(it < TotalViewCount) { "There are too many views" }
177             FirstViewId + it
178         }
179 
forChildnull180     fun forChild(parent: InsertedViewInfo, pos: Int): TranslationContext =
181         copy(itemPosition = pos, parentContext = parent)
182 
183     fun forRoot(root: RemoteViewsInfo): TranslationContext =
184         forChild(pos = 0, parent = root.view)
185             .copy(
186                 isBackgroundSpecified = AtomicBoolean(false),
187                 lastViewId = AtomicInteger(LAST_INVALID_VIEW_ID),
188             )
189 
190     fun forRootAndSize(root: RemoteViewsInfo, layoutSize: DpSize): TranslationContext =
191         forChild(pos = 0, parent = root.view)
192             .copy(
193                 isBackgroundSpecified = AtomicBoolean(false),
194                 lastViewId = AtomicInteger(LAST_INVALID_VIEW_ID),
195                 layoutSize = layoutSize
196             )
197 
198     fun resetViewId(newViewId: Int = 0) = copy(lastViewId = AtomicInteger(newViewId))
199 
200     fun forLazyCollection(viewId: Int) =
201         copy(isLazyCollectionDescendant = true, layoutCollectionViewId = viewId)
202 
203     fun forLazyViewItem(itemId: Int, newViewId: Int = 0) =
204         copy(lastViewId = AtomicInteger(newViewId), layoutCollectionViewId = itemId)
205 
206     fun canUseSelectableGroup() = copy(canUseSelectableGroup = true)
207 
208     fun forActionTargetId(viewId: Int) = copy(actionTargetId = viewId)
209 }
210 
211 internal fun DpSize.toSizeString(): String {
212     return if (isSpecified) {
213         "${width}x$height"
214     } else {
215         "Unspecified"
216     }
217 }
218 
translateChildnull219 internal fun RemoteViews.translateChild(
220     translationContext: TranslationContext,
221     element: Emittable
222 ) {
223     when (element) {
224         is EmittableBox -> translateEmittableBox(translationContext, element)
225         is EmittableButton -> translateEmittableButton(translationContext, element)
226         is EmittableRow -> translateEmittableRow(translationContext, element)
227         is EmittableColumn -> translateEmittableColumn(translationContext, element)
228         is EmittableText -> translateEmittableText(translationContext, element)
229         is EmittableLazyListItem -> translateEmittableLazyListItem(translationContext, element)
230         is EmittableLazyColumn -> translateEmittableLazyColumn(translationContext, element)
231         is EmittableAndroidRemoteViews -> {
232             translateEmittableAndroidRemoteViews(translationContext, element)
233         }
234         is EmittableCheckBox -> translateEmittableCheckBox(translationContext, element)
235         is EmittableSpacer -> translateEmittableSpacer(translationContext, element)
236         is EmittableSwitch -> translateEmittableSwitch(translationContext, element)
237         is EmittableImage -> translateEmittableImage(translationContext, element)
238         is EmittableLinearProgressIndicator -> {
239             translateEmittableLinearProgressIndicator(translationContext, element)
240         }
241         is EmittableCircularProgressIndicator -> {
242             translateEmittableCircularProgressIndicator(translationContext, element)
243         }
244         is EmittableLazyVerticalGrid -> {
245             translateEmittableLazyVerticalGrid(translationContext, element)
246         }
247         is EmittableLazyVerticalGridListItem -> {
248             translateEmittableLazyVerticalGridListItem(translationContext, element)
249         }
250         is EmittableRadioButton -> translateEmittableRadioButton(translationContext, element)
251         is EmittableSizeBox -> translateEmittableSizeBox(translationContext, element)
252         else -> {
253             throw IllegalArgumentException(
254                 "Unknown element type ${element.javaClass.canonicalName}"
255             )
256         }
257     }
258 }
259 
translateEmittableSizeBoxnull260 internal fun RemoteViews.translateEmittableSizeBox(
261     translationContext: TranslationContext,
262     element: EmittableSizeBox
263 ) {
264     require(element.children.size <= 1) {
265         "Size boxes can only have at most one child ${element.children.size}. " +
266             "The normalization of the composition tree failed."
267     }
268     element.children.firstOrNull()?.let { translateChild(translationContext, it) }
269 }
270 
remoteViewsnull271 internal fun remoteViews(translationContext: TranslationContext, @LayoutRes layoutId: Int) =
272     RemoteViews(translationContext.context.packageName, layoutId)
273 
274 internal fun Alignment.Horizontal.toGravity(): Int =
275     when (this) {
276         Alignment.Horizontal.Start -> Gravity.START
277         Alignment.Horizontal.End -> Gravity.END
278         Alignment.Horizontal.CenterHorizontally -> Gravity.CENTER_HORIZONTAL
279         else -> {
280             Log.w(GlanceAppWidgetTag, "Unknown horizontal alignment: $this")
281             Gravity.START
282         }
283     }
284 
toGravitynull285 internal fun Alignment.Vertical.toGravity(): Int =
286     when (this) {
287         Alignment.Vertical.Top -> Gravity.TOP
288         Alignment.Vertical.Bottom -> Gravity.BOTTOM
289         Alignment.Vertical.CenterVertically -> Gravity.CENTER_VERTICAL
290         else -> {
291             Log.w(GlanceAppWidgetTag, "Unknown vertical alignment: $this")
292             Gravity.TOP
293         }
294     }
295 
toGravitynull296 internal fun Alignment.toGravity() = horizontal.toGravity() or vertical.toGravity()
297 
298 private fun RemoteViews.translateEmittableBox(
299     translationContext: TranslationContext,
300     element: EmittableBox
301 ) {
302     val viewDef =
303         insertContainerView(
304             translationContext,
305             LayoutType.Box,
306             element.children.size,
307             element.modifier,
308             element.contentAlignment.horizontal,
309             element.contentAlignment.vertical,
310         )
311     applyModifiers(translationContext, this, element.modifier, viewDef)
312     element.children.forEach {
313         it.modifier = it.modifier.then(AlignmentModifier(element.contentAlignment))
314     }
315     setChildren(translationContext, viewDef, element.children)
316 }
317 
RemoteViewsnull318 private fun RemoteViews.translateEmittableRow(
319     translationContext: TranslationContext,
320     element: EmittableRow
321 ) {
322     val layoutType =
323         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && element.modifier.isSelectableGroup) {
324             LayoutType.RadioRow
325         } else {
326             LayoutType.Row
327         }
328     val viewDef =
329         insertContainerView(
330             translationContext,
331             layoutType,
332             element.children.size,
333             element.modifier,
334             horizontalAlignment = null,
335             verticalAlignment = element.verticalAlignment,
336         )
337     setLinearLayoutGravity(
338         viewDef.mainViewId,
339         Alignment(element.horizontalAlignment, element.verticalAlignment).toGravity()
340     )
341     applyModifiers(translationContext.canUseSelectableGroup(), this, element.modifier, viewDef)
342     setChildren(translationContext, viewDef, element.children)
343     if (element.modifier.isSelectableGroup) checkSelectableGroupChildren(element.children)
344 }
345 
RemoteViewsnull346 private fun RemoteViews.translateEmittableColumn(
347     translationContext: TranslationContext,
348     element: EmittableColumn
349 ) {
350     val layoutType =
351         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && element.modifier.isSelectableGroup) {
352             LayoutType.RadioColumn
353         } else {
354             LayoutType.Column
355         }
356     val viewDef =
357         insertContainerView(
358             translationContext,
359             layoutType,
360             element.children.size,
361             element.modifier,
362             horizontalAlignment = element.horizontalAlignment,
363             verticalAlignment = null,
364         )
365     setLinearLayoutGravity(
366         viewDef.mainViewId,
367         Alignment(element.horizontalAlignment, element.verticalAlignment).toGravity()
368     )
369     applyModifiers(translationContext.canUseSelectableGroup(), this, element.modifier, viewDef)
370     setChildren(translationContext, viewDef, element.children)
371     if (element.modifier.isSelectableGroup) checkSelectableGroupChildren(element.children)
372 }
373 
checkSelectableGroupChildrennull374 private fun checkSelectableGroupChildren(children: List<Emittable>) {
375     check(children.count { it is EmittableRadioButton && it.checked } <= 1) {
376         "When using GlanceModifier.selectableGroup(), no more than one RadioButton " +
377             "may be checked at a time."
378     }
379 }
380 
translateEmittableAndroidRemoteViewsnull381 private fun RemoteViews.translateEmittableAndroidRemoteViews(
382     translationContext: TranslationContext,
383     element: EmittableAndroidRemoteViews
384 ) {
385     val rv =
386         if (element.children.isEmpty()) {
387             element.remoteViews
388         } else {
389             check(element.containerViewId != View.NO_ID) {
390                 "To add children to an `AndroidRemoteViews`, its `containerViewId` must be set."
391             }
392             element.remoteViews.copy().apply {
393                 removeAllViews(element.containerViewId)
394                 element.children.forEachIndexed { index, child ->
395                     val rvInfo = createRootView(translationContext, child.modifier, index)
396                     val rv = rvInfo.remoteViews
397                     rv.translateChild(translationContext.forRoot(rvInfo), child)
398                     addChildView(element.containerViewId, rv, index)
399                 }
400             }
401         }
402     val viewDef = insertView(translationContext, LayoutType.Frame, element.modifier)
403     applyModifiers(translationContext, this, element.modifier, viewDef)
404     removeAllViews(viewDef.mainViewId)
405     addChildView(viewDef.mainViewId, rv, stableId = 0)
406 }
407 
RemoteViewsnull408 private fun RemoteViews.translateEmittableButton(
409     translationContext: TranslationContext,
410     element: EmittableButton
411 ) {
412     check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
413         "Buttons in Android R and below are emulated using a EmittableBox containing the text."
414     }
415     val viewDef = insertView(translationContext, LayoutType.Button, element.modifier)
416     setText(
417         translationContext,
418         viewDef.mainViewId,
419         element.text,
420         element.style,
421         maxLines = element.maxLines,
422         verticalTextGravity = Gravity.CENTER_VERTICAL,
423     )
424 
425     // Adjust appWidget specific modifiers.
426     element.modifier = element.modifier.enabled(element.enabled).cornerRadius(16.dp)
427     if (element.modifier.findModifier<PaddingModifier>() == null) {
428         element.modifier = element.modifier.padding(horizontal = 16.dp, vertical = 8.dp)
429     }
430     applyModifiers(translationContext, this, element.modifier, viewDef)
431 }
432 
RemoteViewsnull433 private fun RemoteViews.translateEmittableSpacer(
434     translationContext: TranslationContext,
435     element: EmittableSpacer
436 ) {
437     val viewDef = insertView(translationContext, LayoutType.Frame, element.modifier)
438     applyModifiers(translationContext, this, element.modifier, viewDef)
439 }
440 
441 // Sets the emittables as children to the view. This first remove any previously added view, the
442 // add a view per child, with a stable id if of Android S+. Currently the stable id is the index
443 // of the child in the iterable.
setChildrennull444 internal fun RemoteViews.setChildren(
445     translationContext: TranslationContext,
446     parentDef: InsertedViewInfo,
447     children: List<Emittable>
448 ) {
449     children.take(10).forEachIndexed { index, child ->
450         translateChild(
451             translationContext.forChild(parent = parentDef, pos = index),
452             child,
453         )
454     }
455 }
456 
457 /** Add stable view if on Android S+, otherwise simply add the view. */
addChildViewnull458 internal fun RemoteViews.addChildView(viewId: Int, childView: RemoteViews, stableId: Int) {
459     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
460         RemoteViewsTranslatorApi31Impl.addChildView(this, viewId, childView, stableId)
461         return
462     }
463     addView(viewId, childView)
464 }
465 
466 /** Copy a RemoteViews (the exact method depends on the version of Android) */
467 @Suppress("DEPRECATION") // RemoteViews.clone must be used before Android P.
RemoteViewsnull468 private fun RemoteViews.copy(): RemoteViews =
469     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
470         RemoteViewsTranslatorApi28Impl.copyRemoteViews(this)
471     } else {
472         clone()
473     }
474 
475 @RequiresApi(Build.VERSION_CODES.P)
476 private object RemoteViewsTranslatorApi28Impl {
copyRemoteViewsnull477     fun copyRemoteViews(rv: RemoteViews) = RemoteViews(rv)
478 }
479 
480 @RequiresApi(Build.VERSION_CODES.S)
481 private object RemoteViewsTranslatorApi31Impl {
482     fun addChildView(rv: RemoteViews, viewId: Int, childView: RemoteViews, stableId: Int) {
483         rv.addStableView(viewId, childView, stableId)
484     }
485 }
486