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

Commit 00aff95a authored by Sebastian Franco's avatar Sebastian Franco
Browse files

Give the tests the ability to emulate other devices screens

This code contains utility clases that can change the display
of a device and make it look like another device.
The function DisplayEmulator#emulate receives a DeviceEmulationData
a certain grid to emulate and a callback, everyting that happens
inside of the callback will happen when the device is being emulated
and can be used by other tests.

Example test:
package com.android.launcher3.deviceemulator;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import com.android.launcher3.deviceemulator.models.DeviceEmulationData;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeUnit;

@MediumTest
@RunWith(AndroidJUnit4.class)
public class TestTest extends AbstractLauncherUiTest {
    @Test
    public void testEmulation() throws Exception {
        String deviceCode = "pixel6pro";
        DeviceEmulationData deviceData = DeviceEmulationData.getDevice(deviceCode);
        String grid = "normal";
        DisplayEmulator displayEmulator = new DisplayEmulator(mTargetContext);

        displayEmulator.emulate(deviceData, grid, () ->{
            TimeUnit.SECONDS.sleep(10);
            return true;
        });
    }
}

Test: You could use the test above to make your device look like a
Pixel6 pro for 10 secons.
Fix: 229028257

Change-Id: Icd79be405a2e14dda0bc5f555b0e46149e16f912
parent 795a9a8c
Loading
Loading
Loading
Loading
+7 −3
Original line number Diff line number Diff line
@@ -242,7 +242,9 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable {
            change |= CHANGE_SUPPORTED_BOUNDS;

            Point currentS = newInfo.currentSize;
            Point expectedS = oldInfo.mPerDisplayBounds.get(newInfo.displayId).first.size;
            Pair<CachedDisplayInfo, WindowBounds[]> cachedBounds =
                    oldInfo.mPerDisplayBounds.get(newInfo.displayId);
            Point expectedS = cachedBounds == null ? null : cachedBounds.first.size;
            if (newInfo.supportedBounds.size() != oldInfo.supportedBounds.size()) {
                Log.e("b/198965093",
                        "Inconsistent number of displays"
@@ -250,10 +252,12 @@ public class DisplayController implements ComponentCallbacks, SafeCloseable {
                                + "\noldInfo.supportedBounds: " + oldInfo.supportedBounds
                                + "\nnewInfo.supportedBounds: " + newInfo.supportedBounds);
            }
            if ((Math.min(currentS.x, currentS.y) != Math.min(expectedS.x, expectedS.y)
            if (expectedS != null
                    && (Math.min(currentS.x, currentS.y) != Math.min(expectedS.x, expectedS.y)
                    || Math.max(currentS.x, currentS.y) != Math.max(expectedS.x, expectedS.y))
                    && display.getState() == Display.STATE_OFF) {
                Log.e("b/198965093", "Display size changed while display is off, ignoring change");
                Log.e("b/198965093",
                        "Display size changed while display is off, ignoring change");
                return;
            }
        }
+14 −8
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import static com.android.launcher3.ResourceUtils.INVALID_RESOURCE_HANDLE;
import static com.android.launcher3.ResourceUtils.NAVBAR_HEIGHT;
import static com.android.launcher3.ResourceUtils.NAVBAR_HEIGHT_LANDSCAPE;
import static com.android.launcher3.ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE;
import static com.android.launcher3.ResourceUtils.getDimenByName;
import static com.android.launcher3.Utilities.dpiFromPx;
import static com.android.launcher3.util.MainThreadInitializedObject.forOverride;
import static com.android.launcher3.util.RotationUtils.deltaRotation;
@@ -157,16 +156,16 @@ public class WindowManagerProxy implements ResourceBasedOverride {
        int bottomNav = isTablet
                ? 0
                : (config.screenHeightDp > config.screenWidthDp
                        ? getDimenByName(NAVBAR_HEIGHT, systemRes, 0)
                        ? getDimenByName(NAVBAR_HEIGHT, systemRes)
                        : (isGesture
                                ? getDimenByName(NAVBAR_HEIGHT_LANDSCAPE, systemRes, 0)
                                ? getDimenByName(NAVBAR_HEIGHT_LANDSCAPE, systemRes)
                                : 0));
        Insets newNavInsets = Insets.of(navInsets.left, navInsets.top, navInsets.right, bottomNav);
        insetsBuilder.setInsets(WindowInsets.Type.navigationBars(), newNavInsets);
        insetsBuilder.setInsetsIgnoringVisibility(WindowInsets.Type.navigationBars(), newNavInsets);

        Insets statusBarInsets = oldInsets.getInsets(WindowInsets.Type.statusBars());
        int statusBarHeight = getDimenByName("status_bar_height", systemRes, 0);
        int statusBarHeight = getDimenByName("status_bar_height", systemRes);
        Insets newStatusBarInsets = Insets.of(
                statusBarInsets.left,
                Math.max(statusBarInsets.top, statusBarHeight),
@@ -222,23 +221,23 @@ public class WindowManagerProxy implements ResourceBasedOverride {
        boolean isTabletOrGesture = isTablet
                || (Utilities.ATLEAST_R && isGestureNav(context));

        int statusBarHeight = getDimenByName("status_bar_height", systemRes, 0);
        int statusBarHeight = getDimenByName("status_bar_height", systemRes);

        int navBarHeightPortrait, navBarHeightLandscape, navbarWidthLandscape;

        navBarHeightPortrait = isTablet
                ? (mTaskbarDrawnInProcess
                        ? 0 : systemRes.getDimensionPixelSize(R.dimen.taskbar_size))
                : getDimenByName(NAVBAR_HEIGHT, systemRes, 0);
                : getDimenByName(NAVBAR_HEIGHT, systemRes);

        navBarHeightLandscape = isTablet
                ? (mTaskbarDrawnInProcess
                        ? 0 : systemRes.getDimensionPixelSize(R.dimen.taskbar_size))
                : (isTabletOrGesture
                        ? getDimenByName(NAVBAR_HEIGHT_LANDSCAPE, systemRes, 0) : 0);
                        ? getDimenByName(NAVBAR_HEIGHT_LANDSCAPE, systemRes) : 0);
        navbarWidthLandscape = isTabletOrGesture
                ? 0
                : getDimenByName(NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE, systemRes, 0);
                : getDimenByName(NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE, systemRes);

        WindowBounds[] result = new WindowBounds[4];
        Point tempSize = new Point();
@@ -274,6 +273,13 @@ public class WindowManagerProxy implements ResourceBasedOverride {
        return result;
    }

    /**
     * Wrapper around the utility method for easier emulation
     */
    protected int getDimenByName(String resName, Resources res) {
        return ResourceUtils.getDimenByName(resName, res, 0);
    }

    protected boolean isGestureNav(Context context) {
        return ResourceUtils.getIntegerByName("config_navBarInteractionMode",
                context.getResources(), INVALID_RESOURCE_HANDLE) == 2;
+45 −0
Original line number Diff line number Diff line
{
  "pixel6pro": {
    "width": 1440,
    "height": 3120,
    "density": 560,
    "name": "pixel6pro",
    "cutout": "0, 130, 0, 0",
    "grids": [
      "normal",
      "reasonable",
      "practical",
      "big",
      "crazy_big"
    ],
    "resourceOverrides": {
      "status_bar_height": 98,
      "navigation_bar_height_landscape": 56,
      "navigation_bar_height": 56,
      "navigation_bar_width": 56
    }
  },
  "test": {
    "data needs updating": 0
  },
  "pixel5": {
    "width": 1080,
    "height": 2340,
    "density": 440,
    "name": "pixel5",
    "cutout": "0, 136, 0, 0",
    "grids": [
      "normal",
      "reasonable",
      "practical",
      "big",
      "crazy_big"
    ],
    "resourceOverrides": {
      "status_bar_height": 66,
      "navigation_bar_height_landscape": 44,
      "navigation_bar_height": 44,
      "navigation_bar_width": 44
    }
  }
}
+95 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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 com.android.launcher3.deviceemulator;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.UserHandle;
import android.view.Display;
import android.view.IWindowManager;
import android.view.WindowManagerGlobal;

import androidx.test.uiautomator.UiDevice;

import com.android.launcher3.deviceemulator.models.DeviceEmulationData;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.util.window.WindowManagerProxy;

import java.util.concurrent.Callable;


public class DisplayEmulator {
    Context mContext;
    LauncherInstrumentation mLauncher;
    DisplayEmulator(Context context, LauncherInstrumentation launcher) {
        mContext = context;
        mLauncher = launcher;
    }

    /**
     * By changing the WindowManagerProxy we can override the window insets information
     **/
    private IWindowManager changeWindowManagerInstance(DeviceEmulationData deviceData) {
        WindowManagerProxy.INSTANCE.initializeForTesting(
                new TestWindowManagerProxy(mContext, deviceData));
        return WindowManagerGlobal.getWindowManagerService();
    }

    public <T> T emulate(DeviceEmulationData device, String grid, Callable<T> runInEmulation)
            throws Exception {
        WindowManagerProxy original = WindowManagerProxy.INSTANCE.get(mContext);
        // Set up emulation
        final int userId = UserHandle.myUserId();
        WindowManagerProxy.INSTANCE.initializeForTesting(
                new TestWindowManagerProxy(mContext, device));
        IWindowManager wm = changeWindowManagerInstance(device);
        // Change density twice to force display controller to reset its state
        wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, device.density / 2, userId);
        wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, device.density, userId);
        wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, device.width, device.height);
        wm.setForcedDisplayScalingMode(Display.DEFAULT_DISPLAY, 1);

        // Set up grid
        setGrid(grid);
        try {
            return runInEmulation.call();
        } finally {
            // Clear emulation
            WindowManagerProxy.INSTANCE.initializeForTesting(original);
            UiDevice.getInstance(getInstrumentation()).executeShellCommand("cmd window reset");
        }
    }

    private void setGrid(String gridType) {
        // When the grid changes, the desktop arrangement get stored in SQL and we need to wait to
        // make sure there is no SQL operations running and get SQL_BUSY error, that's why we need
        // to call mLauncher.waitForLauncherInitialized();
        mLauncher.waitForLauncherInitialized();
        String testProviderAuthority = mContext.getPackageName() + ".grid_control";
        Uri gridUri = new Uri.Builder()
                .scheme(ContentResolver.SCHEME_CONTENT)
                .authority(testProviderAuthority)
                .appendPath("default_grid")
                .build();
        ContentValues values = new ContentValues();
        values.put("name", gridType);
        mContext.getContentResolver().update(gridUri, values, null, null);
    }
}
+75 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 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 com.android.launcher3.deviceemulator;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.view.Display;
import android.view.WindowInsets;

import com.android.launcher3.deviceemulator.models.DeviceEmulationData;
import com.android.launcher3.util.RotationUtils;
import com.android.launcher3.util.WindowBounds;
import com.android.launcher3.util.window.CachedDisplayInfo;
import com.android.launcher3.util.window.WindowManagerProxy;

public class TestWindowManagerProxy extends WindowManagerProxy {

    private final DeviceEmulationData mDevice;

    public TestWindowManagerProxy(Context context, DeviceEmulationData device) {
        super(true);
        mDevice = device;
    }

    @Override
    public boolean isInternalDisplay(Display display) {
        return display.getDisplayId() == Display.DEFAULT_DISPLAY;
    }

    @Override
    protected int getDimenByName(String resName, Resources res) {
        Integer mock = mDevice.resourceOverrides.get(resName);
        return mock != null ? mock : super.getDimenByName(resName, res);
    }

    @Override
    public CachedDisplayInfo getDisplayInfo(Context context, Display display) {
        int rotation = display.getRotation();
        Point size = new Point(mDevice.width, mDevice.height);
        RotationUtils.rotateSize(size, rotation);
        Rect cutout = new Rect(mDevice.cutout);
        RotationUtils.rotateRect(cutout, rotation);
        return new CachedDisplayInfo(getDisplayId(display), size, rotation, cutout);
    }

    @Override
    public WindowBounds getRealBounds(Context windowContext, Display display,
            CachedDisplayInfo info) {
        return estimateInternalDisplayBounds(windowContext)
                .get(getDisplayId(display)).second[display.getRotation()];
    }

    @Override
    public WindowInsets normalizeWindowInsets(Context context, WindowInsets oldInsets,
            Rect outInsets) {
        outInsets.set(getRealBounds(context, context.getDisplay(),
                getDisplayInfo(context, context.getDisplay())).insets);
        return oldInsets;
    }
}
Loading