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