Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a1dbbeab authored by Shamali P's avatar Shamali P
Browse files

Add preview container size helper to display previews in picker.

While we want previews to be displayed at true size, today, there is
less consistency among different widgets. This change is a first step
towards consistent sizes. In this change, we provide helper classes
that provide the size of the container in terms of grid spans.

Note: This does not change the size for rendering widget previews; but,
we take that original rendered preview and scale it maintaining the
aspect ratio to display it in one of closest container sizes.

Bug: 319152349
Flag: N/A
Test: Includes a unit test & manual with the child cls
Change-Id: I335373aa1be9a41fe039c98cded0113a007ad8c4
parent 7574bed0
Loading
Loading
Loading
Loading
+91 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 com.android.launcher3.widget.picker.util

import com.android.launcher3.DeviceProfile
import com.android.launcher3.model.WidgetItem
import kotlin.math.abs

/** Size of a preview container in terms of the grid spans. */
data class WidgetPreviewContainerSize(@JvmField val spanX: Int, @JvmField val spanY: Int) {
    companion object {
        /**
         * Returns the size of the preview container in which the given widget's preview should be
         * displayed (by scaling it if necessary).
         */
        fun forItem(item: WidgetItem, dp: DeviceProfile): WidgetPreviewContainerSize {
            val sizes =
                if (dp.isTablet && !dp.isTwoPanels) {
                    TABLET_WIDGET_PREVIEW_SIZES
                } else {
                    HANDHELD_WIDGET_PREVIEW_SIZES
                }

            for ((index, containerSize) in sizes.withIndex()) {
                if (containerSize.spanX == item.spanX && containerSize.spanY == item.spanY) {
                    return containerSize // Exact match!
                }
                if (containerSize.spanX <= item.spanX && containerSize.spanY <= item.spanY) {
                    return findClosestFittingContainer(
                        containerSizes = sizes.toList(),
                        startIndex = index,
                        item = item
                    )
                }
            }
            // Use largest container if no match found
            return sizes.elementAt(0)
        }

        private fun findClosestFittingContainer(
            containerSizes: List<WidgetPreviewContainerSize>,
            startIndex: Int,
            item: WidgetItem
        ): WidgetPreviewContainerSize {
            // Checks if it's a smaller container, but close enough to keep the down-scale minimal.
            fun hasAcceptableSize(currentIndex: Int): Boolean {
                val container = containerSizes[currentIndex]
                val isSmallerThanItem =
                    container.spanX <= item.spanX && container.spanY <= item.spanY
                val isCloseToItemSize =
                    (item.spanY - container.spanY <= 1) && (item.spanX - container.spanX <= 1)

                return isSmallerThanItem && isCloseToItemSize
            }

            var currentIndex = startIndex
            var match = containerSizes[currentIndex]
            val itemCellSizeRatio = item.spanX.toFloat() / item.spanY
            var lastCellSizeRatioDiff = Float.MAX_VALUE

            // Look for a smaller container (up to an acceptable extent) with closest cell size
            // ratio.
            while (currentIndex <= containerSizes.lastIndex && hasAcceptableSize(currentIndex)) {
                val current = containerSizes[currentIndex]
                val currentCellSizeRatio = current.spanX.toFloat() / current.spanY
                val currentCellSizeRatioDiff = abs(itemCellSizeRatio - currentCellSizeRatio)

                if (currentCellSizeRatioDiff < lastCellSizeRatioDiff) {
                    lastCellSizeRatioDiff = currentCellSizeRatioDiff
                    match = containerSizes[currentIndex]
                }
                currentIndex++
            }
            return match
        }
    }
}
+52 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 com.android.launcher3.widget.picker.util

/**
 * An ordered list of recommended sizes for the preview containers in handheld devices.
 *
 * Size of the preview container in which a widget's preview can be displayed.
 */
val HANDHELD_WIDGET_PREVIEW_SIZES: List<WidgetPreviewContainerSize> =
    listOf(
        WidgetPreviewContainerSize(spanX = 4, spanY = 3),
        WidgetPreviewContainerSize(spanX = 4, spanY = 2),
        WidgetPreviewContainerSize(spanX = 2, spanY = 3),
        WidgetPreviewContainerSize(spanX = 2, spanY = 2),
        WidgetPreviewContainerSize(spanX = 4, spanY = 1),
        WidgetPreviewContainerSize(spanX = 2, spanY = 1),
        WidgetPreviewContainerSize(spanX = 1, spanY = 1),
    )

/**
 * An ordered list of recommended sizes for the preview containers in tablet devices (with larger
 * grids).
 *
 * Size of the preview container in which a widget's preview can be displayed (by scaling the
 * preview if necessary).
 */
val TABLET_WIDGET_PREVIEW_SIZES: List<WidgetPreviewContainerSize> =
    listOf(
        WidgetPreviewContainerSize(spanX = 3, spanY = 4),
        WidgetPreviewContainerSize(spanX = 3, spanY = 3),
        WidgetPreviewContainerSize(spanX = 3, spanY = 2),
        WidgetPreviewContainerSize(spanX = 2, spanY = 3),
        WidgetPreviewContainerSize(spanX = 2, spanY = 2),
        WidgetPreviewContainerSize(spanX = 3, spanY = 1),
        WidgetPreviewContainerSize(spanX = 2, spanY = 1),
        WidgetPreviewContainerSize(spanX = 1, spanY = 1),
    )
