SizeBox.kt

/*
 * Copyright 2022 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.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.unit.DpSize
import androidx.glance.Emittable
import androidx.glance.EmittableWithChildren
import androidx.glance.GlanceModifier
import androidx.glance.GlanceNode
import androidx.glance.LocalSize
import androidx.glance.layout.fillMaxSize

/**
 * A marker for the translator that indicates that this [EmittableSizeBox] and its children should
 * be translated into a distinct [android.widget.RemoteViews] object.
 *
 * EmittableSizeBox is only functional when it is a direct child of the root [RemoteViewsRoot].
 * Multiple EmittableSizeBox children will each be translated into a distinct RemoteViews, then
 * combined into one multi-sized RemoteViews.
 */
internal class EmittableSizeBox : EmittableWithChildren() {
    override var modifier: GlanceModifier
        get() = children.singleOrNull()?.modifier
            ?: GlanceModifier.fillMaxSize()
        set(_) {
            throw IllegalAccessError("You cannot set the modifier of an EmittableSizeBox")
        }
    var size: DpSize = DpSize.Unspecified
    var sizeMode: SizeMode = SizeMode.Single

    override fun copy(): Emittable = EmittableSizeBox().also {
        it.size = size
        it.sizeMode = sizeMode
        it.children.addAll(children.map { it.copy() })
    }

    override fun toString(): String = "EmittableSizeBox(" +
        "size=$size, " +
        "sizeMode=$sizeMode, " +
        "children=[\n${childrenToString()}\n]" +
        ")"
}

/**
 * This composable emits a marker that lets the translator know that this [SizeBox] and its children
 * should be translated into a distinct RemoteViews that is then combined with its siblings to form
 * a multi-sized RemoteViews.
 *
 * This should not be used directly. The correct SizeBoxes can be generated with [ForEachSize].
 */
@Composable
internal fun SizeBox(
    size: DpSize,
    sizeMode: SizeMode,
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(LocalSize provides size) {
        GlanceNode(
            factory = ::EmittableSizeBox,
            update = {
                this.set(size) { this.size = it }
                this.set(sizeMode) { this.sizeMode = it }
            },
            content = content
        )
    }
}

/**
 * For each size indicated by [sizeMode], run [content] with a [SizeBox] set to the corresponding
 * size.
 */
@Composable
internal fun ForEachSize(
    sizeMode: SizeMode,
    minSize: DpSize,
    content: @Composable () -> Unit
) {
    val sizes = when (sizeMode) {
        is SizeMode.Single -> listOf(minSize)
        is SizeMode.Exact -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            LocalAppWidgetOptions.current.extractAllSizes { minSize }
        } else {
            LocalAppWidgetOptions.current.extractOrientationSizes()
                .ifEmpty { listOf(minSize) }
        }
        is SizeMode.Responsive -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            sizeMode.sizes
        } else {
            val smallestSize = sizeMode.sizes.sortedBySize()[0]
            LocalAppWidgetOptions.current.extractOrientationSizes()
                .map { findBestSize(it, sizeMode.sizes) ?: smallestSize }
                .ifEmpty { listOf(smallestSize, smallestSize) }
        }
    }
    sizes.map { size ->
        SizeBox(size, sizeMode, content)
    }
}