Merge "Making the Launcher Customizar available to Launcher3 and not only Nexus" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
73dd3d3723
@@ -163,6 +163,7 @@ filegroup {
|
||||
"src/**/*Test.kt",
|
||||
"src/**/RoboApiWrapper.kt",
|
||||
"src/**/EventsRule.kt",
|
||||
"src/**/LauncherCustomizer.kt",
|
||||
"multivalentTests/src/**/*Test.java",
|
||||
"multivalentTests/src/**/*Test.kt",
|
||||
],
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
filegroup {
|
||||
name: "launcher-emulator-util",
|
||||
srcs: [
|
||||
"models/DeviceEmulationData.kt",
|
||||
"models/EmulationParams.kt",
|
||||
"models/EmulatorDisplay.kt",
|
||||
"models/LauncherDeviceList.kt",
|
||||
"DensityPicker.java",
|
||||
"TestWindowManagerProxy.java",
|
||||
],
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.util.launcheremulator;
|
||||
|
||||
import com.android.launcher3.util.launcheremulator.models.DeviceEmulationData;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility class to get the different densities used when going to setting and changing the density
|
||||
* of the display
|
||||
*/
|
||||
public class DensityPicker {
|
||||
|
||||
private static final Density[] SUMMARIES_SMALLER = new Density[] { Density.SMALL };
|
||||
private static final Density[] SUMMARIES_LARGER = new Density[] {
|
||||
Density.LARGE, Density.LARGER, Density.LARGEST};
|
||||
|
||||
/**
|
||||
* Defines the available densities to pick from
|
||||
*/
|
||||
public enum Density {
|
||||
SMALL, NORMAL, LARGE, LARGER, LARGEST;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name().toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return returns a map defining the different density for a given device, the map entries are
|
||||
* defined in {@code Densities}
|
||||
*/
|
||||
public static Map<Density, Integer> getDisplayEntries(DeviceEmulationData device) {
|
||||
// Display logic copied from
|
||||
// packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java
|
||||
// Compute number of "larger" and "smaller" scales for this display.
|
||||
final int minDimensionPx = Math.min(device.width, device.height);
|
||||
final int maxDensity = minDimensionPx / 2;
|
||||
final float maxScale = Math.min(device.densityMaxScale,
|
||||
maxDensity / (float) device.density);
|
||||
final float minScale = device.densityMinScale;
|
||||
final float minScaleInterval = device.densityMinScaleInterval;
|
||||
final int defaultDensity = device.density;
|
||||
|
||||
final int numLarger = (int) Math.max(0, Math.min((maxScale - 1) / minScaleInterval,
|
||||
SUMMARIES_LARGER.length));
|
||||
final int numSmaller = (int) Math.max(0, Math.min((1 - minScale) / minScaleInterval,
|
||||
SUMMARIES_SMALLER.length));
|
||||
|
||||
Map<Density, Integer> displayEntries = new HashMap<>();
|
||||
displayEntries.put(Density.NORMAL, defaultDensity);
|
||||
if (numSmaller > 0) {
|
||||
final float interval = (1 - minScale) / numSmaller;
|
||||
for (int i = numSmaller - 1; i >= 0; i--) {
|
||||
// Round down to a multiple of 2 by truncating the low bit.
|
||||
final int density = ((int) (defaultDensity * (1 - (i + 1) * interval))) & ~1;
|
||||
displayEntries.put(SUMMARIES_SMALLER[i], density);
|
||||
}
|
||||
}
|
||||
|
||||
if (numLarger > 0) {
|
||||
final float interval = (maxScale - 1) / numLarger;
|
||||
for (int i = 0; i < numLarger; i++) {
|
||||
// Round down to a multiple of 2 by truncating the low bit.
|
||||
final int density = ((int) (defaultDensity * (1 + (i + 1) * interval))) & ~1;
|
||||
displayEntries.put(SUMMARIES_LARGER[i], density);
|
||||
}
|
||||
}
|
||||
return displayEntries;
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.util.launcheremulator;
|
||||
|
||||
import static android.view.Surface.ROTATION_0;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.Rect;
|
||||
import android.util.ArrayMap;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import com.android.launcher3.util.DisplayController.Info;
|
||||
import com.android.launcher3.util.WindowBounds;
|
||||
import com.android.launcher3.util.launcheremulator.models.DeviceEmulationData;
|
||||
import com.android.launcher3.util.window.CachedDisplayInfo;
|
||||
import com.android.launcher3.util.window.WindowManagerProxy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Testing class that overrides some values of the Launcher in order to be able to emulate it.
|
||||
* This class overrides the singleton of {@code WindowManagerProxy}.
|
||||
*/
|
||||
public class TestWindowManagerProxy extends WindowManagerProxy {
|
||||
|
||||
private DeviceEmulationData mDevice;
|
||||
|
||||
private boolean mIsInDesktopMode;
|
||||
|
||||
/**
|
||||
* Constructor to be used when initiating using xml overrides.
|
||||
*
|
||||
* Use a new DisplayController.Info object to avoid circular dependency when initiating
|
||||
* DisplayController
|
||||
*/
|
||||
public TestWindowManagerProxy(Context context) {
|
||||
this(DeviceEmulationData.Companion.getCurrentDeviceData(context, new Info(context)));
|
||||
}
|
||||
|
||||
public TestWindowManagerProxy(DeviceEmulationData device) {
|
||||
super(true);
|
||||
mDevice = device;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDimenByName(Resources res, String resName) {
|
||||
Integer mock = mDevice.resourceOverrides.get(resName);
|
||||
return mock != null ? mock : super.getDimenByName(res, resName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDimenByName(Resources res, String resName, String fallback) {
|
||||
return getDimenByName(res, resName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CachedDisplayInfo getDisplayInfo(Context displayInfoContext) {
|
||||
return mDevice.toCachedDisplayInfo(getRotation(displayInfoContext));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowBounds getRealBounds(Context displayInfoContext, CachedDisplayInfo info) {
|
||||
List<WindowBounds> windowBounds = estimateInternalDisplayBounds(displayInfoContext).get(
|
||||
getDisplayInfo(displayInfoContext).normalize(this));
|
||||
return windowBounds.get(getDisplay(displayInfoContext).getRotation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets normalizeWindowInsets(Context context, WindowInsets oldInsets,
|
||||
Rect outInsets) {
|
||||
|
||||
boolean isGesture = isGestureNav(context);
|
||||
outInsets.set(getRealBounds(context, getDisplayInfo(context)).insets);
|
||||
|
||||
WindowInsets.Builder insetsBuilder = new WindowInsets.Builder(oldInsets);
|
||||
|
||||
// This is the same implementation used in WindowManagerProxy to prevent the taskbar
|
||||
// size to be count in the inset. It overrides the tappable bottom inset to be 0
|
||||
// for gesture nav (otherwise taskbar would count towards it).
|
||||
// This is used for the bottom protection in All Apps for example.
|
||||
if (isGesture) {
|
||||
Insets oldTappableInsets = oldInsets.getInsets(WindowInsets.Type.tappableElement());
|
||||
Insets newTappableInsets = Insets.of(oldTappableInsets.left, oldTappableInsets.top,
|
||||
oldTappableInsets.right, 0);
|
||||
insetsBuilder.setInsets(WindowInsets.Type.tappableElement(), newTappableInsets);
|
||||
}
|
||||
|
||||
return insetsBuilder.build();
|
||||
}
|
||||
|
||||
protected CachedDisplayInfo getSecondaryDisplayInfo(int rotation) {
|
||||
return mDevice.secondDisplay.toCachedDisplayInfo(rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map of normalized info of internal displays to estimated window bounds
|
||||
* for that display
|
||||
*/
|
||||
@Override
|
||||
public ArrayMap<CachedDisplayInfo, List<WindowBounds>> estimateInternalDisplayBounds(
|
||||
Context displayInfoContext) {
|
||||
ArrayMap<CachedDisplayInfo, List<WindowBounds>> result =
|
||||
super.estimateInternalDisplayBounds(displayInfoContext);
|
||||
if (mDevice.secondDisplay == null) {
|
||||
return result;
|
||||
}
|
||||
CachedDisplayInfo info = getSecondaryDisplayInfo(ROTATION_0).normalize(this);
|
||||
result.put(info, estimateWindowBounds(displayInfoContext, info));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInDesktopMode(int displayId) {
|
||||
return mIsInDesktopMode;
|
||||
}
|
||||
|
||||
public void setInDesktopMode(boolean isInDesktopMode) {
|
||||
mIsInDesktopMode = isInDesktopMode;
|
||||
}
|
||||
|
||||
public void setDevice(DeviceEmulationData device) {
|
||||
mDevice = device;
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.util.launcheremulator.models
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.ArrayMap
|
||||
import com.android.launcher3.InvariantDeviceProfile
|
||||
import com.android.launcher3.InvariantDeviceProfile.DeviceType
|
||||
import com.android.launcher3.testing.shared.ResourceUtils
|
||||
import com.android.launcher3.util.DisplayController
|
||||
import com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH
|
||||
import java.util.Locale
|
||||
import kotlin.math.min
|
||||
|
||||
/** Model class that holds the data needed to emulate a device display. */
|
||||
class DeviceEmulationData(
|
||||
val name: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
density: Int,
|
||||
@JvmField val densityMaxScale: Float,
|
||||
@JvmField val densityMinScale: Float,
|
||||
@JvmField val densityMinScaleInterval: Float,
|
||||
cutout: Rect,
|
||||
var defaultGrid: String,
|
||||
@JvmField val resourceOverrides: Map<String, Int>,
|
||||
@JvmField val secondDisplay: EmulatorDisplay?,
|
||||
val supportsFixedLandscape: Boolean,
|
||||
) : EmulatorDisplay(width, height, density, cutout) {
|
||||
|
||||
override fun toString(): String {
|
||||
val secondDisplayString =
|
||||
secondDisplay?.let { "EmulatorDisplay(${it.propString("\n\t\t")})" } ?: "null"
|
||||
val resourcesString =
|
||||
resourceOverrides.entries.joinToString { "\n\t\t\"${it.key}\" to ${it.value}" }
|
||||
return "DeviceEmulationData(" +
|
||||
"\n\tname = \"$name\"," +
|
||||
propString("\n\t") +
|
||||
"," +
|
||||
"\n\tdensityMaxScale = ${densityMaxScale}f," +
|
||||
"\n\tdensityMinScale = ${densityMinScale}f," +
|
||||
"\n\tdensityMinScaleInterval = ${densityMinScaleInterval}f," +
|
||||
"\n\tdefaultGrid = \"${defaultGrid}\"," +
|
||||
"\n\tsecondDisplay = $secondDisplayString," +
|
||||
"\n\tresourceOverrides = mapOf($resourcesString)" +
|
||||
"\n)"
|
||||
}
|
||||
|
||||
/** Returns if the device is in tablet dimension */
|
||||
fun isTablet() = (min(width, height).toFloat() / (density / 160)) > MIN_TABLET_WIDTH
|
||||
|
||||
companion object {
|
||||
private const val DENSITY_MIN_SCALE = 0.85f
|
||||
private const val DENSITY_MAX_SCALE = 1.5f
|
||||
private const val DENSITY_SCALE_INTERVAL = 0.09f
|
||||
private val EMULATED_SYSTEM_RESOURCES =
|
||||
arrayOf(
|
||||
ResourceUtils.NAVBAR_HEIGHT,
|
||||
ResourceUtils.NAVBAR_HEIGHT_LANDSCAPE,
|
||||
ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE,
|
||||
ResourceUtils.STATUS_BAR_HEIGHT,
|
||||
ResourceUtils.STATUS_BAR_HEIGHT_LANDSCAPE,
|
||||
ResourceUtils.STATUS_BAR_HEIGHT_PORTRAIT,
|
||||
)
|
||||
|
||||
const val FIXED_LANDSCAPE_GRID = "fixed_landscape_mode"
|
||||
|
||||
private fun getFraction(c: Context, resName: String, fallback: Float): Float {
|
||||
val resId = c.resources.getIdentifier(resName, "fraction", c.packageName)
|
||||
return if (resId != Resources.ID_NULL) c.resources.getFraction(resId, 1, 1)
|
||||
else fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a `DeviceEmulationData` representing the current device.
|
||||
*
|
||||
* @param context Launcher context.
|
||||
* @return `DeviceEmulationData`
|
||||
*/
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun getCurrentDeviceData(
|
||||
context: Context,
|
||||
info: DisplayController.Info = DisplayController.INSTANCE[context].info,
|
||||
): DeviceEmulationData {
|
||||
val code = Build.MODEL.replace("\\s".toRegex(), "").lowercase(Locale.getDefault())
|
||||
val resourceOverrides: MutableMap<String, Int> = ArrayMap()
|
||||
for (s in EMULATED_SYSTEM_RESOURCES) {
|
||||
resourceOverrides[s] = ResourceUtils.getDimenByName(s, context.resources, 0)
|
||||
}
|
||||
|
||||
// Print overridden resources
|
||||
val settingsCtx =
|
||||
try {
|
||||
context.createPackageContext("com.android.settings", 0)
|
||||
} catch (e: Exception) {
|
||||
context
|
||||
}
|
||||
val displayDensityMaxScale =
|
||||
getFraction(settingsCtx, "display_density_max_scale", DENSITY_MAX_SCALE)
|
||||
val displayDensityMinScale =
|
||||
getFraction(settingsCtx, "display_density_min_scale", DENSITY_MIN_SCALE)
|
||||
val displayDensityMinScaleInterval =
|
||||
getFraction(
|
||||
settingsCtx,
|
||||
"display_density_min_scale_interval",
|
||||
DENSITY_SCALE_INTERVAL,
|
||||
)
|
||||
|
||||
@DeviceType val deviceType = info.deviceType
|
||||
val defaultGrid =
|
||||
InvariantDeviceProfile.parseAllDefinedGridOptions(context, info)
|
||||
.stream()
|
||||
.filter { it.isEnabled(deviceType) }
|
||||
.map { it.name }
|
||||
.findFirst()
|
||||
.get()
|
||||
|
||||
return DeviceEmulationData(
|
||||
name = code,
|
||||
width = info.currentSize.x,
|
||||
height = info.currentSize.y,
|
||||
density = info.densityDpi,
|
||||
densityMaxScale = displayDensityMaxScale,
|
||||
densityMinScale = displayDensityMinScale,
|
||||
densityMinScaleInterval = displayDensityMinScaleInterval,
|
||||
cutout = info.cutout,
|
||||
defaultGrid = defaultGrid,
|
||||
resourceOverrides = resourceOverrides,
|
||||
secondDisplay = null,
|
||||
supportsFixedLandscape = deviceType == InvariantDeviceProfile.TYPE_PHONE,
|
||||
)
|
||||
}
|
||||
|
||||
/** Returns a stored `DeviceEmulationData` */
|
||||
@JvmStatic
|
||||
fun getDevice(deviceCode: String) =
|
||||
LauncherDeviceList.ALL_DEVICES.find { it.name == deviceCode }!!
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.util.launcheremulator.models
|
||||
|
||||
import com.android.launcher3.util.launcheremulator.DensityPicker.Density
|
||||
import com.android.launcher3.util.launcheremulator.models.DeviceEmulationData.Companion.FIXED_LANDSCAPE_GRID
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParamsUtils.getDensityShort
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParamsUtils.getDeviceShort
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParamsUtils.getFontScaleShort
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParamsUtils.getGridShort
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParamsUtils.getOrientationShort
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParamsUtils.getRtlShort
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParamsUtils.isDarkThemeShort
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParamsUtils.isUsingThemeIconsShort
|
||||
import com.android.launcher3.util.launcheremulator.models.LauncherOrientation.LANDSCAPE
|
||||
import com.android.launcher3.util.launcheremulator.models.LauncherOrientation.PORTRAIT
|
||||
import com.android.launcher3.util.launcheremulator.models.LauncherOrientation.SEASCAPE
|
||||
import java.util.Locale
|
||||
|
||||
/** Class to hold all parameters used for device and launcher config emulation */
|
||||
data class EmulationParams
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
val device: DeviceEmulationData,
|
||||
val density: Density = Density.NORMAL,
|
||||
val grid: String = device.defaultGrid,
|
||||
val orientation: LauncherOrientation = PORTRAIT,
|
||||
val fontScale: FontScale = FontScale.DEFAULT,
|
||||
val isRtl: Boolean = false,
|
||||
val isDarkTheme: Boolean = false,
|
||||
val isUsingThemeIcons: Boolean = false,
|
||||
) {
|
||||
|
||||
init {
|
||||
validateParameters()
|
||||
}
|
||||
|
||||
val deviceType: DeviceType =
|
||||
when {
|
||||
device.secondDisplay != null -> DeviceType.Foldable
|
||||
device.isTablet() -> DeviceType.Tablet
|
||||
else -> DeviceType.Phone
|
||||
}
|
||||
|
||||
val isFixedLandscape = grid == FIXED_LANDSCAPE_GRID && orientation == LANDSCAPE
|
||||
|
||||
override fun toString(): String =
|
||||
listOf(
|
||||
getDeviceShort(device),
|
||||
getGridShort(grid),
|
||||
getDensityShort(density),
|
||||
getOrientationShort(orientation),
|
||||
getFontScaleShort(fontScale),
|
||||
getRtlShort(isRtl),
|
||||
isDarkThemeShort(isDarkTheme),
|
||||
isUsingThemeIconsShort(isUsingThemeIcons),
|
||||
)
|
||||
.filter { it.isNotBlank() }
|
||||
.joinToString("_")
|
||||
|
||||
enum class DeviceType {
|
||||
Phone,
|
||||
Tablet,
|
||||
Foldable,
|
||||
}
|
||||
|
||||
private fun validateParameters() {
|
||||
if (isFixedLandscape && !device.supportsFixedLandscape) {
|
||||
throw Exception(
|
||||
"Device ${device.name} doesn't support fixed landscape, update " +
|
||||
"EmulationParams or update the DeviceList.kt"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object EmulationParamsUtils {
|
||||
|
||||
fun getDeviceShort(device: DeviceEmulationData): String =
|
||||
when (device.name) {
|
||||
"pixel5" -> "p5"
|
||||
"pixel6" -> "p6"
|
||||
"pixel6pro" -> "p6pro"
|
||||
"pixel7pro" -> "p7pro"
|
||||
"pixelFoldable2023" -> "fold"
|
||||
"pixelFoldable2023_frontDisplay" -> "foldFront"
|
||||
"pixelTablet2023" -> "tab"
|
||||
else -> device.name
|
||||
}
|
||||
|
||||
fun getGridShort(grid: String) =
|
||||
when (grid) {
|
||||
"normal" -> "5x5"
|
||||
"practical" -> "4x5"
|
||||
"reasonable" -> "4x4"
|
||||
"big" -> "3x3"
|
||||
"crazy_big" -> "2x2"
|
||||
"tablet_normal" -> "6x5"
|
||||
else -> grid
|
||||
}
|
||||
|
||||
fun getDensityShort(density: Density) =
|
||||
"den" +
|
||||
when (density) {
|
||||
Density.SMALL -> "S"
|
||||
Density.NORMAL -> "N"
|
||||
Density.LARGE -> "L"
|
||||
Density.LARGER -> "L2"
|
||||
Density.LARGEST -> "L3"
|
||||
}
|
||||
|
||||
fun getOrientationShort(orientation: LauncherOrientation) =
|
||||
when (orientation) {
|
||||
PORTRAIT -> "port"
|
||||
LANDSCAPE -> "land"
|
||||
SEASCAPE -> "seas"
|
||||
}
|
||||
|
||||
fun getFontScaleShort(fontScale: FontScale) =
|
||||
if (fontScale != FontScale.DEFAULT) "font${fontScale.value}" else ""
|
||||
|
||||
fun getRtlShort(isRtl: Boolean) = if (isRtl) "rtl" else ""
|
||||
|
||||
fun isDarkThemeShort(isDarkTheme: Boolean) = if (isDarkTheme) "dark" else ""
|
||||
|
||||
fun isUsingThemeIconsShort(isUsingThemeIcons: Boolean) =
|
||||
if (isUsingThemeIcons) "thIcons" else ""
|
||||
}
|
||||
|
||||
enum class LauncherOrientation {
|
||||
PORTRAIT,
|
||||
LANDSCAPE,
|
||||
SEASCAPE,
|
||||
}
|
||||
|
||||
enum class FontScale(val value: Float) {
|
||||
SMALL(0.85f),
|
||||
DEFAULT(1f),
|
||||
LARGEST(2f);
|
||||
|
||||
override fun toString() = name.lowercase(Locale.getDefault())
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.util.launcheremulator.models
|
||||
|
||||
import android.graphics.Insets
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.view.DisplayCutout
|
||||
import com.android.launcher3.util.RotationUtils
|
||||
import com.android.launcher3.util.window.CachedDisplayInfo
|
||||
|
||||
/** Represents the model for the display in DeviceList */
|
||||
open class EmulatorDisplay(
|
||||
@JvmField val width: Int,
|
||||
@JvmField val height: Int,
|
||||
@JvmField val density: Int,
|
||||
val cutout: Rect,
|
||||
) {
|
||||
|
||||
fun propString(prefix: String) =
|
||||
"${prefix}width = $width,${prefix}height = $height,${prefix}density = $density,${prefix}cutout = Rect(${cutout.left}, ${cutout.top}, ${cutout.right}, ${cutout.bottom})"
|
||||
|
||||
/**
|
||||
* Returns a CachedDisplayInfo rotated to the given rotation, representing the current emulation
|
||||
*/
|
||||
fun toCachedDisplayInfo(rotation: Int): CachedDisplayInfo {
|
||||
val size = Point(width, height)
|
||||
RotationUtils.rotateSize(size, rotation)
|
||||
val cutoutRotated = Rect(cutout)
|
||||
RotationUtils.rotateRect(cutoutRotated, rotation)
|
||||
return CachedDisplayInfo(
|
||||
size,
|
||||
rotation,
|
||||
DisplayCutout(Insets.of(cutoutRotated), null, null, null, null),
|
||||
)
|
||||
}
|
||||
}
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.util.launcheremulator.models
|
||||
|
||||
import android.graphics.Rect
|
||||
|
||||
object LauncherDeviceList {
|
||||
|
||||
val pixel9pro =
|
||||
DeviceEmulationData(
|
||||
name = "pixel9pro",
|
||||
width = 960,
|
||||
height = 2142,
|
||||
density = 360,
|
||||
cutout = Rect(0, 153, 0, 0),
|
||||
densityMaxScale = 1.5f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09000003f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay = null,
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 153,
|
||||
"status_bar_height" to 153,
|
||||
"navigation_bar_height_landscape" to 54,
|
||||
"navigation_bar_height" to 54,
|
||||
"status_bar_height_landscape" to 54,
|
||||
"navigation_bar_width" to 54,
|
||||
),
|
||||
supportsFixedLandscape = false,
|
||||
)
|
||||
|
||||
val pixel9proFold =
|
||||
DeviceEmulationData(
|
||||
name = "pixel9profold",
|
||||
width = 2076,
|
||||
height = 2152,
|
||||
density = 390,
|
||||
cutout = Rect(0, 136, 0, 0),
|
||||
densityMaxScale = 1.3849792f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09000003f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay =
|
||||
EmulatorDisplay(
|
||||
width = 1080,
|
||||
height = 2424,
|
||||
density = 390,
|
||||
cutout = Rect(0, 152, 0, 0),
|
||||
),
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 88,
|
||||
"status_bar_height" to 88,
|
||||
"navigation_bar_height_landscape" to 59,
|
||||
"navigation_bar_height" to 59,
|
||||
"status_bar_height_landscape" to 88,
|
||||
"navigation_bar_width" to 59,
|
||||
),
|
||||
supportsFixedLandscape = false,
|
||||
)
|
||||
|
||||
val pixel9proFold_frontDisplay =
|
||||
DeviceEmulationData(
|
||||
name = "pixel9profold_front",
|
||||
width = 1080,
|
||||
height = 2424,
|
||||
density = 390,
|
||||
cutout = Rect(0, 152, 0, 0),
|
||||
densityMaxScale = 1.3849792f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09000003f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay =
|
||||
EmulatorDisplay(
|
||||
width = 2076,
|
||||
height = 2152,
|
||||
density = 390,
|
||||
cutout = Rect(0, 136, 0, 0),
|
||||
),
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 161,
|
||||
"status_bar_height" to 161,
|
||||
"navigation_bar_height_landscape" to 59,
|
||||
"navigation_bar_height" to 59,
|
||||
"status_bar_height_landscape" to 59,
|
||||
"navigation_bar_width" to 59,
|
||||
),
|
||||
supportsFixedLandscape = false,
|
||||
)
|
||||
|
||||
val pixel8proHD =
|
||||
DeviceEmulationData(
|
||||
name = "p8pro_qhd",
|
||||
width = 1344,
|
||||
height = 2992,
|
||||
density = 480,
|
||||
cutout = Rect(0, 151, 0, 0),
|
||||
densityMaxScale = 1.5f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay = null,
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 151,
|
||||
"status_bar_height" to 151,
|
||||
"navigation_bar_height_landscape" to 72,
|
||||
"navigation_bar_height" to 72,
|
||||
"status_bar_height_landscape" to 84,
|
||||
"navigation_bar_width" to 72,
|
||||
),
|
||||
supportsFixedLandscape = true,
|
||||
)
|
||||
|
||||
val pixel8proFHD =
|
||||
DeviceEmulationData(
|
||||
name = "p8pro_fhd",
|
||||
width = 1008,
|
||||
height = 2244,
|
||||
density = 360,
|
||||
cutout = Rect(0, 113, 0, 0),
|
||||
densityMaxScale = 1.5f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay = null,
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 151,
|
||||
"status_bar_height" to 151,
|
||||
"navigation_bar_height_landscape" to 54,
|
||||
"navigation_bar_height" to 54,
|
||||
"status_bar_height_landscape" to 63,
|
||||
"navigation_bar_width" to 54,
|
||||
),
|
||||
supportsFixedLandscape = true,
|
||||
)
|
||||
|
||||
val pixel8 =
|
||||
DeviceEmulationData(
|
||||
name = "p8",
|
||||
width = 1080,
|
||||
height = 2400,
|
||||
density = 420,
|
||||
cutout = Rect(0, 132, 0, 0),
|
||||
densityMaxScale = 1.5f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay = null,
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 132,
|
||||
"status_bar_height" to 132,
|
||||
"navigation_bar_height_landscape" to 63,
|
||||
"navigation_bar_height" to 63,
|
||||
"status_bar_height_landscape" to 74,
|
||||
"navigation_bar_width" to 63,
|
||||
),
|
||||
supportsFixedLandscape = true,
|
||||
)
|
||||
|
||||
val pixel7pro =
|
||||
DeviceEmulationData(
|
||||
name = "pixel7pro",
|
||||
width = 1080,
|
||||
height = 2340,
|
||||
density = 420,
|
||||
cutout = Rect(0, 98, 0, 0),
|
||||
densityMaxScale = 1.5f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay = null,
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 130,
|
||||
"status_bar_height" to 130,
|
||||
"navigation_bar_height_landscape" to 63,
|
||||
"navigation_bar_height" to 63,
|
||||
"status_bar_height_landscape" to 74,
|
||||
"navigation_bar_width" to 63,
|
||||
),
|
||||
supportsFixedLandscape = true,
|
||||
)
|
||||
|
||||
val pixel6pro =
|
||||
DeviceEmulationData(
|
||||
name = "pixel6pro",
|
||||
width = 1440,
|
||||
height = 3120,
|
||||
density = 560,
|
||||
cutout = Rect(0, 130, 0, 0),
|
||||
densityMaxScale = 1.5f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay = null,
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 130,
|
||||
"status_bar_height" to 130,
|
||||
"navigation_bar_height_landscape" to 84,
|
||||
"navigation_bar_height" to 84,
|
||||
"status_bar_height_landscape" to 98,
|
||||
"navigation_bar_width" to 84,
|
||||
),
|
||||
supportsFixedLandscape = true,
|
||||
)
|
||||
|
||||
val pixel6 =
|
||||
DeviceEmulationData(
|
||||
name = "pixel6",
|
||||
width = 1080,
|
||||
height = 2400,
|
||||
density = 420,
|
||||
cutout = Rect(0, 118, 0, 0),
|
||||
densityMaxScale = 1.5f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay = null,
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 118,
|
||||
"status_bar_height" to 118,
|
||||
"navigation_bar_height_landscape" to 63,
|
||||
"navigation_bar_height" to 63,
|
||||
"status_bar_height_landscape" to 74,
|
||||
"navigation_bar_width" to 63,
|
||||
),
|
||||
supportsFixedLandscape = true,
|
||||
)
|
||||
|
||||
val pixelTablet2023 =
|
||||
DeviceEmulationData(
|
||||
name = "pixelTablet2023",
|
||||
width = 2560,
|
||||
height = 1600,
|
||||
density = 320,
|
||||
cutout = Rect(0, 0, 0, 0),
|
||||
densityMaxScale = 1.3312378f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "tablet_normal",
|
||||
secondDisplay = null,
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 104,
|
||||
"status_bar_height" to 104,
|
||||
"navigation_bar_height_landscape" to 48,
|
||||
"navigation_bar_height" to 48,
|
||||
"status_bar_height_landscape" to 104,
|
||||
"navigation_bar_width" to 48,
|
||||
),
|
||||
supportsFixedLandscape = false,
|
||||
)
|
||||
|
||||
val pixelFold2023 =
|
||||
DeviceEmulationData(
|
||||
name = "pixelFoldable2023",
|
||||
width = 2208,
|
||||
height = 1840,
|
||||
density = 420,
|
||||
cutout = Rect(0, 0, 0, 0),
|
||||
densityMaxScale = 1.1679993f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay =
|
||||
EmulatorDisplay(
|
||||
width = 1080,
|
||||
height = 2092,
|
||||
density = 420,
|
||||
cutout = Rect(0, 133, 0, 0),
|
||||
),
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 110,
|
||||
"status_bar_height" to 110,
|
||||
"navigation_bar_height_landscape" to 63,
|
||||
"navigation_bar_height" to 63,
|
||||
"status_bar_height_landscape" to 110,
|
||||
"navigation_bar_width" to 63,
|
||||
),
|
||||
supportsFixedLandscape = false,
|
||||
)
|
||||
|
||||
val pixelFold2023_frontDisplay =
|
||||
DeviceEmulationData(
|
||||
name = "pixelFoldable2023_frontDisplay",
|
||||
width = 1080,
|
||||
height = 2092,
|
||||
density = 420,
|
||||
cutout = Rect(0, 133, 0, 0),
|
||||
densityMaxScale = 1.1679993f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay =
|
||||
EmulatorDisplay(
|
||||
width = 2208,
|
||||
height = 1840,
|
||||
density = 420,
|
||||
cutout = Rect(0, 0, 0, 0),
|
||||
),
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 133,
|
||||
"status_bar_height" to 133,
|
||||
"navigation_bar_height_landscape" to 63,
|
||||
"navigation_bar_height" to 63,
|
||||
"status_bar_height_landscape" to 110,
|
||||
"navigation_bar_width" to 63,
|
||||
),
|
||||
supportsFixedLandscape = false,
|
||||
)
|
||||
|
||||
val pixel5 =
|
||||
DeviceEmulationData(
|
||||
name = "pixel5",
|
||||
width = 1080,
|
||||
height = 2340,
|
||||
density = 440,
|
||||
cutout = Rect(0, 136, 0, 0),
|
||||
densityMaxScale = 1.5f,
|
||||
densityMinScale = 0.85f,
|
||||
densityMinScaleInterval = 0.09f,
|
||||
defaultGrid = "medium",
|
||||
secondDisplay = null,
|
||||
resourceOverrides =
|
||||
mapOf(
|
||||
"status_bar_height_portrait" to 136,
|
||||
"status_bar_height" to 136,
|
||||
"navigation_bar_height_landscape" to 66,
|
||||
"navigation_bar_height" to 66,
|
||||
"status_bar_height_landscape" to 77,
|
||||
"navigation_bar_width" to 66,
|
||||
),
|
||||
supportsFixedLandscape = true,
|
||||
)
|
||||
|
||||
// This is the list of devices that are currently under development, we keep the other list in
|
||||
// case special tests are needed for other devices
|
||||
val CURRENT_DEVICES =
|
||||
listOf(
|
||||
pixel9pro,
|
||||
pixel9proFold,
|
||||
pixel9proFold_frontDisplay,
|
||||
pixel8proHD,
|
||||
pixel8proFHD,
|
||||
pixel8,
|
||||
pixel7pro,
|
||||
pixelTablet2023,
|
||||
pixelFold2023,
|
||||
pixelFold2023_frontDisplay,
|
||||
)
|
||||
|
||||
val ALL_DEVICES =
|
||||
listOf(
|
||||
pixel9pro,
|
||||
pixel9proFold,
|
||||
pixel9proFold_frontDisplay,
|
||||
pixel8proHD,
|
||||
pixel8proFHD,
|
||||
pixel8,
|
||||
pixel7pro,
|
||||
pixel6pro,
|
||||
pixelTablet2023,
|
||||
pixelFold2023,
|
||||
pixelFold2023_frontDisplay,
|
||||
pixel6,
|
||||
pixel5,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
filegroup {
|
||||
name: "launcher-device-customizer-util",
|
||||
srcs: [
|
||||
"LauncherCustomizer.kt",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.util.launcheremulator
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri.Builder
|
||||
import android.os.RemoteException
|
||||
import android.os.UserHandle
|
||||
import android.platform.uiautomatorhelpers.DeviceHelpers
|
||||
import android.platform.uiautomatorhelpers.DeviceHelpers.context
|
||||
import android.platform.uiautomatorhelpers.DeviceHelpers.uiDevice
|
||||
import android.provider.Settings.Global
|
||||
import android.provider.Settings.System
|
||||
import android.util.Log
|
||||
import android.view.Display
|
||||
import android.view.Surface
|
||||
import android.view.WindowManagerGlobal
|
||||
import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
|
||||
import com.android.launcher3.Flags
|
||||
import com.android.launcher3.InvariantDeviceProfile
|
||||
import com.android.launcher3.InvariantDeviceProfile.OnIDPChangeListener
|
||||
import com.android.launcher3.LauncherAppState
|
||||
import com.android.launcher3.LauncherPrefs
|
||||
import com.android.launcher3.LauncherPrefs.Companion.get
|
||||
import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
|
||||
import com.android.launcher3.util.DisplayController
|
||||
import com.android.launcher3.util.DisplayController.DisplayInfoChangeListener
|
||||
import com.android.launcher3.util.DisplayController.Info
|
||||
import com.android.launcher3.util.Executors
|
||||
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
|
||||
import com.android.launcher3.util.ModelTestExtensions.clearModelDb
|
||||
import com.android.launcher3.util.TestUtil
|
||||
import com.android.launcher3.util.launcheremulator.DensityPicker.Density
|
||||
import com.android.launcher3.util.launcheremulator.models.DeviceEmulationData
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParams
|
||||
import com.android.launcher3.util.launcheremulator.models.FontScale.DEFAULT
|
||||
import com.android.launcher3.util.launcheremulator.models.LauncherOrientation
|
||||
import com.android.launcher3.util.launcheremulator.models.LauncherOrientation.LANDSCAPE
|
||||
import com.android.launcher3.util.launcheremulator.models.LauncherOrientation.PORTRAIT
|
||||
import com.android.launcher3.util.launcheremulator.models.LauncherOrientation.SEASCAPE
|
||||
import com.android.launcher3.util.window.WindowManagerProxy
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import org.junit.Assert
|
||||
import org.mockito.Mockito.doAnswer
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.mockingDetails
|
||||
import org.mockito.kotlin.reset
|
||||
import org.mockito.kotlin.whenever
|
||||
import org.mockito.stubbing.Answer
|
||||
|
||||
object LauncherCustomizer {
|
||||
private const val TAG = "LauncherCustomizer"
|
||||
private const val RESULT_SUCCESS = 1
|
||||
private const val COL_ICON_THEMED_VALUE = "boolean_value"
|
||||
|
||||
// As specified in com.android.settings.development.RtlLayoutPreferenceController
|
||||
private const val RTL_ON = 1
|
||||
private const val RTL_OFF = 0
|
||||
|
||||
/** Apply all non null customizations starting from device, then grid, font scale and theme */
|
||||
@Throws(Exception::class)
|
||||
fun applyAll(context: Context, params: EmulationParams) {
|
||||
LauncherAppState.getInstance(DeviceHelpers.context).model.clearModelDb()
|
||||
|
||||
System.putFloat(context.contentResolver, System.FONT_SCALE, params.fontScale.value)
|
||||
|
||||
// Equivalent to adb shell settings put global debug.force_rtl 1 (or 0)
|
||||
Global.putInt(
|
||||
context.contentResolver,
|
||||
Global.DEVELOPMENT_FORCE_RTL,
|
||||
if (params.isRtl) RTL_ON else RTL_OFF,
|
||||
)
|
||||
|
||||
emulate(context, params.device, params.density)
|
||||
|
||||
applyFixedLandscape(params.isFixedLandscape)
|
||||
|
||||
applyGridOption(context, params.grid)
|
||||
|
||||
context
|
||||
.getSystemService(UiModeManager::class.java)!!
|
||||
.setApplicationNightMode(
|
||||
if (params.isDarkTheme) UiModeManager.MODE_NIGHT_YES
|
||||
else UiModeManager.MODE_NIGHT_NO
|
||||
)
|
||||
|
||||
applyIsThemed(context, params.isUsingThemeIcons)
|
||||
|
||||
// Flush the main thread so that all the settings are applied
|
||||
TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {}
|
||||
|
||||
if (!isOrientationCorrect(params.orientation) && !params.isFixedLandscape) {
|
||||
Log.d(TAG, "retry orientation: ${params.orientation}")
|
||||
applyOrientation(params.orientation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyFixedLandscape(isFixedLandscape: Boolean) {
|
||||
val idp = InvariantDeviceProfile.INSTANCE[context]
|
||||
get(context).put(LauncherPrefs.ALLOW_ROTATION, !isFixedLandscape)
|
||||
if (idp.isFixedLandscape == isFixedLandscape) return
|
||||
val latch = CountDownLatch(1)
|
||||
val listener = OnIDPChangeListener {
|
||||
if (idp.isFixedLandscape == isFixedLandscape) latch.countDown()
|
||||
}
|
||||
idp.addOnChangeListener(listener)
|
||||
get(context).put(LauncherPrefs.FIXED_LANDSCAPE_MODE, isFixedLandscape)
|
||||
latch.await()
|
||||
idp.removeOnChangeListener(listener)
|
||||
}
|
||||
|
||||
private fun applyGridOption(context: Context, gridParam: String) {
|
||||
var grid = gridParam
|
||||
if (Flags.oneGridSpecs()) {
|
||||
when (gridParam) {
|
||||
"normal" -> grid = "medium"
|
||||
}
|
||||
} else {
|
||||
when (gridParam) {
|
||||
"medium" -> grid = "normal"
|
||||
}
|
||||
}
|
||||
sendGridRequest(context, "default_grid", "name", grid)
|
||||
}
|
||||
|
||||
private fun applyIsThemed(context: Context, isThemed: Boolean) =
|
||||
sendGridRequest(context, "icon_themed", COL_ICON_THEMED_VALUE, isThemed)
|
||||
|
||||
private fun sendGridRequest(context: Context, method: String, arg: String, argValue: Any?) {
|
||||
val testProviderAuthority = context.packageName + ".grid_control"
|
||||
val gridUri =
|
||||
Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(testProviderAuthority)
|
||||
.appendPath(method)
|
||||
.build()
|
||||
val values = ContentValues()
|
||||
values.putObject(arg, argValue)
|
||||
Assert.assertEquals(
|
||||
RESULT_SUCCESS,
|
||||
context.appComponent.gridCustomizationsProxy.update(gridUri, values, null, null),
|
||||
)
|
||||
}
|
||||
|
||||
private fun isOrientationCorrect(orientation: LauncherOrientation): Boolean {
|
||||
return (orientation == PORTRAIT && !isLandscape() && !isSeascape()) ||
|
||||
(orientation == LANDSCAPE && isLandscape()) ||
|
||||
(orientation == SEASCAPE && isSeascape())
|
||||
}
|
||||
|
||||
/** Returns if the device orientation is in landscape (width >= height) and the rotation is 0 */
|
||||
private fun isLandscape(): Boolean {
|
||||
val displayRotation = uiDevice.displayRotation
|
||||
val isLandscape = uiDevice.displayWidth >= uiDevice.displayHeight
|
||||
return isLandscape &&
|
||||
(displayRotation == Surface.ROTATION_0 || displayRotation == Surface.ROTATION_90)
|
||||
}
|
||||
|
||||
/** Returns if the device orientation is in landscape (width >= height) and the rotation is */
|
||||
private fun isSeascape(): Boolean {
|
||||
val displayRotation = uiDevice.displayRotation
|
||||
val isLandscape = uiDevice.displayWidth >= uiDevice.displayHeight
|
||||
return isLandscape && displayRotation == Surface.ROTATION_270
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
private fun applyOrientation(orientation: LauncherOrientation) {
|
||||
if (!isOrientationCorrect(orientation)) {
|
||||
when (orientation) {
|
||||
PORTRAIT -> uiDevice.setOrientationPortrait()
|
||||
LANDSCAPE -> uiDevice.setOrientationLandscape()
|
||||
SEASCAPE -> uiDevice.setOrientationRight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param device data required to emulate a given device display */
|
||||
@Throws(Exception::class)
|
||||
private fun emulate(context: Context, device: DeviceEmulationData, densityScale: Density) {
|
||||
val densities = DensityPicker.getDisplayEntries(device)
|
||||
|
||||
// Set up emulation
|
||||
// Override WindowManagerProxy
|
||||
TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {
|
||||
val wmp = WindowManagerProxy.INSTANCE[context]
|
||||
if (mockingDetails(wmp).isSpy) reset(wmp)
|
||||
|
||||
spyOn(wmp)
|
||||
val wmpOverride = TestWindowManagerProxy(device)
|
||||
val answer = Answer {
|
||||
wmpOverride::class
|
||||
.java
|
||||
.getMethod(it.method.name, *it.method.parameterTypes)
|
||||
.invoke(wmpOverride, *it.arguments)
|
||||
}
|
||||
|
||||
doAnswer(answer).whenever(wmp).isTaskbarDrawnInProcess
|
||||
doAnswer(answer).whenever(wmp).estimateInternalDisplayBounds(any())
|
||||
doAnswer(answer).whenever(wmp).isInDesktopMode(any())
|
||||
doAnswer(answer).whenever(wmp).showLockedTaskbarOnHome(any())
|
||||
doAnswer(answer).whenever(wmp).isHomeVisible()
|
||||
doAnswer(answer).whenever(wmp).getRealBounds(any(), any())
|
||||
doAnswer(answer).whenever(wmp).normalizeWindowInsets(any(), any(), any())
|
||||
doAnswer(answer).whenever(wmp).getDisplayInfo(any())
|
||||
doAnswer(answer).whenever(wmp).getCurrentBounds(any())
|
||||
doAnswer(answer).whenever(wmp).getRotation(any())
|
||||
doAnswer(answer).whenever(wmp).getNavigationMode(any())
|
||||
}
|
||||
|
||||
val userId = UserHandle.myUserId()
|
||||
|
||||
// This is equivalent to calling:
|
||||
// adb shell wm size {device.width}x{device.height} and adb shell wm scale 1 "
|
||||
WindowManagerGlobal.getWindowManagerService()!!.apply {
|
||||
setForcedDisplaySize(Display.DEFAULT_DISPLAY, device.width, device.height)
|
||||
setForcedDisplayScalingMode(Display.DEFAULT_DISPLAY, 1)
|
||||
|
||||
// Change density twice to force display controller to reset its state
|
||||
val targetDensity = densities[densityScale]!!
|
||||
setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, targetDensity / 2, userId)
|
||||
waitForDensityChange(context, targetDensity / 2)
|
||||
|
||||
setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, targetDensity, userId)
|
||||
waitForDensityChange(context, targetDensity)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun waitForDensityChange(context: Context, targetDensity: Int) {
|
||||
val latch = CountDownLatch(1)
|
||||
Executors.MAIN_EXECUTOR.execute {
|
||||
val controller = DisplayController.INSTANCE[context]
|
||||
if (controller.info.densityDpi == targetDensity) {
|
||||
latch.countDown()
|
||||
return@execute
|
||||
}
|
||||
controller.addChangeListener(
|
||||
object : DisplayInfoChangeListener {
|
||||
override fun onDisplayInfoChanged(context: Context, info: Info, flags: Int) {
|
||||
if (info.densityDpi == targetDensity) {
|
||||
// Remove listener asynchronously
|
||||
Executors.MAIN_EXECUTOR.handler.post {
|
||||
controller.removeChangeListener(this)
|
||||
}
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
latch.await()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun stopEmulation() {
|
||||
// Disable themed icon to prevent interfering with future image tests
|
||||
sendGridRequest(context, "icon_themed", COL_ICON_THEMED_VALUE, false)
|
||||
System.putFloat(context.contentResolver, System.FONT_SCALE, DEFAULT.value)
|
||||
Global.putInt(context.contentResolver, Global.DEVELOPMENT_FORCE_RTL, RTL_OFF)
|
||||
applyFixedLandscape(false)
|
||||
|
||||
TestUtil.runOnExecutorSync(MAIN_EXECUTOR) {
|
||||
WindowManagerProxy.INSTANCE[context].let { wmp ->
|
||||
if (mockingDetails(wmp).isSpy) reset(wmp)
|
||||
}
|
||||
}
|
||||
|
||||
WindowManagerGlobal.getWindowManagerService()!!.apply {
|
||||
clearForcedDisplaySize(Display.DEFAULT_DISPLAY)
|
||||
clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, UserHandle.myUserId())
|
||||
}
|
||||
uiDevice.setOrientationNatural()
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.util.launcheremulator
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.hardware.display.DisplayManagerGlobal
|
||||
import android.net.Uri.Builder
|
||||
import android.view.Display
|
||||
import android.view.DisplayInfo
|
||||
import com.android.launcher3.Flags
|
||||
import com.android.launcher3.LauncherPrefs
|
||||
import com.android.launcher3.LauncherPrefs.Companion.get
|
||||
import com.android.launcher3.dagger.LauncherComponentProvider.appComponent
|
||||
import com.android.launcher3.util.DisplayController
|
||||
import com.android.launcher3.util.launcheremulator.models.EmulationParams
|
||||
import com.android.launcher3.util.launcheremulator.models.LauncherOrientation
|
||||
import com.android.launcher3.util.window.WindowManagerProxy
|
||||
import kotlin.math.roundToInt
|
||||
import org.junit.Assert
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.shadow.api.Shadow.extract
|
||||
import org.robolectric.shadows.ShadowDisplayManagerGlobal
|
||||
import org.robolectric.util.ReflectionHelpers
|
||||
|
||||
/** Alternate implementation of LauncherCustomizer for deviceless tests */
|
||||
object LauncherCustomizer {
|
||||
|
||||
private const val RESULT_SUCCESS = 1
|
||||
private const val COL_ICON_THEMED_VALUE = "boolean_value"
|
||||
|
||||
/** Apply all non null customizations starting from device, then grid, font scale and theme */
|
||||
@Throws(Exception::class)
|
||||
@JvmStatic
|
||||
fun applyAll(context: Context, params: EmulationParams) {
|
||||
val device = params.device
|
||||
val proxy = WindowManagerProxy.INSTANCE.get(context)
|
||||
if (proxy is TestWindowManagerProxy) proxy.setDevice(device)
|
||||
|
||||
val density = DensityPicker.getDisplayEntries(device)[params.density]!!
|
||||
val isLandscape = params.orientation != LauncherOrientation.PORTRAIT
|
||||
|
||||
val scaledWidth = (device.width * 160f / density).roundToInt()
|
||||
val scaledHeight = (device.height * 160f / density).roundToInt()
|
||||
val darkMode = if (params.isDarkTheme) "night" else "notnight"
|
||||
val landscape = if (isLandscape) "land" else "port"
|
||||
// Use test pseudolocales *_XA (RTL) and *_XB (LTR)
|
||||
val locale = if (params.isRtl) "en-rXB" else "en"
|
||||
val qualifier =
|
||||
"${locale}-w${scaledWidth}dp-h${scaledHeight}dp-${landscape}-${darkMode}-${density}dpi"
|
||||
RuntimeEnvironment.setQualifiers(qualifier)
|
||||
RuntimeEnvironment.setFontScale(params.fontScale.value)
|
||||
|
||||
// Hack to ensure that the device size exactly matches real devices. Robolectric auto
|
||||
// sets the appWidth to widthDp * density, which can cause rounding errors. Instead we
|
||||
// set those values using reflection
|
||||
val minSize = Math.min(device.width, device.height)
|
||||
val maxSize = Math.max(device.width, device.height)
|
||||
val di = DisplayManagerGlobal.getInstance().getDisplayInfo(Display.DEFAULT_DISPLAY)
|
||||
di.appWidth = if (isLandscape) maxSize else minSize
|
||||
di.appHeight = if (isLandscape) minSize else maxSize
|
||||
di.logicalHeight = di.appHeight
|
||||
di.logicalWidth = di.appWidth
|
||||
di.smallestNominalAppHeight = minSize
|
||||
di.smallestNominalAppWidth = minSize
|
||||
di.largestNominalAppHeight = maxSize
|
||||
di.largestNominalAppWidth = maxSize
|
||||
val dmGlobal: ShadowDisplayManagerGlobal = extract(DisplayManagerGlobal.getInstance())
|
||||
ReflectionHelpers.callInstanceMethod<Void>(
|
||||
dmGlobal,
|
||||
"changeDisplay",
|
||||
ReflectionHelpers.ClassParameter.from(Int::class.java, Display.DEFAULT_DISPLAY),
|
||||
ReflectionHelpers.ClassParameter.from(DisplayInfo::class.java, di),
|
||||
)
|
||||
|
||||
DisplayController.INSTANCE[context].onConfigurationChanged(
|
||||
Configuration(context.resources.configuration)
|
||||
)
|
||||
|
||||
get(context).put(LauncherPrefs.ALLOW_ROTATION, !params.isFixedLandscape)
|
||||
get(context).put(LauncherPrefs.FIXED_LANDSCAPE_MODE, params.isFixedLandscape)
|
||||
|
||||
applyGridOption(context, "default_grid", "name", params.grid)
|
||||
applyGridOption(context, "icon_themed", COL_ICON_THEMED_VALUE, params.isUsingThemeIcons)
|
||||
}
|
||||
|
||||
private fun applyGridOption(context: Context, method: String, arg: String, paramArgValue: Any) {
|
||||
var argValue: Any = paramArgValue
|
||||
if (Flags.oneGridSpecs()) {
|
||||
if (argValue is String && argValue.compareTo("normal") == 0) {
|
||||
argValue = "medium"
|
||||
}
|
||||
} else {
|
||||
if (argValue is String && argValue.compareTo("medium") == 0) {
|
||||
argValue = "normal"
|
||||
}
|
||||
}
|
||||
val gridUri =
|
||||
Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(context.packageName + ".grid_control")
|
||||
.appendPath(method)
|
||||
.build()
|
||||
Assert.assertEquals(
|
||||
RESULT_SUCCESS,
|
||||
context.appComponent.gridCustomizationsProxy.update(
|
||||
gridUri,
|
||||
ContentValues().apply { putObject(arg, argValue) },
|
||||
null,
|
||||
null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(java.lang.Exception::class) fun stopEmulation() {}
|
||||
}
|
||||
Reference in New Issue
Block a user