+154 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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 com.android.launcher3.widget.picker.util

import android.content.ComponentName
import android.content.Context
import android.graphics.Point
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.DeviceProfile
import com.android.launcher3.InvariantDeviceProfile
import com.android.launcher3.LauncherAppState
import com.android.launcher3.icons.IconCache
import com.android.launcher3.model.WidgetItem
import com.android.launcher3.util.ActivityContextWrapper
import com.android.launcher3.util.WidgetUtils.createAppWidgetProviderInfo
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidJUnit4::class)
class WidgetPreviewContainerSizesTest {
    private lateinit var context: Context
    private lateinit var deviceProfile: DeviceProfile
    private lateinit var testInvariantProfile: InvariantDeviceProfile

    @Mock private lateinit var iconCache: IconCache

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
        testInvariantProfile = LauncherAppState.getIDP(context)
        deviceProfile = testInvariantProfile.getDeviceProfile(context).copy(context)
    }

    @Test
    fun widgetPreviewContainerSize_forItem_returnsCorrectContainerSize() {
        val testSizes = getTestSizes(deviceProfile)
        val expectedPreviewContainers = testSizes.values.toList()

        for ((index, widgetSize) in testSizes.keys.withIndex()) {
            val widgetItem = createWidgetItem(widgetSize, context, testInvariantProfile, iconCache)

            assertWithMessage("size for $widgetSize should be: ${expectedPreviewContainers[index]}")
                .that(WidgetPreviewContainerSize.forItem(widgetItem, deviceProfile))
                .isEqualTo(expectedPreviewContainers[index])
        }
    }

    companion object {
        private const val TEST_PACKAGE = "com.google.test"

        private val HANDHELD_TEST_SIZES: Map<Point, WidgetPreviewContainerSize> =
            mapOf(
                // 1x1
                Point(1, 1) to WidgetPreviewContainerSize(1, 1),
                // 2x1
                Point(2, 1) to WidgetPreviewContainerSize(2, 1),
                Point(3, 1) to WidgetPreviewContainerSize(2, 1),
                // 4x1
                Point(4, 1) to WidgetPreviewContainerSize(4, 1),
                // 2x2
                Point(2, 2) to WidgetPreviewContainerSize(2, 2),
                Point(3, 3) to WidgetPreviewContainerSize(2, 2),
                Point(3, 2) to WidgetPreviewContainerSize(2, 2),
                // 2x3
                Point(2, 3) to WidgetPreviewContainerSize(2, 3),
                Point(3, 4) to WidgetPreviewContainerSize(2, 3),
                Point(3, 5) to WidgetPreviewContainerSize(2, 3),
                // 4x2
                Point(4, 2) to WidgetPreviewContainerSize(4, 2),
                // 4x3
                Point(4, 3) to WidgetPreviewContainerSize(4, 3),
                Point(4, 4) to WidgetPreviewContainerSize(4, 3),
            )

        private val TABLET_TEST_SIZES: Map<Point, WidgetPreviewContainerSize> =
            mapOf(
                // 1x1
                Point(1, 1) to WidgetPreviewContainerSize(1, 1),
                // 2x1
                Point(2, 1) to WidgetPreviewContainerSize(2, 1),
                // 3x1
                Point(3, 1) to WidgetPreviewContainerSize(3, 1),
                Point(4, 1) to WidgetPreviewContainerSize(3, 1),
                // 2x2
                Point(2, 2) to WidgetPreviewContainerSize(2, 2),
                // 2x3
                Point(2, 3) to WidgetPreviewContainerSize(2, 3),
                // 3x2
                Point(3, 2) to WidgetPreviewContainerSize(3, 2),
                Point(4, 2) to WidgetPreviewContainerSize(3, 2),
                Point(5, 2) to WidgetPreviewContainerSize(3, 2),
                // 3x3
                Point(3, 3) to WidgetPreviewContainerSize(3, 3),
                Point(4, 4) to WidgetPreviewContainerSize(3, 3),
                // 3x4
                Point(5, 4) to WidgetPreviewContainerSize(3, 4),
                Point(3, 4) to WidgetPreviewContainerSize(3, 4),
                Point(5, 5) to WidgetPreviewContainerSize(3, 4),
                Point(6, 4) to WidgetPreviewContainerSize(3, 4),
                Point(6, 5) to WidgetPreviewContainerSize(3, 4),
            )

        private fun getTestSizes(dp: DeviceProfile) =
            if (dp.isTablet && !dp.isTwoPanels) {
                TABLET_TEST_SIZES
            } else {
                HANDHELD_TEST_SIZES
            }

        private fun createWidgetItem(
            widgetSize: Point,
            context: Context,
            invariantDeviceProfile: InvariantDeviceProfile,
            iconCache: IconCache
        ): WidgetItem {
            val providerInfo =
                createAppWidgetProviderInfo(
                    ComponentName.createRelative(
                        TEST_PACKAGE,
                        /*cls=*/ ".WidgetProvider_" + widgetSize.x + "x" + widgetSize.y
                    )
                )
            val widgetInfo =
                LauncherAppWidgetProviderInfo.fromProviderInfo(context, providerInfo).apply {
                    spanX = widgetSize.x
                    spanY = widgetSize.y
                }
            return WidgetItem(widgetInfo, invariantDeviceProfile, iconCache, context)
        }
    }
}