/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.glance.appwidget
import android.content.Context
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.widget.RemoteViews
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier
import androidx.glance.findModifier
import androidx.glance.layout.Alignment
import androidx.glance.layout.HeightModifier
import androidx.glance.layout.WidthModifier
import androidx.glance.unit.Dimension
/**
* Information about a generated layout, including the layout id, ids of elements within, and other
* details about the layout contents.
*/
internal data class LayoutInfo(@LayoutRes val layoutId: Int)
/**
* Information about a [RemoteViews] created from generated layouts, including the layout id, ids
* of elements within, and other details about the layout contents.
*/
internal data class RemoteViewsInfo(
val remoteViews: RemoteViews,
val view: InsertedViewInfo,
)
internal data class InsertedViewInfo(
val mainViewId: Int = View.NO_ID,
val complexViewId: Int = View.NO_ID,
val children: Map<Int, Map<SizeSelector, Int>> = emptyMap(),
)
internal val InsertedViewInfo.isSimple: Boolean
get() = complexViewId == View.NO_ID
/**
* Container selector.
*
* This class is used to select a particular container layout.
*/
internal data class ContainerSelector(
val type: LayoutType,
val numChildren: Int,
val horizontalAlignment: Alignment.Horizontal? = null,
val verticalAlignment: Alignment.Vertical? = null,
)
internal data class ContainerInfo(@LayoutRes val layoutId: Int)
/** Type of size needed for a layout. */
internal enum class LayoutSize {
Wrap,
Fixed,
Expand,
MatchParent,
}
/** Type of a layout. */
internal enum class LayoutType {
Row,
Column,
Box,
Text,
List,
CheckBox,
CheckBoxBackport,
Button,
Frame,
LinearProgressIndicator,
CircularProgressIndicator,
VerticalGridOneColumn,
VerticalGridTwoColumns,
VerticalGridThreeColumns,
VerticalGridFourColumns,
VerticalGridFiveColumns,
VerticalGridAutoFit,
// Note: Java keywords, such as 'switch', can't be used for layout ids.
Swtch,
SwtchBackport,
ImageCrop,
ImageFit,
ImageFillBounds,
}
/** Mapping from layout type to fixed layout (if any). */
private val LayoutMap = mapOf(
LayoutType.Text to R.layout.text,
LayoutType.List to R.layout.list,
LayoutType.CheckBox to R.layout.check_box,
LayoutType.CheckBoxBackport to R.layout.check_box_backport,
LayoutType.Button to R.layout.button,
LayoutType.Swtch to R.layout.swtch,
LayoutType.SwtchBackport to R.layout.swtch_backport,
LayoutType.Frame to R.layout.frame,
LayoutType.ImageCrop to R.layout.image_crop,
LayoutType.ImageFit to R.layout.image_fit,
LayoutType.ImageFillBounds to R.layout.image_fill_bounds,
LayoutType.LinearProgressIndicator to R.layout.linear_progress_indicator,
LayoutType.CircularProgressIndicator to R.layout.circular_progress_indicator,
LayoutType.VerticalGridOneColumn to R.layout.vertical_grid_one_column,
LayoutType.VerticalGridTwoColumns to R.layout.vertical_grid_two_columns,
LayoutType.VerticalGridThreeColumns to R.layout.vertical_grid_three_columns,
LayoutType.VerticalGridFourColumns to R.layout.vertical_grid_four_columns,
LayoutType.VerticalGridFiveColumns to R.layout.vertical_grid_five_columns,
LayoutType.VerticalGridAutoFit to R.layout.vertical_grid_auto_fit,
)
internal data class SizeSelector(
val width: LayoutSize,
val height: LayoutSize,
)
/** Make the selector for a view sub, that is transforming "Fixed" into "Wrap". */
private fun LayoutSize.toViewStubSize() =
if (this == LayoutSize.Fixed) LayoutSize.Wrap else this
private fun makeViewStubSelector(width: LayoutSize, height: LayoutSize) =
SizeSelector(width = width.toViewStubSize(), height = height.toViewStubSize())
private val RootAliasTypeCount = generatedRootLayoutShifts.size
internal val TopLevelLayoutsCount: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
RootAliasCount
} else {
RootAliasCount / RootAliasTypeCount
}
/**
* Create the [RemoteViews] that can be used to create the child.
*
* @param translationContext Context for the translation for that node
* @param modifier Modifier attached to the view that will be added to the root
* @param aliasIndex Alias to use to create this root view
* @return The [RemoteViews] created and the descriptor needed to be able to add the first view.
*/
internal fun createRootView(
translationContext: TranslationContext,
modifier: GlanceModifier,
aliasIndex: Int
): RemoteViewsInfo {
val context = translationContext.context
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
require(aliasIndex < RootAliasCount) {
"Index of the root view cannot be more than $RootAliasCount, " +
"currently $aliasIndex"
}
val sizeSelector = SizeSelector(LayoutSize.Wrap, LayoutSize.Wrap)
val layoutId = FirstRootAlias + aliasIndex
return RemoteViewsInfo(
remoteViews = remoteViews(
translationContext,
layoutId
).apply {
modifier.findModifier<WidthModifier>()?.let {
applySimpleWidthModifier(context, this, it, R.id.rootView)
}
modifier.findModifier<HeightModifier>()?.let {
applySimpleHeightModifier(context, this, it, R.id.rootView)
}
},
view = InsertedViewInfo(children = mapOf(0 to mapOf(sizeSelector to R.id.rootStubId)))
)
}
require(RootAliasTypeCount * aliasIndex < RootAliasCount) {
"Index of the root view cannot be more than ${RootAliasCount / 4}, " +
"currently $aliasIndex"
}
val widthMod =
modifier.findModifier<WidthModifier>()?.width?.resolveDimension(context) ?: Dimension.Wrap
val heightMod =
modifier.findModifier<HeightModifier>()?.height?.resolveDimension(context) ?: Dimension.Wrap
val width = if (widthMod == Dimension.Fill) LayoutSize.MatchParent else LayoutSize.Wrap
val height = if (heightMod == Dimension.Fill) LayoutSize.MatchParent else LayoutSize.Wrap
val sizeSelector = makeViewStubSelector(width, height)
val layoutIdShift = generatedRootLayoutShifts[sizeSelector]
?: throw IllegalStateException("Cannot find root element for size [$width, $height]")
val layoutId = FirstRootAlias + RootAliasTypeCount * aliasIndex + layoutIdShift
return RemoteViewsInfo(
remoteViews = remoteViews(translationContext, layoutId),
view = InsertedViewInfo(children = mapOf(0 to mapOf(sizeSelector to R.id.rootStubId))),
)
}
internal fun RemoteViews.insertView(
translationContext: TranslationContext,
type: LayoutType,
modifier: GlanceModifier
): InsertedViewInfo {
val childLayout = LayoutMap[type]
?: throw IllegalArgumentException("Cannot use `insertView` with a container like $type")
return insertViewInternal(translationContext, childLayout, modifier)
}
private fun RemoteViews.insertViewInternal(
translationContext: TranslationContext,
@LayoutRes childLayout: Int,
modifier: GlanceModifier
): InsertedViewInfo {
val pos = translationContext.itemPosition
val widthMod = modifier.findModifier<WidthModifier>()?.width ?: Dimension.Wrap
val heightMod = modifier.findModifier<HeightModifier>()?.height ?: Dimension.Wrap
// Null unless the view Id is specified by some attributes.
val specifiedViewId = if (modifier.all { it !is AppWidgetBackgroundModifier }) {
null
} else {
check(!translationContext.isBackgroundSpecified.getAndSet(true)) {
"At most one view can be set as AppWidgetBackground."
}
android.R.id.background
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val width = if (widthMod == Dimension.Expand) LayoutSize.Expand else LayoutSize.Wrap
val height = if (heightMod == Dimension.Expand) LayoutSize.Expand else LayoutSize.Wrap
val stubId = selectChild(translationContext, pos, width, height)
val resId = inflateViewStub(translationContext, stubId, childLayout, specifiedViewId)
return InsertedViewInfo(mainViewId = resId)
}
val context = translationContext.context
val width = widthMod.resolveDimension(context).toSpecSize()
val height = heightMod.resolveDimension(context).toSpecSize()
val stubId = selectChild(translationContext, pos, width, height)
val needsResize = width == LayoutSize.Fixed || height == LayoutSize.Fixed
return if (needsResize) {
val complexLayout = generatedComplexLayouts[SizeSelector(width, height)]
?: throw IllegalArgumentException(
"Could not find complex layout for width=$width, height=$height"
)
val complexId = inflateViewStub(translationContext, stubId, complexLayout.layoutId)
val childId =
inflateViewStub(translationContext, R.id.glanceViewStub, childLayout, specifiedViewId)
InsertedViewInfo(mainViewId = childId, complexViewId = complexId)
} else {
val resId = inflateViewStub(translationContext, stubId, childLayout, specifiedViewId)
InsertedViewInfo(mainViewId = resId)
}
}
@IdRes
private fun RemoteViews.selectChild(
translationContext: TranslationContext,
pos: Int,
width: LayoutSize,
height: LayoutSize
): Int {
val child = makeViewStubSelector(width, height)
val children = translationContext.parentContext.children[pos]
?: throw IllegalStateException("Parent doesn't have child position $pos")
val stubId = children[child]
?: throw IllegalStateException("No child for position $pos and size $width x $height")
children.values
.filter { it != stubId }
.forEach {
inflateViewStub(translationContext, it, R.layout.deleted_view, R.id.deletedViewId)
}
return stubId
}
internal fun RemoteViews.insertContainerView(
translationContext: TranslationContext,
type: LayoutType,
numChildren: Int,
modifier: GlanceModifier,
horizontalAlignment: Alignment.Horizontal?,
verticalAlignment: Alignment.Vertical?,
): InsertedViewInfo {
val childLayout = generatedContainers[ContainerSelector(
type,
numChildren,
horizontalAlignment,
verticalAlignment
)]
?: throw IllegalArgumentException("Cannot find container $type with $numChildren children")
val childrenMapping = generatedChildren[type]
?: throw IllegalArgumentException("Cannot find generated children for $type")
return insertViewInternal(translationContext, childLayout.layoutId, modifier)
.copy(children = childrenMapping)
}
private fun Dimension.toSpecSize(): LayoutSize =
when (this) {
is Dimension.Wrap -> LayoutSize.Wrap
is Dimension.Expand -> LayoutSize.Expand
is Dimension.Fill -> LayoutSize.MatchParent
is Dimension.Dp, is Dimension.Resource -> LayoutSize.Fixed
}
internal fun Dimension.resolveDimension(context: Context): Dimension {
if (this !is Dimension.Resource) return this
val sizePx = context.resources.getDimension(res)
return when (sizePx.toInt()) {
ViewGroup.LayoutParams.MATCH_PARENT -> Dimension.Fill
ViewGroup.LayoutParams.WRAP_CONTENT -> Dimension.Wrap
else -> Dimension.Dp((sizePx / context.resources.displayMetrics.density).dp)
}
}