/* * Copyright (C) 2023 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 import android.content.Context import android.content.res.Configuration import android.graphics.Point import android.graphics.Rect import android.util.DisplayMetrics import android.view.Surface import androidx.test.core.app.ApplicationProvider import com.android.launcher3.testing.shared.ResourceUtils import com.android.launcher3.util.DisplayController import com.android.launcher3.util.NavigationMode import com.android.launcher3.util.WindowBounds import com.android.launcher3.util.window.CachedDisplayInfo import com.android.launcher3.util.window.WindowManagerProxy import java.io.BufferedReader import java.io.File import java.io.PrintWriter import java.io.StringWriter import kotlin.math.max import kotlin.math.min import org.junit.After import org.junit.Before import org.mockito.ArgumentMatchers import org.mockito.Mockito.mock import org.mockito.Mockito.`when` as whenever /** * This is an abstract class for DeviceProfile tests that create an InvariantDeviceProfile based on * a real device spec. * * For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest] */ abstract class AbstractDeviceProfileTest { protected var context: Context? = null protected open val runningContext: Context = ApplicationProvider.getApplicationContext() private var displayController: DisplayController = mock(DisplayController::class.java) private var windowManagerProxy: WindowManagerProxy = mock(WindowManagerProxy::class.java) private lateinit var originalDisplayController: DisplayController private lateinit var originalWindowManagerProxy: WindowManagerProxy @Before open fun setUp() { val appContext: Context = ApplicationProvider.getApplicationContext() originalWindowManagerProxy = WindowManagerProxy.INSTANCE.get(appContext) originalDisplayController = DisplayController.INSTANCE.get(appContext) WindowManagerProxy.INSTANCE.initializeForTesting(windowManagerProxy) DisplayController.INSTANCE.initializeForTesting(displayController) } @After open fun tearDown() { WindowManagerProxy.INSTANCE.initializeForTesting(originalWindowManagerProxy) DisplayController.INSTANCE.initializeForTesting(originalDisplayController) } class DeviceSpec( val naturalSize: Pair, var densityDpi: Int, val statusBarNaturalPx: Int, val statusBarRotatedPx: Int, val gesturePx: Int, val cutoutPx: Int ) open val deviceSpecs = mapOf( "phone" to DeviceSpec( Pair(1080, 2400), densityDpi = 420, statusBarNaturalPx = 118, statusBarRotatedPx = 74, gesturePx = 63, cutoutPx = 118 ), "tablet" to DeviceSpec( Pair(2560, 1600), densityDpi = 320, statusBarNaturalPx = 104, statusBarRotatedPx = 104, gesturePx = 0, cutoutPx = 0 ), "twopanel-phone" to DeviceSpec( Pair(1080, 2092), densityDpi = 420, statusBarNaturalPx = 133, statusBarRotatedPx = 110, gesturePx = 63, cutoutPx = 133 ), "twopanel-tablet" to DeviceSpec( Pair(2208, 1840), densityDpi = 420, statusBarNaturalPx = 110, statusBarRotatedPx = 133, gesturePx = 0, cutoutPx = 0 ) ) protected fun initializeVarsForPhone( deviceSpec: DeviceSpec, isGestureMode: Boolean = true, isVerticalBar: Boolean = false ) { val (naturalX, naturalY) = deviceSpec.naturalSize val windowsBounds = phoneWindowsBounds(deviceSpec, isGestureMode, naturalX, naturalY) val displayInfo = CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0)) val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds) initializeCommonVars( perDisplayBoundsCache, displayInfo, rotation = if (isVerticalBar) Surface.ROTATION_90 else Surface.ROTATION_0, isGestureMode, densityDpi = deviceSpec.densityDpi ) } protected fun initializeVarsForTablet( deviceSpec: DeviceSpec, isLandscape: Boolean = false, isGestureMode: Boolean = true ) { val (naturalX, naturalY) = deviceSpec.naturalSize val windowsBounds = tabletWindowsBounds(deviceSpec, naturalX, naturalY) val displayInfo = CachedDisplayInfo(Point(naturalX, naturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0)) val perDisplayBoundsCache = mapOf(displayInfo to windowsBounds) initializeCommonVars( perDisplayBoundsCache, displayInfo, rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90, isGestureMode, densityDpi = deviceSpec.densityDpi ) } protected fun initializeVarsForTwoPanel( deviceSpecUnfolded: DeviceSpec, deviceSpecFolded: DeviceSpec, isLandscape: Boolean = false, isGestureMode: Boolean = true, isFolded: Boolean = false ) { val (unfoldedNaturalX, unfoldedNaturalY) = deviceSpecUnfolded.naturalSize val unfoldedWindowsBounds = tabletWindowsBounds(deviceSpecUnfolded, unfoldedNaturalX, unfoldedNaturalY) val unfoldedDisplayInfo = CachedDisplayInfo( Point(unfoldedNaturalX, unfoldedNaturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0) ) val (foldedNaturalX, foldedNaturalY) = deviceSpecFolded.naturalSize val foldedWindowsBounds = phoneWindowsBounds(deviceSpecFolded, isGestureMode, foldedNaturalX, foldedNaturalY) val foldedDisplayInfo = CachedDisplayInfo( Point(foldedNaturalX, foldedNaturalY), Surface.ROTATION_0, Rect(0, 0, 0, 0) ) val perDisplayBoundsCache = mapOf( unfoldedDisplayInfo to unfoldedWindowsBounds, foldedDisplayInfo to foldedWindowsBounds ) if (isFolded) { initializeCommonVars( perDisplayBoundsCache = perDisplayBoundsCache, displayInfo = foldedDisplayInfo, rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0, isGestureMode = isGestureMode, densityDpi = deviceSpecFolded.densityDpi ) } else { initializeCommonVars( perDisplayBoundsCache = perDisplayBoundsCache, displayInfo = unfoldedDisplayInfo, rotation = if (isLandscape) Surface.ROTATION_0 else Surface.ROTATION_90, isGestureMode = isGestureMode, densityDpi = deviceSpecUnfolded.densityDpi ) } } private fun phoneWindowsBounds( deviceSpec: DeviceSpec, isGestureMode: Boolean, naturalX: Int, naturalY: Int ): List { val buttonsNavHeight = Utilities.dpToPx(48f, deviceSpec.densityDpi) val rotation0Insets = Rect( 0, max(deviceSpec.statusBarNaturalPx, deviceSpec.cutoutPx), 0, if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight ) val rotation90Insets = Rect( deviceSpec.cutoutPx, deviceSpec.statusBarRotatedPx, if (isGestureMode) 0 else buttonsNavHeight, if (isGestureMode) deviceSpec.gesturePx else 0 ) val rotation180Insets = Rect( 0, deviceSpec.statusBarNaturalPx, 0, max( if (isGestureMode) deviceSpec.gesturePx else buttonsNavHeight, deviceSpec.cutoutPx ) ) val rotation270Insets = Rect( if (isGestureMode) 0 else buttonsNavHeight, deviceSpec.statusBarRotatedPx, deviceSpec.cutoutPx, if (isGestureMode) deviceSpec.gesturePx else 0 ) return listOf( WindowBounds(Rect(0, 0, naturalX, naturalY), rotation0Insets, Surface.ROTATION_0), WindowBounds(Rect(0, 0, naturalY, naturalX), rotation90Insets, Surface.ROTATION_90), WindowBounds(Rect(0, 0, naturalX, naturalY), rotation180Insets, Surface.ROTATION_180), WindowBounds(Rect(0, 0, naturalY, naturalX), rotation270Insets, Surface.ROTATION_270) ) } private fun tabletWindowsBounds( deviceSpec: DeviceSpec, naturalX: Int, naturalY: Int ): List { val naturalInsets = Rect(0, deviceSpec.statusBarNaturalPx, 0, 0) val rotatedInsets = Rect(0, deviceSpec.statusBarRotatedPx, 0, 0) return listOf( WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_0), WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_90), WindowBounds(Rect(0, 0, naturalX, naturalY), naturalInsets, Surface.ROTATION_180), WindowBounds(Rect(0, 0, naturalY, naturalX), rotatedInsets, Surface.ROTATION_270) ) } private fun initializeCommonVars( perDisplayBoundsCache: Map>, displayInfo: CachedDisplayInfo, rotation: Int, isGestureMode: Boolean = true, densityDpi: Int ) { val windowsBounds = perDisplayBoundsCache[displayInfo]!! val realBounds = windowsBounds[rotation] whenever(windowManagerProxy.getDisplayInfo(ArgumentMatchers.any())).thenReturn(displayInfo) whenever(windowManagerProxy.getRealBounds(ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(realBounds) whenever(windowManagerProxy.getRotation(ArgumentMatchers.any())).thenReturn(rotation) whenever(windowManagerProxy.getNavigationMode(ArgumentMatchers.any())) .thenReturn( if (isGestureMode) NavigationMode.NO_BUTTON else NavigationMode.THREE_BUTTONS ) val density = densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat() val config = Configuration(runningContext.resources.configuration).apply { this.densityDpi = densityDpi screenWidthDp = (realBounds.bounds.width() / density).toInt() screenHeightDp = (realBounds.bounds.height() / density).toInt() smallestScreenWidthDp = min(screenWidthDp, screenHeightDp) } context = runningContext.createConfigurationContext(config) val info = DisplayController.Info(context, windowManagerProxy, perDisplayBoundsCache) whenever(displayController.info).thenReturn(info) whenever(displayController.isTransientTaskbar).thenReturn(isGestureMode) } /** Create a new dump of DeviceProfile, saves to a file in the device and returns it */ protected fun dump(context: Context, dp: DeviceProfile, fileName: String): String { val stringWriter = StringWriter() PrintWriter(stringWriter).use { dp.dump(context, "", it) } return stringWriter.toString().also { content -> writeToDevice(context, fileName, content) } } /** Read a file from assets/ and return it as a string */ protected fun readDumpFromAssets(context: Context, fileName: String): String = context.assets.open("dumpTests/$fileName").bufferedReader().use(BufferedReader::readText) private fun writeToDevice(context: Context, fileName: String, content: String) { File(context.getDir("dumpTests", Context.MODE_PRIVATE), fileName).writeText(content) } protected fun Float.dpToPx(): Float { return ResourceUtils.pxFromDp(this, context!!.resources.displayMetrics).toFloat() } protected fun Int.dpToPx(): Int { return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics) } }