diff options
Diffstat (limited to 'tests')
238 files changed, 10581 insertions, 2887 deletions
diff --git a/tests/ApkVerityTest/ApkVerityTestApp/feature_split/AndroidManifest.xml b/tests/ApkVerityTest/ApkVerityTestApp/feature_split/AndroidManifest.xml deleted file mode 100644 index 3f1a4f3a26a1..000000000000 --- a/tests/ApkVerityTest/ApkVerityTestApp/feature_split/AndroidManifest.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - * Copyright (C) 2019 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. - --> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.apkverity" - android:isFeatureSplit="true" - split="feature_x"> - <application> - <activity android:name=".feature_x.DummyActivity"/> - </application> -</manifest> diff --git a/tests/ApkVerityTest/ApkVerityTestApp/src/com/android/apkverity/DummyActivity.java b/tests/ApkVerityTest/ApkVerityTestApp/src/com/android/apkverity/DummyActivity.java deleted file mode 100644 index a7bd771400c0..000000000000 --- a/tests/ApkVerityTest/ApkVerityTestApp/src/com/android/apkverity/DummyActivity.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2019 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.apkverity; - -import android.app.Activity; - -/** Placeholder class just to generate some dex */ -public class DummyActivity extends Activity {} diff --git a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java deleted file mode 100644 index 591ffeb39721..000000000000 --- a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Copyright (C) 2019 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.apkverity; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import android.platform.test.annotations.RootPermissionTest; - -import com.android.blockdevicewriter.BlockDeviceWriter; -import com.android.tradefed.device.DeviceNotAvailableException; -import com.android.tradefed.device.ITestDevice; -import com.android.tradefed.log.LogUtil.CLog; -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; -import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; -import com.android.tradefed.util.CommandResult; -import com.android.tradefed.util.CommandStatus; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -/** - * This test makes sure app installs with fs-verity signature, and on-access verification works. - * - * <p>When an app is installed, all or none of the files should have their corresponding .fsv_sig - * signature file. Otherwise, install will fail. - * - * <p>Once installed, file protected by fs-verity is verified by kernel every time a block is loaded - * from disk to memory. The file is immutable by design, enforced by filesystem. - * - * <p>In order to make sure a block of the file is readable only if the underlying block on disk - * stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical - * address against the block device. - * - * <p>Requirements to run this test: - * <ul> - * <li>Device is rootable</li> - * <li>The filesystem supports fs-verity</li> - * <li>The feature flag is enabled</li> - * </ul> - */ -@RootPermissionTest -@RunWith(DeviceJUnit4ClassRunner.class) -public class ApkVerityTest extends BaseHostJUnit4Test { - private static final String TARGET_PACKAGE = "com.android.apkverity"; - - private static final String BASE_APK = "ApkVerityTestApp.apk"; - private static final String BASE_APK_DM = "ApkVerityTestApp.dm"; - private static final String SPLIT_APK = "ApkVerityTestAppSplit.apk"; - private static final String SPLIT_APK_DM = "ApkVerityTestAppSplit.dm"; - - private static final String INSTALLED_BASE_APK = "base.apk"; - private static final String INSTALLED_BASE_DM = "base.dm"; - private static final String INSTALLED_SPLIT_APK = "split_feature_x.apk"; - private static final String INSTALLED_SPLIT_DM = "split_feature_x.dm"; - private static final String INSTALLED_BASE_APK_FSV_SIG = "base.apk.fsv_sig"; - private static final String INSTALLED_BASE_DM_FSV_SIG = "base.dm.fsv_sig"; - private static final String INSTALLED_SPLIT_APK_FSV_SIG = "split_feature_x.apk.fsv_sig"; - private static final String INSTALLED_SPLIT_DM_FSV_SIG = "split_feature_x.dm.fsv_sig"; - - private static final String DAMAGING_EXECUTABLE = "/data/local/tmp/block_device_writer"; - private static final String CERT_PATH = "/data/local/tmp/ApkVerityTestCert.der"; - - /** Only 4K page is supported by fs-verity currently. */ - private static final int FSVERITY_PAGE_SIZE = 4096; - - private ITestDevice mDevice; - private boolean mDmRequireFsVerity; - - @Before - public void setUp() throws DeviceNotAvailableException { - mDevice = getDevice(); - mDmRequireFsVerity = "true".equals( - mDevice.getProperty("pm.dexopt.dm.require_fsverity")); - - expectRemoteCommandToSucceed("cmd file_integrity append-cert " + CERT_PATH); - uninstallPackage(TARGET_PACKAGE); - } - - @After - public void tearDown() throws DeviceNotAvailableException { - expectRemoteCommandToSucceed("cmd file_integrity remove-last-cert"); - uninstallPackage(TARGET_PACKAGE); - } - - @Test - public void testFsverityKernelSupports() throws DeviceNotAvailableException { - ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data"); - expectRemoteCommandToSucceed("test -f /sys/fs/" + mountPoint.type + "/features/verity"); - } - - @Test - public void testInstallBase() throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFileAndSignature(BASE_APK) - .run(); - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_APK_FSV_SIG); - verifyInstalledFilesHaveFsverity(INSTALLED_BASE_APK); - } - - @Test - public void testInstallBaseWithWrongSignature() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFile(BASE_APK) - .addFile(SPLIT_APK_DM + ".fsv_sig", - BASE_APK + ".fsv_sig") - .runExpectingFailure(); - } - - @Test - public void testInstallBaseWithSplit() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFileAndSignature(BASE_APK) - .addFileAndSignature(SPLIT_APK) - .run(); - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_APK_FSV_SIG, - INSTALLED_SPLIT_APK, - INSTALLED_SPLIT_APK_FSV_SIG); - verifyInstalledFilesHaveFsverity( - INSTALLED_BASE_APK, - INSTALLED_SPLIT_APK); - } - - @Test - public void testInstallBaseWithDm() throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFileAndSignature(BASE_APK) - .addFileAndSignature(BASE_APK_DM) - .run(); - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_APK_FSV_SIG, - INSTALLED_BASE_DM, - INSTALLED_BASE_DM_FSV_SIG); - verifyInstalledFilesHaveFsverity( - INSTALLED_BASE_APK, - INSTALLED_BASE_DM); - } - - @Test - public void testInstallEverything() throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFileAndSignature(BASE_APK) - .addFileAndSignature(BASE_APK_DM) - .addFileAndSignature(SPLIT_APK) - .addFileAndSignature(SPLIT_APK_DM) - .run(); - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_APK_FSV_SIG, - INSTALLED_BASE_DM, - INSTALLED_BASE_DM_FSV_SIG, - INSTALLED_SPLIT_APK, - INSTALLED_SPLIT_APK_FSV_SIG, - INSTALLED_SPLIT_DM, - INSTALLED_SPLIT_DM_FSV_SIG); - verifyInstalledFilesHaveFsverity( - INSTALLED_BASE_APK, - INSTALLED_BASE_DM, - INSTALLED_SPLIT_APK, - INSTALLED_SPLIT_DM); - } - - @Test - public void testInstallSplitOnly() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFileAndSignature(BASE_APK) - .run(); - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_APK_FSV_SIG); - - new InstallMultiple() - .inheritFrom(TARGET_PACKAGE) - .addFileAndSignature(SPLIT_APK) - .run(); - - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_APK_FSV_SIG, - INSTALLED_SPLIT_APK, - INSTALLED_SPLIT_APK_FSV_SIG); - verifyInstalledFilesHaveFsverity( - INSTALLED_BASE_APK, - INSTALLED_SPLIT_APK); - } - - @Test - public void testInstallSplitOnlyMissingSignature() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFileAndSignature(BASE_APK) - .run(); - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_APK_FSV_SIG); - - new InstallMultiple() - .inheritFrom(TARGET_PACKAGE) - .addFile(SPLIT_APK) - .runExpectingFailure(); - } - - @Test - public void testInstallSplitOnlyWithoutBaseSignature() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFile(BASE_APK) - .run(); - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - verifyInstalledFiles(INSTALLED_BASE_APK); - - new InstallMultiple() - .inheritFrom(TARGET_PACKAGE) - .addFileAndSignature(SPLIT_APK) - .run(); - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_SPLIT_APK, - INSTALLED_SPLIT_APK_FSV_SIG); - } - - @Test - public void testInstallOnlyDmHasFsvSig() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFile(BASE_APK) - .addFileAndSignature(BASE_APK_DM) - .addFile(SPLIT_APK) - .addFileAndSignature(SPLIT_APK_DM) - .run(); - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_DM, - INSTALLED_BASE_DM_FSV_SIG, - INSTALLED_SPLIT_APK, - INSTALLED_SPLIT_DM, - INSTALLED_SPLIT_DM_FSV_SIG); - verifyInstalledFilesHaveFsverity( - INSTALLED_BASE_DM, - INSTALLED_SPLIT_DM); - } - - @Test - public void testInstallDmWithoutFsvSig_Base() - throws DeviceNotAvailableException, FileNotFoundException { - InstallMultiple installer = new InstallMultiple() - .addFile(BASE_APK) - .addFile(BASE_APK_DM) - .addFile(SPLIT_APK) - .addFileAndSignature(SPLIT_APK_DM); - if (mDmRequireFsVerity) { - installer.runExpectingFailure(); - } else { - installer.run(); - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_DM, - INSTALLED_SPLIT_APK, - INSTALLED_SPLIT_DM, - INSTALLED_SPLIT_DM_FSV_SIG); - verifyInstalledFilesHaveFsverity(INSTALLED_SPLIT_DM); - } - } - - @Test - public void testInstallDmWithoutFsvSig_Split() - throws DeviceNotAvailableException, FileNotFoundException { - InstallMultiple installer = new InstallMultiple() - .addFile(BASE_APK) - .addFileAndSignature(BASE_APK_DM) - .addFile(SPLIT_APK) - .addFile(SPLIT_APK_DM); - if (mDmRequireFsVerity) { - installer.runExpectingFailure(); - } else { - installer.run(); - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_DM, - INSTALLED_BASE_DM_FSV_SIG, - INSTALLED_SPLIT_APK, - INSTALLED_SPLIT_DM); - verifyInstalledFilesHaveFsverity(INSTALLED_BASE_DM); - } - } - - @Test - public void testInstallSomeApkIsMissingFsvSig_Base() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFileAndSignature(BASE_APK) - .addFileAndSignature(BASE_APK_DM) - .addFile(SPLIT_APK) - .addFileAndSignature(SPLIT_APK_DM) - .runExpectingFailure(); - } - - @Test - public void testInstallSomeApkIsMissingFsvSig_Split() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFile(BASE_APK) - .addFileAndSignature(BASE_APK_DM) - .addFileAndSignature(SPLIT_APK) - .addFileAndSignature(SPLIT_APK_DM) - .runExpectingFailure(); - } - - @Test - public void testInstallBaseWithFsvSigThenSplitWithout() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFileAndSignature(BASE_APK) - .run(); - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - verifyInstalledFiles( - INSTALLED_BASE_APK, - INSTALLED_BASE_APK_FSV_SIG); - - new InstallMultiple() - .addFile(SPLIT_APK) - .runExpectingFailure(); - } - - @Test - public void testInstallBaseWithoutFsvSigThenSplitWith() - throws DeviceNotAvailableException, FileNotFoundException { - new InstallMultiple() - .addFile(BASE_APK) - .run(); - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - verifyInstalledFiles(INSTALLED_BASE_APK); - - new InstallMultiple() - .addFileAndSignature(SPLIT_APK) - .runExpectingFailure(); - } - - @Test - public void testFsverityFileIsImmutableAndReadable() throws DeviceNotAvailableException { - new InstallMultiple().addFileAndSignature(BASE_APK).run(); - String apkPath = getApkPath(TARGET_PACKAGE); - - assertNotNull(getDevice().getAppPackageInfo(TARGET_PACKAGE)); - expectRemoteCommandToFail("echo -n '' >> " + apkPath); - expectRemoteCommandToSucceed("cat " + apkPath + " > /dev/null"); - } - - @Test - public void testFsverityFailToReadModifiedBlockAtFront() throws DeviceNotAvailableException { - new InstallMultiple().addFileAndSignature(BASE_APK).run(); - String apkPath = getApkPath(TARGET_PACKAGE); - - long apkSize = getFileSizeInBytes(apkPath); - long offsetFirstByte = 0; - - // The first two pages should be both readable at first. - assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetFirstByte)); - if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) { - assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, - offsetFirstByte + FSVERITY_PAGE_SIZE)); - } - - // Damage the file directly against the block device. - damageFileAgainstBlockDevice(apkPath, offsetFirstByte); - - // Expect actual read from disk to fail but only at damaged page. - BlockDeviceWriter.dropCaches(mDevice); - assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetFirstByte)); - if (apkSize > offsetFirstByte + FSVERITY_PAGE_SIZE) { - long lastByteOfTheSamePage = - offsetFirstByte % FSVERITY_PAGE_SIZE + FSVERITY_PAGE_SIZE - 1; - assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, lastByteOfTheSamePage)); - assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, lastByteOfTheSamePage + 1)); - } - } - - @Test - public void testFsverityFailToReadModifiedBlockAtBack() throws DeviceNotAvailableException { - new InstallMultiple().addFileAndSignature(BASE_APK).run(); - String apkPath = getApkPath(TARGET_PACKAGE); - - long apkSize = getFileSizeInBytes(apkPath); - long offsetOfLastByte = apkSize - 1; - - // The first two pages should be both readable at first. - assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetOfLastByte)); - if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) { - assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, - offsetOfLastByte - FSVERITY_PAGE_SIZE)); - } - - // Damage the file directly against the block device. - damageFileAgainstBlockDevice(apkPath, offsetOfLastByte); - - // Expect actual read from disk to fail but only at damaged page. - BlockDeviceWriter.dropCaches(mDevice); - assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, offsetOfLastByte)); - if (offsetOfLastByte - FSVERITY_PAGE_SIZE > 0) { - long firstByteOfTheSamePage = offsetOfLastByte - offsetOfLastByte % FSVERITY_PAGE_SIZE; - assertFalse(BlockDeviceWriter.canReadByte(mDevice, apkPath, firstByteOfTheSamePage)); - assertTrue(BlockDeviceWriter.canReadByte(mDevice, apkPath, firstByteOfTheSamePage - 1)); - } - } - - private void verifyInstalledFilesHaveFsverity(String... filenames) - throws DeviceNotAvailableException { - // Verify that all files are protected by fs-verity - String apkPath = getApkPath(TARGET_PACKAGE); - String appDir = apkPath.substring(0, apkPath.lastIndexOf("/")); - long kTargetOffset = 0; - for (String basename : filenames) { - String path = appDir + "/" + basename; - damageFileAgainstBlockDevice(path, kTargetOffset); - - // Retry is sometimes needed to pass the test. Package manager may have FD leaks - // (see b/122744005 as example) that prevents the file in question to be evicted - // from filesystem cache. Forcing GC workarounds the problem. - int retry = 5; - for (; retry > 0; retry--) { - BlockDeviceWriter.dropCaches(mDevice); - if (!BlockDeviceWriter.canReadByte(mDevice, path, kTargetOffset)) { - break; - } - try { - String openFiles = expectRemoteCommandToSucceed("lsof " + apkPath); - CLog.d("lsof: " + openFiles); - Thread.sleep(1000); - forceGCOnOpenFilesProcess(getOpenFilesPIDs(openFiles)); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return; - } - } - assertTrue("Read from " + path + " should fail", retry > 0); - } - } - - /** - * This is a helper method that parses the lsof output to get PIDs of process holding FD. - * Here is an example output of lsof. This method extracts the second columns(PID). - * - * Example lsof output: - * COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME - * .example.app 1063 u0_a38 mem REG 253,6 8599 12826 example.apk - * .example.app 1063 u0_a38 99r REG 253,6 8599 12826 example.apk - */ - private Set<String> getOpenFilesPIDs(String lsof) { - Set<String> openFilesPIDs = new HashSet<>(); - String[] lines = lsof.split("\n"); - for (int i = 1; i < lines.length; i++) { - openFilesPIDs.add(lines[i].split("\\s+")[1]); - } - return openFilesPIDs; - } - - /** - * This is a helper method that forces GC on processes given their PIDs. - * That is to execute shell command "kill -10" on PIDs. - */ - private void forceGCOnOpenFilesProcess(Set<String> openFilesPIDs) - throws DeviceNotAvailableException { - for (String openFilePID : openFilesPIDs) { - mDevice.executeShellV2Command("kill -10 " + openFilePID); - } - } - - private void verifyInstalledFiles(String... filenames) throws DeviceNotAvailableException { - String apkPath = getApkPath(TARGET_PACKAGE); - String appDir = apkPath.substring(0, apkPath.lastIndexOf("/")); - // Exclude directories since we only care about files. - HashSet<String> actualFiles = new HashSet<>(Arrays.asList( - expectRemoteCommandToSucceed("ls -p " + appDir + " | grep -v '/'").split("\n"))); - - HashSet<String> expectedFiles = new HashSet<>(Arrays.asList(filenames)); - assertEquals(expectedFiles, actualFiles); - } - - private void damageFileAgainstBlockDevice(String path, long offsetOfTargetingByte) - throws DeviceNotAvailableException { - assertTrue(path.startsWith("/data/")); - ITestDevice.MountPointInfo mountPoint = mDevice.getMountPointInfo("/data"); - ArrayList<String> args = new ArrayList<>(); - args.add(DAMAGING_EXECUTABLE); - if ("f2fs".equals(mountPoint.type)) { - args.add("--use-f2fs-pinning"); - } - args.add(mountPoint.filesystem); - args.add(path); - args.add(Long.toString(offsetOfTargetingByte)); - expectRemoteCommandToSucceed(String.join(" ", args)); - } - - private String getApkPath(String packageName) throws DeviceNotAvailableException { - String line = expectRemoteCommandToSucceed("pm path " + packageName + " | grep base.apk"); - int index = line.trim().indexOf(":"); - assertTrue(index >= 0); - return line.substring(index + 1); - } - - private long getFileSizeInBytes(String packageName) throws DeviceNotAvailableException { - return Long.parseLong(expectRemoteCommandToSucceed("stat -c '%s' " + packageName).trim()); - } - - private String expectRemoteCommandToSucceed(String cmd) throws DeviceNotAvailableException { - CommandResult result = mDevice.executeShellV2Command(cmd); - assertEquals("`" + cmd + "` failed: " + result.getStderr(), CommandStatus.SUCCESS, - result.getStatus()); - return result.getStdout(); - } - - private void expectRemoteCommandToFail(String cmd) throws DeviceNotAvailableException { - CommandResult result = mDevice.executeShellV2Command(cmd); - assertTrue("Unexpected success from `" + cmd + "`: " + result.getStderr(), - result.getStatus() != CommandStatus.SUCCESS); - } - - private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { - InstallMultiple() { - super(getDevice(), getBuild()); - } - - InstallMultiple addFileAndSignature(String filename) { - try { - addFile(filename); - addFile(filename + ".fsv_sig"); - } catch (FileNotFoundException e) { - fail("Missing test file: " + e); - } - return this; - } - } -} diff --git a/tests/ApkVerityTest/src/com/android/apkverity/BaseInstallMultiple.java b/tests/ApkVerityTest/src/com/android/apkverity/BaseInstallMultiple.java deleted file mode 100644 index 02e73d157dde..000000000000 --- a/tests/ApkVerityTest/src/com/android/apkverity/BaseInstallMultiple.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2019 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.apkverity; - -import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; -import com.android.tradefed.build.IBuildInfo; -import com.android.tradefed.device.DeviceNotAvailableException; -import com.android.tradefed.device.ITestDevice; - -import junit.framework.TestCase; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Base class for invoking the install-multiple command via ADB. Subclass this for less typing: - * - * <code> private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { public - * InstallMultiple() { super(getDevice(), null); } } </code> - */ -/*package*/ class BaseInstallMultiple<T extends BaseInstallMultiple<?>> { - - private final ITestDevice mDevice; - private final IBuildInfo mBuild; - - private final List<String> mArgs = new ArrayList<>(); - private final Map<File, String> mFileToRemoteMap = new HashMap<>(); - - /*package*/ BaseInstallMultiple(ITestDevice device, IBuildInfo buildInfo) { - mDevice = device; - mBuild = buildInfo; - addArg("-g"); - } - - T addArg(String arg) { - mArgs.add(arg); - return (T) this; - } - - T addFile(String filename) throws FileNotFoundException { - return addFile(filename, filename); - } - - T addFile(String filename, String remoteName) throws FileNotFoundException { - CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuild); - mFileToRemoteMap.put(buildHelper.getTestFile(filename), remoteName); - return (T) this; - } - - T inheritFrom(String packageName) { - addArg("-r"); - addArg("-p " + packageName); - return (T) this; - } - - void run() throws DeviceNotAvailableException { - run(true); - } - - void runExpectingFailure() throws DeviceNotAvailableException { - run(false); - } - - private void run(boolean expectingSuccess) throws DeviceNotAvailableException { - final ITestDevice device = mDevice; - - // Create an install session - final StringBuilder cmd = new StringBuilder(); - cmd.append("pm install-create"); - for (String arg : mArgs) { - cmd.append(' ').append(arg); - } - - String result = device.executeShellCommand(cmd.toString()); - TestCase.assertTrue(result, result.startsWith("Success")); - - final int start = result.lastIndexOf("["); - final int end = result.lastIndexOf("]"); - int sessionId = -1; - try { - if (start != -1 && end != -1 && start < end) { - sessionId = Integer.parseInt(result.substring(start + 1, end)); - } - } catch (NumberFormatException e) { - throw new IllegalStateException("Failed to parse install session: " + result); - } - if (sessionId == -1) { - throw new IllegalStateException("Failed to create install session: " + result); - } - - // Push our files into session. Ideally we'd use stdin streaming, - // but ddmlib doesn't support it yet. - for (final Map.Entry<File, String> entry : mFileToRemoteMap.entrySet()) { - final File file = entry.getKey(); - final String remoteName = entry.getValue(); - final String remotePath = "/data/local/tmp/" + file.getName(); - if (!device.pushFile(file, remotePath)) { - throw new IllegalStateException("Failed to push " + file); - } - - cmd.setLength(0); - cmd.append("pm install-write"); - cmd.append(' ').append(sessionId); - cmd.append(' ').append(remoteName); - cmd.append(' ').append(remotePath); - - result = device.executeShellCommand(cmd.toString()); - TestCase.assertTrue(result, result.startsWith("Success")); - } - - // Everything staged; let's pull trigger - cmd.setLength(0); - cmd.append("pm install-commit"); - cmd.append(' ').append(sessionId); - - result = device.executeShellCommand(cmd.toString()); - if (expectingSuccess) { - TestCase.assertTrue(result, result.contains("Success")); - } else { - TestCase.assertFalse(result, result.contains("Success")); - } - } -} diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestApp.dm b/tests/ApkVerityTest/testdata/ApkVerityTestApp.dm Binary files differdeleted file mode 100644 index e53a86131366..000000000000 --- a/tests/ApkVerityTest/testdata/ApkVerityTestApp.dm +++ /dev/null diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm b/tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm Binary files differdeleted file mode 100644 index 75396f1ba730..000000000000 --- a/tests/ApkVerityTest/testdata/ApkVerityTestAppSplit.dm +++ /dev/null diff --git a/tests/ApkVerityTest/testdata/README.md b/tests/ApkVerityTest/testdata/README.md deleted file mode 100644 index 163cb183a5ad..000000000000 --- a/tests/ApkVerityTest/testdata/README.md +++ /dev/null @@ -1,13 +0,0 @@ -This test only runs on rooted / debuggable device. - -The test tries to install subsets of base.{apk,dm}, split.{apk,dm} and their -corresponding .fsv_sig files (generated by build rule). If installed, the -tests also tries to tamper with the file at absolute disk offset to verify -if fs-verity is effective. - -How to generate dex metadata (.dm) -================================== - - adb shell profman --generate-test-profile=/data/local/tmp/primary.prof - adb pull /data/local/tmp/primary.prof - zip foo.dm primary.prof diff --git a/tests/BinaryTransparencyHostTest/Android.bp b/tests/BinaryTransparencyHostTest/Android.bp index dc6bdff6716c..615990f22e3c 100644 --- a/tests/BinaryTransparencyHostTest/Android.bp +++ b/tests/BinaryTransparencyHostTest/Android.bp @@ -35,6 +35,8 @@ java_test_host { data: [ ":BinaryTransparencyTestApp", ":EasterEgg", + ":FeatureSplitBase", + ":FeatureSplit1", ":com.android.apex.cts.shim.v2_rebootless_prebuilt", ], test_suites: [ diff --git a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java index 346622f0f467..6e5f08a11ed8 100644 --- a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java +++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java @@ -91,20 +91,20 @@ public final class BinaryTransparencyHostTest extends BaseHostJUnit4Test { public void testCollectAllSilentInstalledMbaInfo() throws Exception { try { new InstallMultiple() - .addFile("ApkVerityTestApp.apk") - .addFile("ApkVerityTestAppSplit.apk") + .addFile("FeatureSplitBase.apk") + .addFile("FeatureSplit1.apk") .run(); updatePreloadApp(); - assertNotNull(getDevice().getAppPackageInfo("com.android.apkverity")); + assertNotNull(getDevice().getAppPackageInfo("com.android.test.split.feature")); assertNotNull(getDevice().getAppPackageInfo("com.android.egg")); assertTrue(getDevice().setProperty("debug.transparency.bg-install-apps", - "com.android.apkverity,com.android.egg")); + "com.android.test.split.feature,com.android.egg")); runDeviceTest("testCollectAllSilentInstalledMbaInfo"); } finally { // No need to wait until job complete, since we can't verifying very meaningfully. cancelPendingJob(); - uninstallPackage("com.android.apkverity"); + uninstallPackage("com.android.test.split.feature"); uninstallPackage("com.android.egg"); } } diff --git a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java index c087a85da2a8..2bc056ee743f 100644 --- a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java +++ b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java @@ -121,7 +121,7 @@ public class BinaryTransparencyTest { // Verify assertThat(appInfoList).isNotEmpty(); // because we just installed from the host side - var expectedAppNames = Set.of("com.android.apkverity", "com.android.egg"); + var expectedAppNames = Set.of("com.android.test.split.feature", "com.android.egg"); var actualAppNames = appInfoList.stream().map((appInfo) -> appInfo.packageName) .collect(Collectors.toList()); assertThat(actualAppNames).containsAtLeastElementsIn(expectedAppNames); @@ -141,6 +141,6 @@ public class BinaryTransparencyTest { } } } - assertThat(actualSplitNames).containsExactly("feature_x"); // Name of ApkVerityTestAppSplit + assertThat(actualSplitNames).containsExactly("feature1"); // Name of FeatureSplit1 } } diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java index 48d050ce4391..bb0d30af42ee 100644 --- a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java +++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java @@ -22,9 +22,9 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import android.Manifest; -import android.app.compat.CompatChanges; import android.hardware.display.DisplayManager; import android.os.Looper; import android.support.test.uiautomator.UiDevice; @@ -34,6 +34,7 @@ import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceHolder; import android.view.SurfaceView; +import android.view.WindowManager; import androidx.lifecycle.Lifecycle; import androidx.test.core.app.ActivityScenario; @@ -52,7 +53,7 @@ import java.util.concurrent.TimeUnit; public class AttachedChoreographerTest { private static final String TAG = "AttachedChoreographerTest"; private static final long DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID = 170503758; - private static final long THRESHOLD_MS = 10; + private static final long THRESHOLD_MS = 4; private static final int FRAME_ITERATIONS = 21; private static final int CALLBACK_MISSED_THRESHOLD = 2; @@ -66,7 +67,7 @@ public class AttachedChoreographerTest { private SurfaceView mSurfaceView; private SurfaceHolder mSurfaceHolder; private Choreographer mChoreographer; - private boolean mIsFirstCallback = true; + private long mExpectedPresentTime; private int mCallbackMissedCounter = 0; @Before @@ -74,6 +75,13 @@ public class AttachedChoreographerTest { mScenario = ActivityScenario.launch(GraphicsActivity.class); mScenario.moveToState(Lifecycle.State.CREATED); mScenario.onActivity(activity -> { + // Lock the display frame rate. This will not allow SurfaceFlinger to use the frame rate + // override feature that throttles down the global choreographer for this test. + float refreshRate = activity.getDisplay().getMode().getRefreshRate(); + WindowManager.LayoutParams attrs = activity.getWindow().getAttributes(); + attrs.preferredRefreshRate = refreshRate; + activity.getWindow().setAttributes(attrs); + mSurfaceView = activity.findViewById(R.id.surface); mSurfaceHolder = mSurfaceView.getHolder(); mSurfaceHolder.addCallback(new SurfaceHolder.Callback() { @@ -95,6 +103,12 @@ public class AttachedChoreographerTest { }); UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + // TODO(b/290634611): clean this up once SF new front end is enabled by default + boolean sfNewFeEnabled = uiDevice.executeShellCommand("dumpsys SurfaceFlinger") + .indexOf("SurfaceFlinger New Frontend Enabled:true") != -1; + assumeTrue(sfNewFeEnabled); + uiDevice.wakeUp(); uiDevice.executeShellCommand("wm dismiss-keyguard"); mScenario.moveToState(Lifecycle.State.RESUMED); @@ -112,18 +126,16 @@ public class AttachedChoreographerTest { mDisplayManager.setRefreshRateSwitchingType( DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY); mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true); - boolean changeIsEnabled = - CompatChanges.isChangeEnabled( - DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGEID); - Log.i(TAG, "DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE_CHANGE_ID is " - + (changeIsEnabled ? "enabled" : "disabled")); }); } @After public void tearDown() { - mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate); - mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false); + if (mDisplayManager != null) { + mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate); + mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false); + } + InstrumentationRegistry.getInstrumentation().getUiAutomation() .dropShellPermissionIdentity(); } @@ -407,18 +419,141 @@ public class AttachedChoreographerTest { } } + @Test + public void testChoreographerAttachedAfterSetFrameRate() { + Log.i(TAG, "adyabr: starting testChoreographerAttachedAfterSetFrameRate"); + + class TransactionGenerator implements SurfaceControl.TransactionCommittedListener { + private SurfaceControl mSc; + private int mFrame; + private static final int NUM_FRAMES = 50; + private final CountDownLatch mCompleteLatch = new CountDownLatch(1); + + TransactionGenerator(SurfaceControl sc) { + mSc = sc; + + } + + @Override + public void onTransactionCommitted() { + mFrame++; + if (mFrame >= NUM_FRAMES) { + mCompleteLatch.countDown(); + return; + } + + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + transaction.setAlpha(mSc, 1.0f / mFrame) + .addTransactionCommittedListener(Runnable::run, this) + .apply(); + + } + + public void startAndWaitForCompletion() { + onTransactionCommitted(); + if (waitForCountDown(mCompleteLatch, /* timeoutInSeconds */ 10L)) { + fail("failed to send new transactions"); + } + } + } + + + for (int divisor : new int[]{2, 3}) { + CountDownLatch choreographerLatch = new CountDownLatch(1); + mScenario.onActivity(activity -> { + if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) { + fail("Unable to create surface within 1 Second"); + } + + SurfaceControl sc = mSurfaceView.getSurfaceControl(); + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + float displayRefreshRate = activity.getDisplay().getMode().getRefreshRate(); + float fps = displayRefreshRate / divisor; + long callbackDurationMs = Math.round(1000 / fps); + mCallbackMissedCounter = 0; + transaction.setFrameRate(sc, fps, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE) + .apply(); + + + new TransactionGenerator(sc).startAndWaitForCompletion(); + + Choreographer choreographer = sc.getChoreographer(); + verifyVsyncCallbacks(choreographer, callbackDurationMs, + choreographerLatch, FRAME_ITERATIONS); + }); + // wait for the previous callbacks to finish before moving to the next divisor + if (waitForCountDown(choreographerLatch, /* timeoutInSeconds */ 5L)) { + fail("Test not finished in 5 Seconds"); + } + } + } + + @Test + public void testChoreographerNonDivisorFixedSourceRefreshRate() { + CountDownLatch continueLatch = new CountDownLatch(1); + mScenario.onActivity(activity -> { + if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) { + fail("Unable to create surface within 1 Second"); + } + SurfaceControl sc = mSurfaceView.getSurfaceControl(); + Choreographer choreographer = sc.getChoreographer(); + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + float displayRefreshRate = activity.getDisplay().getMode().getRefreshRate(); + float fps = 61.7f; // hopefully not a divisor + long callbackDurationMs = Math.round(1000 / displayRefreshRate); + mCallbackMissedCounter = 0; + transaction.setFrameRate(sc, fps, Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE) + .addTransactionCommittedListener(Runnable::run, + () -> verifyVsyncCallbacks(choreographer, + callbackDurationMs, continueLatch, FRAME_ITERATIONS)) + .apply(); + }); + // wait for the previous callbacks to finish before moving to the next divisor + if (waitForCountDown(continueLatch, /* timeoutInSeconds */ 5L)) { + fail("Test not finished in 5 Seconds"); + } + } + + @Test + public void testChoreographerNonDivisorRefreshRate() { + CountDownLatch continueLatch = new CountDownLatch(1); + mScenario.onActivity(activity -> { + if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds */ 1L)) { + fail("Unable to create surface within 1 Second"); + } + SurfaceControl sc = mSurfaceView.getSurfaceControl(); + Choreographer choreographer = sc.getChoreographer(); + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + float displayRefreshRate = activity.getDisplay().getMode().getRefreshRate(); + float fps = 61.7f; // hopefully not a divisor + float expectedFps = displayRefreshRate / Math.round(displayRefreshRate / fps); + long callbackDurationMs = Math.round(1000 / expectedFps); + mCallbackMissedCounter = 0; + transaction.setFrameRate(sc, fps, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT) + .addTransactionCommittedListener(Runnable::run, + () -> verifyVsyncCallbacks(choreographer, + callbackDurationMs, continueLatch, FRAME_ITERATIONS)) + .apply(); + }); + // wait for the previous callbacks to finish before moving to the next divisor + if (waitForCountDown(continueLatch, /* timeoutInSeconds */ 5L)) { + fail("Test not finished in 5 Seconds"); + } + } + private void verifyVsyncCallbacks(Choreographer choreographer, long callbackDurationMs, CountDownLatch continueLatch, int frameCount) { - long callbackRequestedTimeNs = System.nanoTime(); choreographer.postVsyncCallback(frameData -> { + long expectedPresentTime = + frameData.getPreferredFrameTimeline().getExpectedPresentationTimeNanos(); if (frameCount > 0) { - if (!mIsFirstCallback) { - // Skip the first callback as it takes 1 frame - // to reflect the new refresh rate - long callbackDurationDiffMs = getCallbackDurationDiffInMs( - frameData.getFrameTimeNanos(), - callbackRequestedTimeNs, callbackDurationMs); - if (callbackDurationDiffMs < 0 || callbackDurationDiffMs > THRESHOLD_MS) { + if (mExpectedPresentTime > 0) { + long callbackDurationDiffMs = + TimeUnit.NANOSECONDS.toMillis( + expectedPresentTime - mExpectedPresentTime); + if (callbackDurationDiffMs < 0 + || Math.abs(callbackDurationMs - callbackDurationDiffMs) + > THRESHOLD_MS) { mCallbackMissedCounter++; Log.e(TAG, "Frame #" + Math.abs(frameCount - FRAME_ITERATIONS) + " vsync callback failed, expected callback in " @@ -427,25 +562,19 @@ public class AttachedChoreographerTest { + " but actual duration difference is " + callbackDurationDiffMs); } } - mIsFirstCallback = false; + mExpectedPresentTime = expectedPresentTime; verifyVsyncCallbacks(choreographer, callbackDurationMs, continueLatch, frameCount - 1); } else { - assertTrue("Missed timeline for " + mCallbackMissedCounter + " callbacks, while " - + CALLBACK_MISSED_THRESHOLD + " missed callbacks are allowed", + assertTrue("Missed timeline for " + mCallbackMissedCounter + + " callbacks, while " + CALLBACK_MISSED_THRESHOLD + + " missed callbacks are allowed", mCallbackMissedCounter <= CALLBACK_MISSED_THRESHOLD); continueLatch.countDown(); } }); } - private long getCallbackDurationDiffInMs(long callbackTimeNs, long requestedTimeNs, - long expectedCallbackMs) { - long actualTimeMs = TimeUnit.NANOSECONDS.toMillis(callbackTimeNs) - - TimeUnit.NANOSECONDS.toMillis(requestedTimeNs); - return Math.abs(expectedCallbackMs - actualTimeMs); - } - private boolean waitForCountDown(CountDownLatch countDownLatch, long timeoutInSeconds) { try { return !countDownLatch.await(timeoutInSeconds, TimeUnit.SECONDS); diff --git a/tests/CompanionDeviceMultiDeviceTests/OWNERS b/tests/CompanionDeviceMultiDeviceTests/OWNERS new file mode 100644 index 000000000000..7517836d9d71 --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/OWNERS @@ -0,0 +1,6 @@ +# Bug component: 708992 +evanxinchen@google.com +guojing@google.com +jeremyns@google.com +raphk@google.com +yukl@google.com diff --git a/tests/CompanionDeviceMultiDeviceTests/README.md b/tests/CompanionDeviceMultiDeviceTests/README.md new file mode 100644 index 000000000000..6cf735aaf00a --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/README.md @@ -0,0 +1,17 @@ +## CDM Multi-device Tests + +### Device Setup +To test on physical devices, connect _two_ devices locally and enable USB debugging setting on both devices. + +When running on a cloudtop or other remote setups, use pontis to connect the devices on remote set up by running `pontis start`. +Verify that pontis client is connected via `pontis status` and confirm that both devices are in "connected" state via `adb devices`. + +See go/pontis for more details regarding this workflow. + +To test on virtual devices, follow instructions to [set up netsim on cuttlefish](https://g3doc.corp.google.com/ambient/d2di/sim/g3doc/guide/cuttlefish.md?cl=head). +Launch _two_ instances of virtual devices by specifying `--num_instances=2` parameter. + +### Running the Test +``` +atest CompanionDeviceManagerMultiDeviceTestCases +``` diff --git a/tests/CompanionDeviceMultiDeviceTests/client/Android.bp b/tests/CompanionDeviceMultiDeviceTests/client/Android.bp new file mode 100644 index 000000000000..1e68c9dd459f --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/client/Android.bp @@ -0,0 +1,50 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "cdm_snippet", + srcs: ["src/**/*.kt"], + manifest: "AndroidManifest.xml", + + platform_apis: true, + target_sdk_version: "current", + + static_libs: [ + "androidx.test.ext.junit", + "androidx.test.uiautomator_uiautomator", + "compatibility-device-util-axt", + "cts-companion-common", + "cts-companion-uicommon", + "kotlin-stdlib", + "mobly-snippet-lib", + ], + libs: [ + "android.test.base", + "android.test.runner", + ], + + optimize: { + proguard_compatibility: true, + proguard_flags_files: ["proguard.flags"], + }, +} diff --git a/tests/CompanionDeviceMultiDeviceTests/client/AndroidManifest.xml b/tests/CompanionDeviceMultiDeviceTests/client/AndroidManifest.xml new file mode 100644 index 000000000000..11dc7634aab3 --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/client/AndroidManifest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<manifest + xmlns:android="http://schemas.android.com/apk/res/android" + package="android.companion.multidevices"> + + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.REQUEST_COMPANION_SELF_MANAGED" /> + <uses-permission android:name="android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE" /> + <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_WATCH" /> + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> + <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> + <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> + <uses-permission android:name="android.permission.DELIVER_COMPANION_MESSAGES" /> + + <uses-feature android:name="android.hardware.bluetooth" android:required="true"/> + <uses-feature android:name="android.software.companion_device_setup" /> + + <application> + <!-- Add any classes that implement the Snippet interface as meta-data, whose + value is a comma-separated string, each section being the package path + of a snippet class --> + <meta-data + android:name="mobly-snippets" + android:value="android.companion.multidevices.CompanionDeviceManagerSnippet" /> + </application> + + <!-- Add an instrumentation tag so that the app can be launched through an + instrument command. The runner `com.google.android.mobly.snippet.SnippetRunner` + is derived from `AndroidJUnitRunner`, and is required to use the + Mobly Snippet Lib. --> + <instrumentation + android:name="com.google.android.mobly.snippet.SnippetRunner" + android:targetPackage="android.companion.multidevices" /> +</manifest> diff --git a/tests/CompanionDeviceMultiDeviceTests/client/proguard.flags b/tests/CompanionDeviceMultiDeviceTests/client/proguard.flags new file mode 100644 index 000000000000..1c70253af87c --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/client/proguard.flags @@ -0,0 +1,24 @@ +# Keep all companion classes. +-keep class android.companion.** { + *; +} + +# Do not touch Mobly. +-keep class com.google.android.mobly.** { + *; +} + +# Keep names for easy debugging. +-dontobfuscate + +# Necessary to allow debugging. +-keepattributes * + +# By default, proguard leaves all classes in their original package, which +# needlessly repeats com.google.android.apps.etc. +-repackageclasses "" + +# Allows proguard to make private and protected methods and fields public as +# part of optimization. This lets proguard inline trivial getter/setter +# methods. +-allowaccessmodification
\ No newline at end of file diff --git a/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/CallbackUtils.kt b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/CallbackUtils.kt new file mode 100644 index 000000000000..3e4944a21258 --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/CallbackUtils.kt @@ -0,0 +1,107 @@ +/* + * 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 android.companion.multidevices + +import android.companion.AssociationInfo +import android.companion.CompanionDeviceManager +import android.companion.CompanionException +import android.content.IntentSender +import android.os.OutcomeReceiver +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit.SECONDS +import java.util.concurrent.TimeoutException + +/** Blocking callbacks for Wi-Fi Aware and Connectivity Manager. */ +object CallbackUtils { + private const val TAG = "CDM_CallbackUtils" + private const val CALLBACK_TIMEOUT_SEC = 30L + private const val MESSAGE_CALLBACK_TIMEOUT_SEC = 5L + + class AssociationCallback : CompanionDeviceManager.Callback() { + private val pending = CountDownLatch(1) + private val created = CountDownLatch(1) + + private var pendingIntent: IntentSender? = null + private var associationInfo: AssociationInfo? = null + private var error: String? = null + + override fun onAssociationPending(intentSender: IntentSender) { + this.pendingIntent = intentSender + pending.countDown() + } + + override fun onAssociationCreated(associationInfo: AssociationInfo) { + this.associationInfo = associationInfo + created.countDown() + } + + override fun onFailure(error: CharSequence?) { + this.error = error?.toString() ?: "There was an unexpected failure." + pending.countDown() + created.countDown() + } + + fun waitForPendingIntent(): IntentSender? { + if (!pending.await(CALLBACK_TIMEOUT_SEC, SECONDS)) { + throw TimeoutException("Pending association request timed out.") + } + + error?.let { + throw CompanionException(it) + } + + return pendingIntent + } + + fun waitForAssociation(): AssociationInfo? { + if (!created.await(CALLBACK_TIMEOUT_SEC, SECONDS)) { + throw TimeoutException("Association request timed out.") + } + + error?.let { + throw CompanionException(it) + } + + return associationInfo + } + } + + class SystemDataTransferCallback : OutcomeReceiver<Void, CompanionException> { + private val completed = CountDownLatch(1) + + private var error: CompanionException? = null + + override fun onResult(result: Void?) { + completed.countDown() + } + + override fun onError(error: CompanionException) { + this.error = error + completed.countDown() + } + + fun waitForCompletion() { + if (!completed.await(CALLBACK_TIMEOUT_SEC, SECONDS)) { + throw TimeoutException("System data transfer timed out.") + } + + error?.let { + throw it + } + } + } +} diff --git a/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/CompanionDeviceManagerSnippet.kt b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/CompanionDeviceManagerSnippet.kt new file mode 100644 index 000000000000..03352e36576e --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/CompanionDeviceManagerSnippet.kt @@ -0,0 +1,183 @@ +/* + * 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 android.companion.multidevices + +import android.app.Instrumentation +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.companion.AssociationInfo +import android.companion.AssociationRequest +import android.companion.BluetoothDeviceFilter +import android.companion.CompanionDeviceManager +import android.companion.CompanionException +import android.companion.cts.common.CompanionActivity +import android.companion.multidevices.CallbackUtils.AssociationCallback +import android.companion.multidevices.CallbackUtils.SystemDataTransferCallback +import android.companion.multidevices.bluetooth.BluetoothConnector +import android.companion.multidevices.bluetooth.BluetoothController +import android.companion.cts.uicommon.CompanionDeviceManagerUi +import android.content.Context +import android.os.Handler +import android.os.HandlerExecutor +import android.os.HandlerThread +import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.google.android.mobly.snippet.Snippet +import com.google.android.mobly.snippet.event.EventCache +import com.google.android.mobly.snippet.rpc.Rpc +import java.util.concurrent.Executor +import java.util.regex.Pattern + +/** + * Snippet class that exposes Android APIs in CompanionDeviceManager. + */ +class CompanionDeviceManagerSnippet : Snippet { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()!! + private val context: Context = instrumentation.targetContext + + private val btAdapter: BluetoothAdapter by lazy { + (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter + } + private val companionDeviceManager: CompanionDeviceManager by lazy { + context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager + } + private val btConnector: BluetoothConnector by lazy { + BluetoothConnector(btAdapter, companionDeviceManager) + } + + private val uiDevice by lazy { UiDevice.getInstance(instrumentation) } + private val confirmationUi by lazy { CompanionDeviceManagerUi(uiDevice) } + private val btController by lazy { BluetoothController(context, btAdapter, uiDevice) } + + private val eventCache = EventCache.getInstance() + private val handlerThread = HandlerThread("Snippet-Aware") + private val handler: Handler + private val executor: Executor + + init { + handlerThread.start() + handler = Handler(handlerThread.looper) + executor = HandlerExecutor(handler) + } + + /** + * Make device discoverable to other devices via BLE and return device name. + */ + @Rpc(description = "Start advertising device to be discoverable.") + fun becomeDiscoverable(): String { + btController.becomeDiscoverable() + return btAdapter.name + } + + /** + * Associate with a nearby device with given name and return newly-created association ID. + */ + @Rpc(description = "Start device association flow.") + @Throws(Exception::class) + fun associate(deviceName: String): Int { + val filter = BluetoothDeviceFilter.Builder() + .setNamePattern(Pattern.compile(deviceName)) + .build() + val request = AssociationRequest.Builder() + .setSingleDevice(true) + .addDeviceFilter(filter) + .build() + val callback = AssociationCallback() + companionDeviceManager.associate(request, callback, handler) + val pendingConfirmation = callback.waitForPendingIntent() + ?: throw CompanionException("Association is pending but intent sender is null.") + CompanionActivity.launchAndWait(context) + CompanionActivity.startIntentSender(pendingConfirmation) + confirmationUi.waitUntilVisible() + confirmationUi.waitUntilPositiveButtonIsEnabledAndClick() + confirmationUi.waitUntilGone() + + val (_, result) = CompanionActivity.waitForActivityResult() + if (result == null) { + throw CompanionException("Association result can't be null.") + } + + val association = checkNotNull(result.getParcelableExtra( + CompanionDeviceManager.EXTRA_ASSOCIATION, + AssociationInfo::class.java + )) + val remoteDevice = association.associatedDevice?.getBluetoothDevice()!! + + // Register associated device + btConnector.registerDevice(association.id, remoteDevice) + + return association.id + } + + /** + * Disassociate an association with given ID. + */ + @Rpc(description = "Disassociate device.") + @Throws(Exception::class) + fun disassociate(associationId: Int) { + companionDeviceManager.disassociate(associationId) + } + + /** + * Consent to system data transfer and carry it out using Bluetooth socket. + */ + @Rpc(description = "Start permissions sync.") + fun startPermissionsSync(associationId: Int) { + val pendingIntent = checkNotNull(companionDeviceManager + .buildPermissionTransferUserConsentIntent(associationId)) + CompanionActivity.launchAndWait(context) + CompanionActivity.startIntentSender(pendingIntent) + confirmationUi.waitUntilSystemDataTransferConfirmationVisible() + confirmationUi.clickPositiveButton() + confirmationUi.waitUntilGone() + + CompanionActivity.waitForActivityResult() + + val callback = SystemDataTransferCallback() + companionDeviceManager.startSystemDataTransfer(associationId, executor, callback) + callback.waitForCompletion() + } + + @Rpc(description = "Attach transport to the BT client socket.") + fun attachClientSocket(id: Int) { + btConnector.attachClientSocket(id) + } + + @Rpc(description = "Attach transport to the BT server socket.") + fun attachServerSocket(id: Int) { + btConnector.attachServerSocket(id) + } + + @Rpc(description = "Close all open sockets.") + fun closeAllSockets() { + // Close all open sockets + btConnector.closeAllSockets() + } + + @Rpc(description = "Disassociate all associations.") + fun disassociateAll() { + companionDeviceManager.myAssociations.forEach { + Log.d(TAG, "Disassociating id=${it.id}.") + companionDeviceManager.disassociate(it.id) + } + } + + companion object { + private const val TAG = "CDM_CompanionDeviceManagerSnippet" + } +} diff --git a/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/bluetooth/BluetoothConnector.kt b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/bluetooth/BluetoothConnector.kt new file mode 100644 index 000000000000..c7312d2e4b10 --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/bluetooth/BluetoothConnector.kt @@ -0,0 +1,155 @@ +/* + * 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 android.companion.multidevices.bluetooth + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothServerSocket +import android.bluetooth.BluetoothSocket +import android.companion.CompanionDeviceManager +import android.util.Log +import java.io.IOException +import java.util.UUID + +class BluetoothConnector( + private val adapter: BluetoothAdapter, + private val cdm: CompanionDeviceManager +) { + companion object { + private const val TAG = "CDM_BluetoothServer" + + private val SERVICE_NAME = "CDM_BluetoothChannel" + private val SERVICE_UUID = UUID.fromString("435fe1d9-56c5-455d-a516-d5e6b22c52f9") + + // Registry of bluetooth server threads + private val serverThreads = mutableMapOf<Int, BluetoothServerThread>() + + // Registry of remote bluetooth devices + private val remoteDevices = mutableMapOf<Int, BluetoothDevice>() + + // Set of connected client sockets + private val clientSockets = mutableMapOf<Int, BluetoothSocket>() + } + + fun attachClientSocket(associationId: Int) { + try { + val device = remoteDevices[associationId]!! + val socket = device.createRfcommSocketToServiceRecord(SERVICE_UUID) + if (clientSockets.containsKey(associationId)) { + detachClientSocket(associationId) + clientSockets[associationId] = socket + } else { + clientSockets += associationId to socket + } + + socket.connect() + Log.d(TAG, "Attaching client socket $socket.") + cdm.attachSystemDataTransport( + associationId, + socket.inputStream, + socket.outputStream + ) + } catch (e: IOException) { + Log.e(TAG, "Failed to attach client socket.", e) + throw RuntimeException(e) + } + } + + fun attachServerSocket(associationId: Int) { + val serverThread: BluetoothServerThread + if (serverThreads.containsKey(associationId)) { + serverThread = serverThreads[associationId]!! + } else { + serverThread = BluetoothServerThread(associationId) + serverThreads += associationId to serverThread + } + + // Start thread + if (!serverThread.isOpen) { + serverThread.start() + } + } + + fun closeAllSockets() { + val iter = clientSockets.keys.iterator() + while (iter.hasNext()) { + detachClientSocket(iter.next()) + } + for (thread in serverThreads.values) { + thread.shutdown() + } + serverThreads.clear() + } + + fun registerDevice(associationId: Int, remoteDevice: BluetoothDevice) { + remoteDevices[associationId] = remoteDevice + } + + private fun detachClientSocket(associationId: Int) { + try { + Log.d(TAG, "Detaching client socket.") + cdm.detachSystemDataTransport(associationId) + clientSockets[associationId]?.close() + } catch (e: IOException) { + Log.e(TAG, "Failed to detach client socket.", e) + throw RuntimeException(e) + } + } + + inner class BluetoothServerThread( + private val associationId: Int + ) : Thread() { + private lateinit var mServerSocket: BluetoothServerSocket + + var isOpen = false + + override fun run() { + try { + Log.d(TAG, "Listening for remote connections...") + mServerSocket = adapter.listenUsingRfcommWithServiceRecord( + SERVICE_NAME, + SERVICE_UUID + ) + isOpen = true + do { + val socket = mServerSocket.accept() + Log.d(TAG, "Attaching server socket $socket.") + cdm.attachSystemDataTransport( + associationId, + socket.inputStream, + socket.outputStream + ) + } while (isOpen) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + fun shutdown() { + if (!isOpen || !this::mServerSocket.isInitialized) return + + try { + Log.d(TAG, "Closing server socket.") + cdm.detachSystemDataTransport(associationId) + mServerSocket.close() + isOpen = false + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } +} diff --git a/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/bluetooth/BluetoothController.kt b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/bluetooth/BluetoothController.kt new file mode 100644 index 000000000000..c4d202694b87 --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/bluetooth/BluetoothController.kt @@ -0,0 +1,109 @@ +/* + * 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 android.companion.multidevices.bluetooth + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.SystemClock +import android.util.Log +import androidx.test.uiautomator.UiDevice +import java.util.concurrent.TimeoutException +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +/** Controls the local Bluetooth adapter for testing. */ +class BluetoothController( + private val context: Context, + private val adapter: BluetoothAdapter, + private val ui: UiDevice +) { + companion object { + private const val TAG = "CDM_BluetoothController" + } + + private val bluetoothUi by lazy { BluetoothUi(ui) } + + init { + Log.d(TAG, "Registering pairing listener.") + context.registerReceiver( + PairingBroadcastReceiver(), + IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST) + ) + } + + val isEnabled: Boolean + get() = adapter.isEnabled + + /** Turns on the local Bluetooth adapter */ + fun enableBluetooth() { + if (isEnabled) return + + val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + bluetoothUi.clickAllowButton() + waitFor { adapter.state == BluetoothAdapter.STATE_ON } + } + + /** Become discoverable for specified duration */ + fun becomeDiscoverable(duration: Duration = 15.seconds) { + enableBluetooth() + + val intent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, duration.inWholeSeconds) + context.startActivity(intent) + bluetoothUi.clickAllowButton() + } + + /** Unpair all devices for cleanup */ + fun unpairAllDevices() { + for (device in adapter.bondedDevices) { + Log.d(TAG, "Unpairing $device.") + if (!device.removeBond()) continue + waitFor { device.bondState == BluetoothDevice.BOND_NONE } + } + } + + private fun waitFor( + interval: Duration = 1.seconds, + timeout: Duration = 5.seconds, + condition: () -> Boolean + ) { + var elapsed = 0L + while (elapsed < timeout.inWholeMilliseconds) { + if (condition.invoke()) return + SystemClock.sleep(interval.inWholeMilliseconds) + elapsed += interval.inWholeMilliseconds + } + throw TimeoutException("Bluetooth did not become an expected state.") + } + + inner class PairingBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Log.d(TAG, "Received broadcast for ${intent.action}") + + // onReceive() somehow blocks pairing prompt from launching + Thread { bluetoothUi.confirmPairingRequest() }.start() + context.unregisterReceiver(this) + } + } +} diff --git a/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/bluetooth/BluetoothUi.kt b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/bluetooth/BluetoothUi.kt new file mode 100644 index 000000000000..6983cb035498 --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/client/src/android/companion/multidevices/bluetooth/BluetoothUi.kt @@ -0,0 +1,66 @@ +/* + * 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 android.companion.multidevices.bluetooth + +import android.companion.cts.uicommon.CompanionDeviceManagerUi +import android.util.Log +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import java.util.regex.Pattern + +class BluetoothUi(private val ui: UiDevice) : CompanionDeviceManagerUi(ui) { + fun clickAllowButton() = click(ALLOW_BUTTON, "Allow button") + + fun confirmPairingRequest(): Boolean { + if (ui.hasObject(PAIRING_PIN_ENTRY)) { + // It is prompting for a custom user pin entry + Log.d(TAG, "Is user entry prompt.") + ui.findObject(PAIRING_PIN_ENTRY).text = "0000" + click(OK_BUTTON, "Ok button") + } else { + // It just needs user consent + Log.d(TAG, "Looking for pair button.") + val button = ui.wait(Until.findObject(PAIR_BUTTON), 1_000) + if (button != null) { + Log.d(TAG, "Pair button found.") + button.click() + return true + } + Log.d(TAG, "Pair button not found.") + } + return false + } + + companion object { + private const val TAG = "CDM_BluetoothUi" + + private val ALLOW_TEXT_PATTERN = caseInsensitive("allow") + private val ALLOW_BUTTON = By.text(ALLOW_TEXT_PATTERN).clickable(true) + + private val PAIRING_PIN_ENTRY = By.clazz(".EditText") + + private val OK_TEXT_PATTERN = caseInsensitive("ok") + private val OK_BUTTON = By.text(OK_TEXT_PATTERN).clickable(true) + + private val PAIR_TEXT_PATTERN = caseInsensitive("pair") + private val PAIR_BUTTON = By.text(PAIR_TEXT_PATTERN).clickable(true) + + private fun caseInsensitive(text: String): Pattern = + Pattern.compile(text, Pattern.CASE_INSENSITIVE) + } +} diff --git a/tests/CompanionDeviceMultiDeviceTests/host/Android.bp b/tests/CompanionDeviceMultiDeviceTests/host/Android.bp new file mode 100644 index 000000000000..03335c7cd576 --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/host/Android.bp @@ -0,0 +1,50 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +python_test_host { + name: "CompanionDeviceManagerMultiDeviceTestCases", + main: "cdm_transport_test.py", + srcs: ["*.py"], + libs: [ + "mobly", + ], + test_suites: [ + "general-tests", + ], + test_options: { + unit_test: false, + tags: ["mobly"], + }, + data: [ + ":cdm_snippet", + ], + version: { + py2: { + enabled: false, + }, + py3: { + enabled: true, + embedded_launcher: true, + }, + }, +} diff --git a/tests/CompanionDeviceMultiDeviceTests/host/AndroidTest.xml b/tests/CompanionDeviceMultiDeviceTests/host/AndroidTest.xml new file mode 100644 index 000000000000..9d1813ff79bc --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/host/AndroidTest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Config for CDM multi-device test cases"> + <option name="test-tag" value="CompanionDeviceMultiDeviceTests" /> + <option name="config-descriptor:metadata" key="component" value="framework" /> + <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" /> + <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" /> + <option name="config-descriptor:metadata" key="parameter" value="secondary_user" /> + + <object class="com.android.tradefed.testtype.suite.module.DeviceFeatureModuleController" + type="module_controller"> + <option name="required-feature" value="android.software.companion_device_setup" /> + </object> + + <device name="device1"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="test-file-name" value="cdm_snippet.apk" /> + </target_preparer> + </device> + <device name="device2"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="test-file-name" value="cdm_snippet.apk" /> + </target_preparer> + </device> + + <test class="com.android.tradefed.testtype.mobly.MoblyBinaryHostTest"> + <!-- The mobly-par-file-name should match the module name --> + <option name="mobly-par-file-name" value="CompanionDeviceManagerMultiDeviceTestCases" /> + <!-- Timeout limit in milliseconds for all test cases of the python binary --> + <option name="mobly-test-timeout" value="60000" /> + </test> +</configuration> diff --git a/tests/CompanionDeviceMultiDeviceTests/host/cdm_base_test.py b/tests/CompanionDeviceMultiDeviceTests/host/cdm_base_test.py new file mode 100644 index 000000000000..bb10658c25c3 --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/host/cdm_base_test.py @@ -0,0 +1,63 @@ +# Lint as: python3 +""" +Base class for setting up devices for CDM functionalities. +""" + +from mobly import base_test +from mobly import utils +from mobly.controllers import android_device + +CDM_SNIPPET_PACKAGE = 'android.companion.multidevices' + + +class BaseTestClass(base_test.BaseTestClass): + + def setup_class(self): + # Declare that two Android devices are needed. + self.sender, self.receiver = self.register_controller( + android_device, min_number=2) + self.sender_id = None + self.receiver_id = None + + def _setup_device(device): + device.load_snippet('cdm', CDM_SNIPPET_PACKAGE) + device.adb.shell('input keyevent KEYCODE_WAKEUP') + device.adb.shell('input keyevent KEYCODE_MENU') + device.adb.shell('input keyevent KEYCODE_HOME') + + # Clean up existing associations + device.cdm.disassociateAll() + + # Sets up devices in parallel to save time. + utils.concurrent_exec( + _setup_device, + ((self.sender,), (self.receiver,)), + max_workers=2, + raise_on_exception=True) + + def associate_devices(self) -> tuple[int, int]: + """Associate devices with each other and return association IDs for both""" + # If association already exists, don't need another + if self.sender_id and self.receiver_id: + return (self.sender_id, self.receiver_id) + + receiver_name = self.receiver.cdm.becomeDiscoverable() + self.receiver_id = self.sender.cdm.associate(receiver_name) + + sender_name = self.sender.cdm.becomeDiscoverable() + self.sender_id = self.receiver.cdm.associate(sender_name) + + return (self.sender_id, self.receiver_id) + + def attach_transports(self): + """Attach transports to both devices""" + self.associate_devices() + + self.receiver.cdm.attachServerSocket(self.sender_id) + self.sender.cdm.attachClientSocket(self.receiver_id) + + def teardown_class(self): + """Clean up the opened sockets""" + self.sender.cdm.closeAllSockets() + self.receiver.cdm.closeAllSockets() + diff --git a/tests/CompanionDeviceMultiDeviceTests/host/cdm_transport_test.py b/tests/CompanionDeviceMultiDeviceTests/host/cdm_transport_test.py new file mode 100644 index 000000000000..5516c0f8e09f --- /dev/null +++ b/tests/CompanionDeviceMultiDeviceTests/host/cdm_transport_test.py @@ -0,0 +1,28 @@ +# Lint as: python3 +""" +Test E2E CDM functions on mobly. +""" + +import cdm_base_test +import sys + +from mobly import asserts +from mobly import test_runner + +CDM_SNIPPET_PACKAGE = 'android.companion.multidevices' + + +class TransportTestClass(cdm_base_test.BaseTestClass): + + def test_permissions_sync(self): + """This tests permissions sync from one device to another.""" + + # associate and attach transports + self.attach_transports() + + # start permissions sync + self.sender.cdm.startPermissionsSync(self.receiver_id) + + +if __name__ == '__main__': + test_runner.main()
\ No newline at end of file diff --git a/tests/CtsSurfaceControlTestsStaging/Android.bp b/tests/CtsSurfaceControlTestsStaging/Android.bp new file mode 100644 index 000000000000..3272cef2eb79 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/Android.bp @@ -0,0 +1,48 @@ +// Copyright 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + // SurfaceControl tests for APIs that are not public. If an API becomes public, they should + // live in CtsSurfaceControlTests instead. + name: "CtsSurfaceControlTestsStaging", + srcs: ["src/**/*.java"], + libs: [ + "android.test.runner", + "android.test.base", + ], + static_libs: [ + "androidx.test.core", + "androidx.test.ext.junit", + "androidx.test.rules", + "compatibility-device-util-axt", + "com.google.android.material_material", + "truth-prebuilt", + ], + resource_dirs: ["src/main/res"], + certificate: "platform", + platform_apis: true, + test_suites: ["device-tests"], + optimize: { + enabled: false, + }, +} diff --git a/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml b/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml new file mode 100644 index 000000000000..d8eb9ff37e78 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.view.surfacecontroltests"> + + <application android:debuggable="true" android:testOnly="true"> + <uses-library android:name="android.test.runner"/> + <activity + android:name=".GraphicsActivity" + android:exported="false"> + </activity> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="android.view.surfacecontroltests" + android:label="Tests of android.view.surfacecontroltests"> + </instrumentation> +</manifest>
\ No newline at end of file diff --git a/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml b/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml new file mode 100644 index 000000000000..5c0163fcfa7e --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 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. + --> +<configuration description="Config for CtsSurfaceControlTestsStaging cases"> + <option name="test-tag" value="CtsSurfaceControlTestsStaging"/> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="CtsSurfaceControlTestsStaging.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="android.view.surfacecontroltests" /> + <option name="hidden-api-checks" value="false" /> + <option name="isolated-storage" value="false" /> + </test> +</configuration>
\ No newline at end of file diff --git a/tests/CtsSurfaceControlTestsStaging/OWNERS b/tests/CtsSurfaceControlTestsStaging/OWNERS new file mode 100644 index 000000000000..438b7f2ce335 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/OWNERS @@ -0,0 +1 @@ +include platform/cts:/tests/tests/graphics/OWNERS
\ No newline at end of file diff --git a/tests/CtsSurfaceControlTestsStaging/README.md b/tests/CtsSurfaceControlTestsStaging/README.md new file mode 100644 index 000000000000..1a2c5f64e73f --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/README.md @@ -0,0 +1,4 @@ +Tests for SurfaceControl for APIs that are not public. If APIs are made public, they can +be moved into the cts files, e.g.: +1. cts/tests/surfacecontrol/src/android/view/surfacecontrol/cts/SurfaceControlTest.java +2. cts/tests/surfacecontrol/src/android/view/surfacecontrol/cts/ASurfaceControlTest.java
\ No newline at end of file diff --git a/tests/CtsSurfaceControlTestsStaging/TEST_MAPPING b/tests/CtsSurfaceControlTestsStaging/TEST_MAPPING new file mode 100644 index 000000000000..de0ad5915868 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "CtsSurfaceControlTestsStaging" + } + ] +}
\ No newline at end of file diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java new file mode 100644 index 000000000000..4b12053af5c7 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java @@ -0,0 +1,639 @@ +/* + * Copyright 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 android.view.surfacecontroltests; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.app.Activity; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +/** + * An Activity to help with frame rate testing. + */ +public class GraphicsActivity extends Activity { + private static final String TAG = "GraphicsActivity"; + private static final long FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS = 2; + private static final long STABLE_FRAME_RATE_WAIT_SECONDS = 1; + private static final long POST_BUFFER_INTERVAL_MILLIS = 500; + private static final int PRECONDITION_WAIT_MAX_ATTEMPTS = 5; + private static final long PRECONDITION_WAIT_TIMEOUT_SECONDS = 20; + private static final long PRECONDITION_VIOLATION_WAIT_TIMEOUT_SECONDS = 3; + private static final float FRAME_RATE_TOLERANCE = 0.01f; + + static class FpsRange { + // The max difference between refresh rates in order to be considered equal. + private static final double FPS_THRESHOLD = 0.001; + double mMin; + double mMax; + FpsRange(double min, double max) { + mMin = min; + mMax = max; + } + public boolean includes(double fps) { + return fps >= mMin - FPS_THRESHOLD && mMax + FPS_THRESHOLD >= fps; + } + } + + // TODO(b/293651105): Unhardcode category fps range mapping + private static final FpsRange FRAME_RATE_CATEGORY_HIGH = new FpsRange(90, 120); + private static final FpsRange FRAME_RATE_CATEGORY_NORMAL = new FpsRange(60, 90); + private static final FpsRange FRAME_RATE_CATEGORY_LOW = new FpsRange(30, 60); + + private DisplayManager mDisplayManager; + private SurfaceView mSurfaceView; + private Handler mHandler = new Handler(Looper.getMainLooper()); + private final Object mLock = new Object(); + private Surface mSurface = null; + private float mDeviceFrameRate; + private ModeChangedEvents mModeChangedEvents = new ModeChangedEvents(); + + private enum ActivityState { RUNNING, PAUSED, DESTROYED } + + private ActivityState mActivityState; + + SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + synchronized (mLock) { + mSurface = holder.getSurface(); + mLock.notify(); + } + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + synchronized (mLock) { + mSurface = null; + mLock.notify(); + } + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} + }; + + DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { + @Override + public void onDisplayAdded(int displayId) {} + + @Override + public void onDisplayChanged(int displayId) { + if (displayId != Display.DEFAULT_DISPLAY) { + return; + } + synchronized (mLock) { + Display.Mode mode = mDisplayManager.getDisplay(displayId).getMode(); + mModeChangedEvents.add(mode); + float frameRate = mode.getRefreshRate(); + if (frameRate != mDeviceFrameRate) { + Log.i(TAG, + String.format("Frame rate changed: %.2f --> %.2f", mDeviceFrameRate, + frameRate)); + mDeviceFrameRate = frameRate; + mLock.notify(); + } + } + } + + @Override + public void onDisplayRemoved(int displayId) {} + }; + + // Wrapper around ArrayList for which the only allowed mutable operation is add(). + // We use this to store all mode change events during a test. When we need to iterate over + // all mode changes during a certain operation, we use the number of events in the beginning + // and in the end. It's important to never clear or modify the elements in this list hence the + // wrapper. + private static class ModeChangedEvents { + private List<Display.Mode> mEvents = new ArrayList<>(); + + public void add(Display.Mode mode) { + mEvents.add(mode); + } + + public Display.Mode get(int i) { + return mEvents.get(i); + } + + public int size() { + return mEvents.size(); + } + } + + private static class PreconditionViolatedException extends RuntimeException { + PreconditionViolatedException() {} + } + + private static class FrameRateTimeoutException extends RuntimeException { + FrameRateTimeoutException(float expectedFrameRate, float deviceFrameRate) { + this.expectedFrameRate = expectedFrameRate; + this.deviceFrameRate = deviceFrameRate; + } + + public float expectedFrameRate; + public float deviceFrameRate; + } + + public enum Api { + // Much of the code is copied from the SetFrameRate cts test. Add APIs as support grows. + SURFACE_CONTROL("SurfaceControl"); + + private final String mName; + Api(String name) { + mName = name; + } + + public String toString() { + return mName; + } + } + + private static class TestSurface { + private String mName; + private SurfaceControl mSurfaceControl; + private Surface mSurface; + private int mColor; + private boolean mLastBufferPostTimeValid; + private long mLastBufferPostTime; + + TestSurface(SurfaceControl parentSurfaceControl, Surface parentSurface, String name, + Rect destFrame, boolean visible, int color) { + mName = name; + mColor = color; + + assertNotNull("No parent surface", parentSurfaceControl); + mSurfaceControl = new SurfaceControl.Builder() + .setParent(parentSurfaceControl) + .setName(mName) + .setBufferSize(destFrame.right - destFrame.left, + destFrame.bottom - destFrame.top) + .build(); + try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { + transaction.setGeometry(mSurfaceControl, null, destFrame, Surface.ROTATION_0) + .apply(); + } + mSurface = new Surface(mSurfaceControl); + + setVisibility(visible); + postBuffer(); + } + + public int setFrameRateCategory(int category) { + Log.i(TAG, + String.format( + "Setting frame rate category for %s: category=%d", mName, category)); + + int rc = 0; + try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { + transaction.setFrameRateCategory(mSurfaceControl, category); + transaction.apply(); + } + return rc; + } + + public void setVisibility(boolean visible) { + Log.i(TAG, + String.format("Setting visibility for %s: %s", mName, + visible ? "visible" : "hidden")); + try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { + transaction.setVisibility(mSurfaceControl, visible).apply(); + } + } + + public void postBuffer() { + mLastBufferPostTimeValid = true; + mLastBufferPostTime = System.nanoTime(); + Canvas canvas = mSurface.lockHardwareCanvas(); + canvas.drawColor(mColor); + mSurface.unlockCanvasAndPost(canvas); + } + + public long getLastBufferPostTime() { + assertTrue("No buffer posted yet", mLastBufferPostTimeValid); + return mLastBufferPostTime; + } + + public void release() { + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } + if (mSurfaceControl != null) { + try (SurfaceControl.Transaction transaction = new SurfaceControl.Transaction()) { + transaction.reparent(mSurfaceControl, null).apply(); + } + mSurfaceControl.release(); + mSurfaceControl = null; + } + } + + @Override + protected void finalize() throws Throwable { + try { + release(); + } finally { + super.finalize(); + } + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + synchronized (mLock) { + mDisplayManager = getSystemService(DisplayManager.class); + Display.Mode mode = getDisplay().getMode(); + mDeviceFrameRate = mode.getRefreshRate(); + // Insert the initial mode so we have the full display mode history. + mModeChangedEvents.add(mode); + mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); + mSurfaceView = new SurfaceView(this); + mSurfaceView.setWillNotDraw(false); + mSurfaceView.setZOrderOnTop(true); + setContentView(mSurfaceView, + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mDisplayManager.unregisterDisplayListener(mDisplayListener); + synchronized (mLock) { + mActivityState = ActivityState.DESTROYED; + mLock.notify(); + } + } + + @Override + public void onPause() { + super.onPause(); + synchronized (mLock) { + mActivityState = ActivityState.PAUSED; + mLock.notify(); + } + } + + @Override + public void onResume() { + super.onResume(); + synchronized (mLock) { + mActivityState = ActivityState.RUNNING; + mLock.notify(); + } + } + + // Returns the refresh rates with the same resolution as "mode". + private ArrayList<Float> getRefreshRates(Display.Mode mode, Display display) { + Display.Mode[] modes = display.getSupportedModes(); + ArrayList<Float> frameRates = new ArrayList<>(); + for (Display.Mode supportedMode : modes) { + if (hasSameResolution(supportedMode, mode)) { + frameRates.add(supportedMode.getRefreshRate()); + } + } + Collections.sort(frameRates); + ArrayList<Float> uniqueFrameRates = new ArrayList<>(); + for (float frameRate : frameRates) { + if (uniqueFrameRates.isEmpty() + || frameRate - uniqueFrameRates.get(uniqueFrameRates.size() - 1) + >= FRAME_RATE_TOLERANCE) { + uniqueFrameRates.add(frameRate); + } + } + return uniqueFrameRates; + } + + private boolean hasSameResolution(Display.Mode mode1, Display.Mode mode2) { + return mode1.getPhysicalHeight() == mode2.getPhysicalHeight() + && mode1.getPhysicalWidth() == mode2.getPhysicalWidth(); + } + + private boolean isFrameRateMultiple(float higherFrameRate, float lowerFrameRate) { + float multiple = higherFrameRate / lowerFrameRate; + int roundedMultiple = Math.round(multiple); + return roundedMultiple > 0 + && Math.abs(roundedMultiple * lowerFrameRate - higherFrameRate) + <= FRAME_RATE_TOLERANCE; + } + + // Waits until our SurfaceHolder has a surface and the activity is resumed. + private void waitForPreconditions() throws InterruptedException { + assertNotSame( + "Activity was unexpectedly destroyed", mActivityState, ActivityState.DESTROYED); + if (mSurface == null || mActivityState != ActivityState.RUNNING) { + Log.i(TAG, + String.format( + "Waiting for preconditions. Have surface? %b. Activity resumed? %b.", + mSurface != null, mActivityState == ActivityState.RUNNING)); + } + long nowNanos = System.nanoTime(); + long endTimeNanos = nowNanos + PRECONDITION_WAIT_TIMEOUT_SECONDS * 1_000_000_000L; + while (mSurface == null || mActivityState != ActivityState.RUNNING) { + long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000; + assertTrue(String.format("Timed out waiting for preconditions. Have surface? %b." + + " Activity resumed? %b.", + mSurface != null, mActivityState == ActivityState.RUNNING), + timeRemainingMillis > 0); + mLock.wait(timeRemainingMillis); + assertNotSame( + "Activity was unexpectedly destroyed", mActivityState, ActivityState.DESTROYED); + nowNanos = System.nanoTime(); + } + // Make sure any previous mode changes are completed. + waitForStableFrameRate(); + } + + // Returns true if we encounter a precondition violation, false otherwise. + private boolean waitForPreconditionViolation() throws InterruptedException { + assertNotSame( + "Activity was unexpectedly destroyed", mActivityState, ActivityState.DESTROYED); + long nowNanos = System.nanoTime(); + long endTimeNanos = nowNanos + PRECONDITION_VIOLATION_WAIT_TIMEOUT_SECONDS * 1_000_000_000L; + while (mSurface != null && mActivityState == ActivityState.RUNNING) { + long timeRemainingMillis = (endTimeNanos - nowNanos) / 1_000_000; + if (timeRemainingMillis <= 0) { + break; + } + mLock.wait(timeRemainingMillis); + assertNotSame( + "Activity was unexpectedly destroyed", mActivityState, ActivityState.DESTROYED); + nowNanos = System.nanoTime(); + } + return mSurface == null || mActivityState != ActivityState.RUNNING; + } + + private void verifyPreconditions() { + if (mSurface == null || mActivityState != ActivityState.RUNNING) { + throw new PreconditionViolatedException(); + } + } + + // Returns true if we reached waitUntilNanos, false if some other event occurred. + private boolean waitForEvents(long waitUntilNanos, TestSurface[] surfaces) + throws InterruptedException { + int numModeChangedEvents = mModeChangedEvents.size(); + long nowNanos = System.nanoTime(); + while (nowNanos < waitUntilNanos) { + long surfacePostTime = Long.MAX_VALUE; + for (TestSurface surface : surfaces) { + surfacePostTime = Math.min(surfacePostTime, + surface.getLastBufferPostTime() + + (POST_BUFFER_INTERVAL_MILLIS * 1_000_000L)); + } + long timeoutNs = Math.min(waitUntilNanos, surfacePostTime) - nowNanos; + long timeoutMs = timeoutNs / 1_000_000L; + int remainderNs = (int) (timeoutNs % 1_000_000L); + // Don't call wait(0, 0) - it blocks indefinitely. + if (timeoutMs > 0 || remainderNs > 0) { + mLock.wait(timeoutMs, remainderNs); + } + nowNanos = System.nanoTime(); + verifyPreconditions(); + if (mModeChangedEvents.size() > numModeChangedEvents) { + return false; + } + if (nowNanos >= surfacePostTime) { + for (TestSurface surface : surfaces) { + surface.postBuffer(); + } + } + } + return true; + } + + private void waitForStableFrameRate(TestSurface... surfaces) throws InterruptedException { + verifyCompatibleAndStableFrameRate(0, surfaces); + } + + // Set expectedFrameRate to 0.0 to verify only stable frame rate. + private void verifyCompatibleAndStableFrameRate( + float expectedFrameRate, TestSurface... surfaces) throws InterruptedException { + Log.i(TAG, "Verifying compatible and stable frame rate"); + long nowNanos = System.nanoTime(); + long gracePeriodEndTimeNanos = + nowNanos + FRAME_RATE_SWITCH_GRACE_PERIOD_SECONDS * 1_000_000_000L; + while (true) { + if (expectedFrameRate > FRAME_RATE_TOLERANCE) { // expectedFrameRate > 0 + // Wait until we switch to a compatible frame rate. + while (!isFrameRateMultiple(mDeviceFrameRate, expectedFrameRate) + && !waitForEvents(gracePeriodEndTimeNanos, surfaces)) { + // Empty + } + nowNanos = System.nanoTime(); + if (nowNanos >= gracePeriodEndTimeNanos) { + throw new FrameRateTimeoutException(expectedFrameRate, mDeviceFrameRate); + } + } + + // We've switched to a compatible frame rate. Now wait for a while to see if we stay at + // that frame rate. + long endTimeNanos = nowNanos + STABLE_FRAME_RATE_WAIT_SECONDS * 1_000_000_000L; + while (endTimeNanos > nowNanos) { + int numModeChangedEvents = mModeChangedEvents.size(); + if (waitForEvents(endTimeNanos, surfaces)) { + Log.i(TAG, String.format("Stable frame rate %.2f verified", mDeviceFrameRate)); + return; + } + nowNanos = System.nanoTime(); + if (mModeChangedEvents.size() > numModeChangedEvents) { + break; + } + } + } + } + + private void verifyModeSwitchesDontChangeResolution(int fromId, int toId) { + assertTrue(fromId <= toId); + for (int eventId = fromId; eventId < toId; eventId++) { + Display.Mode fromMode = mModeChangedEvents.get(eventId - 1); + Display.Mode toMode = mModeChangedEvents.get(eventId); + assertTrue("Resolution change was not expected, but there was such from " + fromMode + + " to " + toMode + ".", + hasSameResolution(fromMode, toMode)); + } + } + + // Unfortunately, we can't just use Consumer<Api> for this, because we need to declare that it + // throws InterruptedException. + private interface TestInterface { + void run() throws InterruptedException; + } + + private interface OneSurfaceTestInterface { + void run(TestSurface surface) throws InterruptedException; + } + + // Runs the given test for each api, waiting for the preconditions to be satisfied before + // running the test. Includes retry logic when the test fails because the preconditions are + // violated. E.g. if we lose the SurfaceHolder's surface, or the activity is paused/resumed, + // we'll retry the test. The activity being intermittently paused/resumed has been observed to + // cause test failures in practice. + private void runTestsWithPreconditions(TestInterface test, String testName) + throws InterruptedException { + synchronized (mLock) { + for (Api api : Api.values()) { + Log.i(TAG, String.format("Testing %s %s", api, testName)); + int attempts = 0; + boolean testPassed = false; + try { + while (!testPassed) { + waitForPreconditions(); + try { + test.run(); + testPassed = true; + } catch (PreconditionViolatedException exc) { + // The logic below will retry if we're below max attempts. + } catch (FrameRateTimeoutException exc) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + exc.printStackTrace(printWriter); + String stackTrace = stringWriter.toString(); + + // Sometimes we get a test timeout failure before we get the + // notification that the activity was paused, and it was the pause that + // caused the timeout failure. Wait for a bit to see if we get notified + // of a precondition violation, and if so, retry the test. Otherwise + // fail. + assertTrue(String.format( + "Timed out waiting for a stable and compatible frame" + + " rate. expected=%.2f received=%.2f." + + " Stack trace: " + stackTrace, + exc.expectedFrameRate, exc.deviceFrameRate), + waitForPreconditionViolation()); + } + + if (!testPassed) { + Log.i(TAG, + String.format("Preconditions violated while running the test." + + " Have surface? %b. Activity resumed? %b.", + mSurface != null, + mActivityState == ActivityState.RUNNING)); + attempts++; + assertTrue(String.format( + "Exceeded %d precondition wait attempts. Giving up.", + PRECONDITION_WAIT_MAX_ATTEMPTS), + attempts < PRECONDITION_WAIT_MAX_ATTEMPTS); + } + } + } finally { + String passFailMessage = String.format( + "%s %s %s", testPassed ? "Passed" : "Failed", api, testName); + if (testPassed) { + Log.i(TAG, passFailMessage); + } else { + Log.e(TAG, passFailMessage); + } + } + } + } + } + + private void runOneSurfaceTest(OneSurfaceTestInterface test) throws InterruptedException { + TestSurface surface = null; + try { + surface = new TestSurface(mSurfaceView.getSurfaceControl(), mSurface, "testSurface", + mSurfaceView.getHolder().getSurfaceFrame(), + /*visible=*/true, Color.RED); + + test.run(surface); + } finally { + if (surface != null) { + surface.release(); + } + } + } + + private void testSurfaceControlFrameRateCategoryInternal(int category) + throws InterruptedException { + runOneSurfaceTest((TestSurface surface) -> { + Log.i(TAG, "**** Running testSurfaceControlFrameRateCategory for category " + category); + + float expectedFrameRate = getExpectedFrameRate(category); + int initialNumEvents = mModeChangedEvents.size(); + surface.setFrameRateCategory(category); + verifyCompatibleAndStableFrameRate(expectedFrameRate, surface); + verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size()); + }); + } + + public void testSurfaceControlFrameRateCategory(int category) throws InterruptedException { + runTestsWithPreconditions(() + -> testSurfaceControlFrameRateCategoryInternal(category), + "frame rate category=" + category); + } + + private float getExpectedFrameRate(int category) { + Display display = getDisplay(); + List<Float> frameRates = getRefreshRates(display.getMode(), display); + + if (category == Surface.FRAME_RATE_CATEGORY_DEFAULT + || category == Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE) { + return Collections.min(frameRates); + } + + FpsRange categoryRange = convertCategory(category); + Optional<Float> expectedFrameRate = frameRates.stream() + .filter(fps -> categoryRange.includes(fps)) + .min(Comparator.naturalOrder()); + assumeTrue("**** testSurfaceControlFrameRateCategory SKIPPED for category " + category, + expectedFrameRate.isPresent()); + return expectedFrameRate.get(); + } + + private FpsRange convertCategory(int category) { + switch (category) { + case Surface.FRAME_RATE_CATEGORY_HIGH: + return FRAME_RATE_CATEGORY_HIGH; + case Surface.FRAME_RATE_CATEGORY_NORMAL: + return FRAME_RATE_CATEGORY_NORMAL; + case Surface.FRAME_RATE_CATEGORY_LOW: + return FRAME_RATE_CATEGORY_LOW; + case Surface.FRAME_RATE_CATEGORY_DEFAULT: + case Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE: + fail("Should not get range for category=" + category); + } + return new FpsRange(0, 0); + } +} diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java new file mode 100644 index 000000000000..ccbda7f2fdf0 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 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 android.view.surfacecontroltests; + +import static org.junit.Assume.assumeTrue; + +import android.Manifest; +import android.hardware.display.DisplayManager; +import android.os.SystemProperties; +import android.support.test.uiautomator.UiDevice; +import android.view.Surface; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SurfaceControlTest { + private static final String TAG = "SurfaceControlTest"; + + @Rule + public ActivityTestRule<GraphicsActivity> mActivityRule = + new ActivityTestRule<>(GraphicsActivity.class); + + private int mInitialRefreshRateSwitchingType; + private DisplayManager mDisplayManager; + + @Before + public void setUp() throws Exception { + GraphicsActivity activity = mActivityRule.getActivity(); + + UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + // TODO(b/290634611): clean this up once SF new front end is enabled by default + assumeTrue(SystemProperties.getBoolean( + "persist.debug.sf.enable_layer_lifecycle_manager", false)); + + uiDevice.wakeUp(); + uiDevice.executeShellCommand("wm dismiss-keyguard"); + + InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.LOG_COMPAT_CHANGE, + Manifest.permission.READ_COMPAT_CHANGE_CONFIG, + Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE, + Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS, + Manifest.permission.MANAGE_GAME_MODE); + + // Prevent DisplayManager from limiting the allowed refresh rate range based on + // non-app policies (e.g. low battery, user settings, etc). + mDisplayManager = activity.getSystemService(DisplayManager.class); + mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true); + + mInitialRefreshRateSwitchingType = + toSwitchingType(mDisplayManager.getMatchContentFrameRateUserPreference()); + mDisplayManager.setRefreshRateSwitchingType( + DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS); + } + + @After + public void tearDown() { + if (mDisplayManager != null) { + mDisplayManager.setRefreshRateSwitchingType(mInitialRefreshRateSwitchingType); + mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false); + } + + InstrumentationRegistry.getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + + @Test + public void testSurfaceControlFrameRateCategoryHigh() throws InterruptedException { + GraphicsActivity activity = mActivityRule.getActivity(); + activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_HIGH); + } + + @Test + public void testSurfaceControlFrameRateCategoryNormal() throws InterruptedException { + GraphicsActivity activity = mActivityRule.getActivity(); + activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_NORMAL); + } + + @Test + public void testSurfaceControlFrameRateCategoryLow() throws InterruptedException { + GraphicsActivity activity = mActivityRule.getActivity(); + activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_LOW); + } + + @Test + public void testSurfaceControlFrameRateCategoryNoPreference() throws InterruptedException { + GraphicsActivity activity = mActivityRule.getActivity(); + activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE); + } + + @Test + public void testSurfaceControlFrameRateCategoryDefault() throws InterruptedException { + GraphicsActivity activity = mActivityRule.getActivity(); + activity.testSurfaceControlFrameRateCategory(Surface.FRAME_RATE_CATEGORY_DEFAULT); + } + + private int toSwitchingType(int matchContentFrameRateUserPreference) { + switch (matchContentFrameRateUserPreference) { + case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER: + return DisplayManager.SWITCHING_TYPE_NONE; + case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY: + return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS; + case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS: + return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS; + default: + return -1; + } + } +} diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/activity_surface_control.xml b/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/activity_surface_control.xml new file mode 100644 index 000000000000..d6c821228c9a --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/activity_surface_control.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 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. + --> + +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <SurfaceView + android:id="@+id/surface" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent"/> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-hdpi/ic_launcher.png b/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..cde69bcccec6 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-mdpi/ic_launcher.png b/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..c133a0cbd379 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-xhdpi/ic_launcher.png b/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..bfa42f0e7b91 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-xxhdpi/ic_launcher.png b/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000000..324e72cdd748 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/tests/FixVibrateSetting/res/values/strings.xml b/tests/CtsSurfaceControlTestsStaging/src/main/res/values/strings.xml index 269cef3a715b..364a5c3d94fe 100644 --- a/tests/FixVibrateSetting/res/values/strings.xml +++ b/tests/CtsSurfaceControlTestsStaging/src/main/res/values/strings.xml @@ -1,25 +1,18 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2007 The Android Open Source Project +<!-- Copyright 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. --> - <resources> - <string name="app_label">Fix Vibrate</string> - <string name="title">Fix vibrate setting</string> - <string name="current_setting">"Ringer: %1$s\nNotification: %2$s"</string> - <string name="fix">Fix the setting</string> - <string name="unfix">Break the setting</string> - <string name="test">Test the setting</string> + <string name="app_name">SurfaceControlTests</string> </resources> - diff --git a/tests/EnforcePermission/aidl/android/tests/enforcepermission/IProtected.aidl b/tests/EnforcePermission/aidl/android/tests/enforcepermission/IProtected.aidl index 18e3aecfa832..6e59b042d6ff 100644 --- a/tests/EnforcePermission/aidl/android/tests/enforcepermission/IProtected.aidl +++ b/tests/EnforcePermission/aidl/android/tests/enforcepermission/IProtected.aidl @@ -31,4 +31,31 @@ interface IProtected { @EnforcePermission("INTERNET") void ProtectedByInternetAndReadSyncSettingsImplicitly(); + + @EnforcePermission("TURN_SCREEN_ON") + void ProtectedByTurnScreenOn(); + + @EnforcePermission("READ_CONTACTS") + void ProtectedByReadContacts(); + + @EnforcePermission("READ_CALENDAR") + void ProtectedByReadCalendar(); + + @EnforcePermission(allOf={"INTERNET", "VIBRATE"}) + void ProtectedByInternetAndVibrate(); + + @EnforcePermission(allOf={"INTERNET", "READ_SYNC_SETTINGS"}) + void ProtectedByInternetAndReadSyncSettings(); + + @EnforcePermission(anyOf={"ACCESS_WIFI_STATE", "VIBRATE"}) + void ProtectedByAccessWifiStateOrVibrate(); + + @EnforcePermission(anyOf={"INTERNET", "VIBRATE"}) + void ProtectedByInternetOrVibrate(); + + @RequiresNoPermission + void NotProtected(); + + @PermissionManuallyEnforced + void ManuallyProtected(); } diff --git a/tests/EnforcePermission/perf-app/Android.bp b/tests/EnforcePermission/perf-app/Android.bp new file mode 100644 index 000000000000..b494bb754370 --- /dev/null +++ b/tests/EnforcePermission/perf-app/Android.bp @@ -0,0 +1,45 @@ +// 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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "EnforcePermissionPerfTests", + srcs: [ + "src/**/*.java", + ":frameworks-enforce-permission-test-aidl", + ], + static_libs: [ + "EnforcePermissionTestLib", + "androidx.benchmark_benchmark-common", + "androidx.benchmark_benchmark-junit4", + "apct-perftests-utils", + "collector-device-lib", + "androidx.test.rules", + ], + libs: [ + "android.test.base", + "android.test.runner", + ], + data: [ + ":EnforcePermissionTestHelper", + ":perfetto_artifacts", + "perfetto.textproto", + ], + platform_apis: true, + certificate: "platform", + test_suites: ["device-tests"], +} diff --git a/tests/EnforcePermission/perf-app/AndroidManifest.xml b/tests/EnforcePermission/perf-app/AndroidManifest.xml new file mode 100644 index 000000000000..900270d27304 --- /dev/null +++ b/tests/EnforcePermission/perf-app/AndroidManifest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.tests.enforcepermission.tests"> + + <uses-permission android:name="android.permission.INTERNET" /> + + <!-- Required by perfetto --> + <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + + <queries> + <package android:name="android.tests.enforcepermission.service" /> + </queries> + + <application> + <uses-library android:name="android.test.runner" /> + <profileable android:shell="true" /> + <!-- Instance of the Service within the app. This is to test performance for same-process calls. --> + <service android:name=".TestService" /> + </application> + <instrumentation android:name="androidx.benchmark.junit4.AndroidBenchmarkRunner" + android:targetPackage="android.tests.enforcepermission.tests"/> +</manifest> diff --git a/tests/EnforcePermission/perf-app/AndroidTest.xml b/tests/EnforcePermission/perf-app/AndroidTest.xml new file mode 100644 index 000000000000..3bc1d2dc2eeb --- /dev/null +++ b/tests/EnforcePermission/perf-app/AndroidTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Runs EnforcePermission Perf Tests"> + + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="push-file" key="perfetto.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="EnforcePermissionTestHelper.apk"/> + <option name="test-file-name" value="EnforcePermissionPerfTests.apk"/> + <option name="cleanup-apks" value="true" /> + </target_preparer> + + <option name="isolated-storage" value="false" /> + + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="pull-pattern-keys" value="perfetto_file_path" /> + <option name="collect-on-run-ended-only" value="false" /> + </metrics_collector> + + <option name="test-tag" value="EnforcePermissionTests"/> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="android.tests.enforcepermission.tests"/> + <option name="device-listeners" value="android.device.collectors.PerfettoListener" /> + <!-- PerfettoListener related arguments --> + <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" /> + <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" /> + </test> +</configuration> diff --git a/tests/EnforcePermission/perf-app/perfetto.textproto b/tests/EnforcePermission/perf-app/perfetto.textproto new file mode 100644 index 000000000000..8a3eea4ef402 --- /dev/null +++ b/tests/EnforcePermission/perf-app/perfetto.textproto @@ -0,0 +1,154 @@ +# 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. + +# Based on trace_config_detailed.textproto +# proto-message: TraceConfig + +# Enable periodic flushing of the trace buffer into the output file. +write_into_file: true + +# Writes the userspace buffer into the file every 1s. +file_write_period_ms: 1000 + +# See b/126487238 - we need to guarantee ordering of events. +flush_period_ms: 10000 + +# The trace buffers needs to be big enough to hold |file_write_period_ms| of +# trace data. The trace buffer sizing depends on the number of trace categories +# enabled and the device activity. + +# RSS events +buffers { + size_kb: 32768 + fill_policy: RING_BUFFER +} + +# procfs polling +buffers { + size_kb: 8192 + fill_policy: RING_BUFFER +} + +# perf memory +buffers { + size_kb: 65536 + fill_policy: RING_BUFFER +} + +data_sources { + config { + name: "linux.ftrace" + target_buffer: 0 + ftrace_config { + throttle_rss_stat: true + # These parameters affect only the kernel trace buffer size and how + # frequently it gets moved into the userspace buffer defined above. + buffer_size_kb: 16384 + drain_period_ms: 250 + + # Store certain high-volume "sched" ftrace events in a denser format + # (falling back to the default format if not supported by the tracer). + compact_sched { + enabled: true + } + + # Enables symbol name resolution against /proc/kallsyms + symbolize_ksyms: true + # Parse kallsyms before acknowledging that the ftrace data source has been started. In + # combination with "perfetto --background-wait" as the consumer, it lets us defer the + # test we're tracing until after the cpu has quieted down from the cpu-bound kallsyms parsing. + initialize_ksyms_synchronously_for_testing: true + # Avoid re-parsing kallsyms on every test run, as it takes 200-500ms per run. See b/239951079 + ksyms_mem_policy: KSYMS_RETAIN + + # We need to do process tracking to ensure kernel ftrace events targeted at short-lived + # threads are associated correctly + ftrace_events: "task/task_newtask" + ftrace_events: "task/task_rename" + ftrace_events: "sched/sched_process_exit" + ftrace_events: "sched/sched_process_free" + + # Memory events + ftrace_events: "rss_stat" + ftrace_events: "ion_heap_shrink" + ftrace_events: "ion_heap_grow" + ftrace_events: "ion/ion_stat" + ftrace_events: "dmabuf_heap/dma_heap_stat" + ftrace_events: "oom_score_adj_update" + ftrace_events: "gpu_mem/gpu_mem_total" + ftrace_events: "fastrpc/fastrpc_dma_stat" + + # Power events + ftrace_events: "power/suspend_resume" + ftrace_events: "power/cpu_frequency" + ftrace_events: "power/cpu_idle" + ftrace_events: "power/gpu_frequency" + + # Old (kernel) LMK + ftrace_events: "lowmemorykiller/lowmemory_kill" + + atrace_apps: "*" + + atrace_categories: "am" + atrace_categories: "aidl" + atrace_categories: "bionic" + atrace_categories: "camera" + atrace_categories: "wm" + atrace_categories: "dalvik" + atrace_categories: "sched" + atrace_categories: "freq" + atrace_categories: "gfx" + atrace_categories: "view" + atrace_categories: "webview" + atrace_categories: "input" + atrace_categories: "hal" + atrace_categories: "binder_driver" + atrace_categories: "sync" + atrace_categories: "workq" + atrace_categories: "res" + atrace_categories: "power" + + } + } +} + +data_sources { + config { + name: "linux.process_stats" + target_buffer: 1 + process_stats_config { + proc_stats_poll_ms: 10000 + } + } +} + +data_sources { + config { + name: "linux.perf" + target_buffer: 2 + perf_event_config { + timebase { + frequency: 80 + } + callstack_sampling { + scope { + target_cmdline: "android.tests.enforcepermission.tests" + target_cmdline: "android.tests.enforcepermission.service" + target_cmdline: "system_server" + } + kernel_frames: true + } + } + } +} diff --git a/tests/EnforcePermission/perf-app/src/android/tests/enforcepermission/tests/ServicePerfTest.java b/tests/EnforcePermission/perf-app/src/android/tests/enforcepermission/tests/ServicePerfTest.java new file mode 100644 index 000000000000..7cbf5674c29a --- /dev/null +++ b/tests/EnforcePermission/perf-app/src/android/tests/enforcepermission/tests/ServicePerfTest.java @@ -0,0 +1,169 @@ +/** + * 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 android.tests.enforcepermission.tests; + +import static org.junit.Assert.assertTrue; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.perftests.utils.BenchmarkState; +import android.perftests.utils.PerfStatusReporter; +import android.tests.enforcepermission.IProtected; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** Performance tests for EnforcePermission annotation. + * + * Permission check results are cached on the service side as it relies on + * PermissionManager. It means that only the first request will trigger a + * lookup to system_server. Subsequent requests will use the cached result. As + * this timing is similar to a permission check for a service hosted in + * system_server, we keep this cache active for the tests. The BenchmarkState + * used by PerfStatusReporter includes a warm-up stage. It means that the extra + * time taken by the first request will not be reflected in the outcome of the + * test. + */ +@RunWith(AndroidJUnit4.class) +public class ServicePerfTest { + + private static final String TAG = "EnforcePermission.PerfTests"; + private static final String SERVICE_PACKAGE = "android.tests.enforcepermission.service"; + private static final String LOCAL_SERVICE_PACKAGE = "android.tests.enforcepermission.tests"; + private static final int SERVICE_TIMEOUT_SEC = 5; + + @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); + + private Context mContext; + private volatile ServiceConnection mServiceConnection; + + private void bindService(Intent intent) throws Exception { + mContext = InstrumentationRegistry.getTargetContext(); + mServiceConnection = new ServiceConnection(); + assertTrue(mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)); + } + + public void bindRemoteService() throws Exception { + Log.d(TAG, "bindRemoteService"); + Intent intent = new Intent(); + intent.setClassName(SERVICE_PACKAGE, SERVICE_PACKAGE + ".TestService"); + bindService(intent); + } + + public void bindLocalService() throws Exception { + Log.d(TAG, "bindLocalService"); + Intent intent = new Intent(); + intent.setClassName(LOCAL_SERVICE_PACKAGE, SERVICE_PACKAGE + ".TestService"); + bindService(intent); + } + + @After + public void unbindTestService() throws Exception { + mContext.unbindService(mServiceConnection); + } + + private static final class ServiceConnection implements android.content.ServiceConnection { + private volatile CompletableFuture<IProtected> mFuture = new CompletableFuture<>(); + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + mFuture.complete(IProtected.Stub.asInterface(service)); + } + + @Override + public void onServiceDisconnected(ComponentName className) { + mFuture = new CompletableFuture<>(); + } + + public IProtected get() { + try { + return mFuture.get(SERVICE_TIMEOUT_SEC, TimeUnit.SECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + throw new RuntimeException("Unable to reach TestService: " + e.toString()); + } + } + } + + @Test + public void testAnnotatedPermission() throws Exception { + bindRemoteService(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mServiceConnection.get().ProtectedByInternet(); + } + } + + @Test + public void testNoPermission() throws Exception { + bindRemoteService(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mServiceConnection.get().NotProtected(); + } + } + + @Test + public void testManuallyProtected() throws Exception { + bindRemoteService(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mServiceConnection.get().ManuallyProtected(); + } + } + + @Test + public void testAnnotatedPermissionLocal() + throws Exception { + bindLocalService(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mServiceConnection.get().ProtectedByInternet(); + } + } + + @Test + public void testNoPermissionLocal() throws Exception { + bindLocalService(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mServiceConnection.get().NotProtected(); + } + } + + @Test + public void testManuallyProtectedLocal() throws Exception { + bindLocalService(); + final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + while (state.keepRunning()) { + mServiceConnection.get().ManuallyProtected(); + } + } +} diff --git a/tests/EnforcePermission/service-app/Android.bp b/tests/EnforcePermission/service-app/Android.bp index a4ac1d7c6134..787821546018 100644 --- a/tests/EnforcePermission/service-app/Android.bp +++ b/tests/EnforcePermission/service-app/Android.bp @@ -21,6 +21,14 @@ package { default_applicable_licenses: ["frameworks_base_license"], } +android_library { + name: "EnforcePermissionTestLib", + srcs: [ + "src/**/*.java", + ":frameworks-enforce-permission-test-aidl", + ], +} + android_test_helper_app { name: "EnforcePermissionTestHelper", srcs: [ diff --git a/tests/EnforcePermission/service-app/AndroidManifest.xml b/tests/EnforcePermission/service-app/AndroidManifest.xml index ddafe15ab88f..eba1230ff7a2 100644 --- a/tests/EnforcePermission/service-app/AndroidManifest.xml +++ b/tests/EnforcePermission/service-app/AndroidManifest.xml @@ -15,6 +15,9 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.tests.enforcepermission.service"> + + <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" /> + <application> <service android:name=".TestService" diff --git a/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/NestedTestService.java b/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/NestedTestService.java index 7879a1214c01..0f083c994738 100644 --- a/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/NestedTestService.java +++ b/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/NestedTestService.java @@ -18,13 +18,21 @@ package android.tests.enforcepermission.service; import android.annotation.EnforcePermission; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.os.IBinder; +import android.os.PermissionEnforcer; import android.tests.enforcepermission.INested; import android.util.Log; public class NestedTestService extends Service { private static final String TAG = "EnforcePermission.NestedTestService"; + private INested.Stub mBinder; + + @Override + public void onCreate() { + mBinder = new Stub(this); + } @Override public IBinder onBind(Intent intent) { @@ -32,7 +40,12 @@ public class NestedTestService extends Service { return mBinder; } - private final INested.Stub mBinder = new INested.Stub() { + private static class Stub extends INested.Stub { + + Stub(Context context) { + super(PermissionEnforcer.fromContext(context)); + } + @Override @EnforcePermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void ProtectedByAccessNetworkState() { @@ -44,5 +57,5 @@ public class NestedTestService extends Service { public void ProtectedByReadSyncSettings() { ProtectedByReadSyncSettings_enforcePermission(); } - }; + } } diff --git a/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/TestService.java b/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/TestService.java index e9b897db1294..8b809cf41c3e 100644 --- a/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/TestService.java +++ b/tests/EnforcePermission/service-app/src/android/tests/enforcepermission/service/TestService.java @@ -17,11 +17,13 @@ package android.tests.enforcepermission.service; import android.annotation.EnforcePermission; +import android.annotation.RequiresNoPermission; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.IBinder; +import android.os.PermissionEnforcer; import android.os.RemoteException; import android.tests.enforcepermission.INested; import android.tests.enforcepermission.IProtected; @@ -36,9 +38,11 @@ public class TestService extends Service { private static final String TAG = "EnforcePermission.TestService"; private volatile ServiceConnection mNestedServiceConnection; + private IProtected.Stub mBinder; @Override public void onCreate() { + mBinder = new Stub(this); mNestedServiceConnection = new ServiceConnection(); Intent intent = new Intent(this, NestedTestService.class); boolean bound = bindService(intent, mNestedServiceConnection, Context.BIND_AUTO_CREATE); @@ -78,7 +82,12 @@ public class TestService extends Service { return mBinder; } - private final IProtected.Stub mBinder = new IProtected.Stub() { + private class Stub extends IProtected.Stub { + + Stub(Context context) { + super(PermissionEnforcer.fromContext(context)); + } + @Override @EnforcePermission(android.Manifest.permission.INTERNET) public void ProtectedByInternet() { @@ -105,7 +114,6 @@ public class TestService extends Service { ProtectedByInternetAndAccessNetworkStateImplicitly_enforcePermission(); mNestedServiceConnection.get().ProtectedByAccessNetworkState(); - } @Override @@ -115,5 +123,65 @@ public class TestService extends Service { mNestedServiceConnection.get().ProtectedByReadSyncSettings(); } - }; + + @Override + @EnforcePermission(android.Manifest.permission.TURN_SCREEN_ON) + public void ProtectedByTurnScreenOn() { + ProtectedByTurnScreenOn_enforcePermission(); + } + + @Override + @EnforcePermission(android.Manifest.permission.READ_CONTACTS) + public void ProtectedByReadContacts() { + ProtectedByReadContacts_enforcePermission(); + } + + @Override + @EnforcePermission(android.Manifest.permission.READ_CALENDAR) + public void ProtectedByReadCalendar() { + ProtectedByReadCalendar_enforcePermission(); + } + + @Override + @EnforcePermission(allOf = { + android.Manifest.permission.INTERNET, + android.Manifest.permission.VIBRATE}) + public void ProtectedByInternetAndVibrate() { + ProtectedByInternetAndVibrate_enforcePermission(); + } + + @Override + @EnforcePermission(allOf = { + android.Manifest.permission.INTERNET, + android.Manifest.permission.READ_SYNC_SETTINGS}) + public void ProtectedByInternetAndReadSyncSettings() { + ProtectedByInternetAndReadSyncSettings_enforcePermission(); + } + + @Override + @EnforcePermission(anyOf = { + android.Manifest.permission.ACCESS_WIFI_STATE, + android.Manifest.permission.VIBRATE}) + public void ProtectedByAccessWifiStateOrVibrate() { + ProtectedByAccessWifiStateOrVibrate_enforcePermission(); + } + + @Override + @EnforcePermission(anyOf = { + android.Manifest.permission.INTERNET, + android.Manifest.permission.VIBRATE}) + public void ProtectedByInternetOrVibrate() { + ProtectedByInternetOrVibrate_enforcePermission(); + } + + @Override + @RequiresNoPermission + public void NotProtected() { + } + + @Override + public void ManuallyProtected() { + enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "access denied"); + } + } } diff --git a/tests/EnforcePermission/test-app/AndroidManifest.xml b/tests/EnforcePermission/test-app/AndroidManifest.xml index 4a0c6a86628f..8bd05d7350e5 100644 --- a/tests/EnforcePermission/test-app/AndroidManifest.xml +++ b/tests/EnforcePermission/test-app/AndroidManifest.xml @@ -16,9 +16,20 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.tests.enforcepermission.tests"> - <!-- Expected for the tests (not actually used) --> + <!-- Expected permissions for the tests (not actually used). These + are granted automatically at runtime by Tradefed (see + GrantPermissionPreparer). --> + <!-- normal --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> + <!-- normal|appops --> + <uses-permission android:name="android.permission.TURN_SCREEN_ON" /> + <!-- dangerous --> + <uses-permission android:name="android.permission.READ_CONTACTS" /> + + <!-- Used by the tests to activate/deactivate AppOps --> + <uses-permission android:name="android.permission.MANAGE_APP_OPS_MODES" /> + <uses-permission android:name="android.permission.MANAGE_APPOPS" /> <queries> <package android:name="android.tests.enforcepermission.service" /> diff --git a/tests/EnforcePermission/test-app/src/android/tests/enforcepermission/tests/ServiceTest.java b/tests/EnforcePermission/test-app/src/android/tests/enforcepermission/tests/ServiceTest.java index d2a4a037f125..e09097cd2a04 100644 --- a/tests/EnforcePermission/test-app/src/android/tests/enforcepermission/tests/ServiceTest.java +++ b/tests/EnforcePermission/test-app/src/android/tests/enforcepermission/tests/ServiceTest.java @@ -21,11 +21,13 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import android.app.AppOpsManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; +import android.os.Process; import android.os.RemoteException; import android.tests.enforcepermission.IProtected; import android.util.Log; @@ -126,4 +128,61 @@ public class ServiceTest { throws RemoteException { mServiceConnection.get().ProtectedByInternetAndReadSyncSettingsImplicitly(); } + + @Test + public void testAppOpPermissionGranted_succeeds() throws RemoteException { + AppOpsManager appOpsManager = mContext.getSystemService(AppOpsManager.class); + appOpsManager.setUidMode(AppOpsManager.OP_TURN_SCREEN_ON, + Process.myUid(), AppOpsManager.MODE_ALLOWED); + + mServiceConnection.get().ProtectedByTurnScreenOn(); + } + + @Test + public void testAppOpPermissionDenied_fails() throws RemoteException { + AppOpsManager appOpsManager = mContext.getSystemService(AppOpsManager.class); + appOpsManager.setUidMode(AppOpsManager.OP_TURN_SCREEN_ON, + Process.myUid(), AppOpsManager.MODE_ERRORED); + + final Exception ex = assertThrows(SecurityException.class, + () -> mServiceConnection.get().ProtectedByTurnScreenOn()); + assertThat(ex.getMessage(), containsString("TURN_SCREEN_ON")); + } + + @Test + public void testRuntimePermissionGranted_succeeds() throws RemoteException { + mServiceConnection.get().ProtectedByReadContacts(); + } + + @Test + public void testRuntimePermissionDenied_fails() throws RemoteException { + final Exception ex = assertThrows(SecurityException.class, + () -> mServiceConnection.get().ProtectedByReadCalendar()); + assertThat(ex.getMessage(), containsString("READ_CALENDAR")); + } + + @Test + public void testAllOfPermissionGranted_succeeds() throws RemoteException { + mServiceConnection.get().ProtectedByInternetAndReadSyncSettings(); + } + + @Test + public void testAllOfPermissionDenied_fails() throws RemoteException { + final Exception ex = assertThrows(SecurityException.class, + () -> mServiceConnection.get().ProtectedByInternetAndVibrate()); + assertThat(ex.getMessage(), containsString("VIBRATE")); + } + + @Test + public void testAnyOfPermissionGranted_succeeds() throws RemoteException { + mServiceConnection.get().ProtectedByInternetOrVibrate(); + } + + @Test + public void testAnyOfPermissionDenied_fails() throws RemoteException { + final Exception ex = assertThrows(SecurityException.class, + () -> mServiceConnection.get().ProtectedByAccessWifiStateOrVibrate()); + assertThat(ex.getMessage(), containsString("VIBRATE")); + assertThat(ex.getMessage(), containsString("ACCESS_WIFI_STATE")); + } } diff --git a/tests/FixVibrateSetting/Android.bp b/tests/FixVibrateSetting/Android.bp deleted file mode 100644 index bd7c701026ba..000000000000 --- a/tests/FixVibrateSetting/Android.bp +++ /dev/null @@ -1,15 +0,0 @@ -package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], -} - -android_app { - name: "FixVibrateSetting", - srcs: ["**/*.java"], - sdk_version: "current", - certificate: "platform", -} diff --git a/tests/FixVibrateSetting/AndroidManifest.xml b/tests/FixVibrateSetting/AndroidManifest.xml deleted file mode 100644 index c2d5918a4681..000000000000 --- a/tests/FixVibrateSetting/AndroidManifest.xml +++ /dev/null @@ -1,18 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.fixvibratesetting"> - <uses-permission android:name="android.permission.VIBRATE"/> - - <application> - <activity android:name="FixVibrateSetting" - android:label="@string/app_label" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MAIN"/> - <category android:name="android.intent.category.DEFAULT"/> - <category android:name="android.intent.category.LAUNCHER"/> - </intent-filter> - </activity> - </application> -</manifest> diff --git a/tests/FixVibrateSetting/OWNERS b/tests/FixVibrateSetting/OWNERS deleted file mode 100644 index cc63ceb2c7ad..000000000000 --- a/tests/FixVibrateSetting/OWNERS +++ /dev/null @@ -1 +0,0 @@ -include /services/core/java/com/android/server/vibrator/OWNERS diff --git a/tests/FixVibrateSetting/res/drawable-hdpi/stat_sys_warning.png b/tests/FixVibrateSetting/res/drawable-hdpi/stat_sys_warning.png Binary files differdeleted file mode 100644 index 37c8853a4694..000000000000 --- a/tests/FixVibrateSetting/res/drawable-hdpi/stat_sys_warning.png +++ /dev/null diff --git a/tests/FixVibrateSetting/res/drawable-mdpi/stat_sys_warning.png b/tests/FixVibrateSetting/res/drawable-mdpi/stat_sys_warning.png Binary files differdeleted file mode 100644 index be00f470ad6a..000000000000 --- a/tests/FixVibrateSetting/res/drawable-mdpi/stat_sys_warning.png +++ /dev/null diff --git a/tests/FixVibrateSetting/res/layout/fix_vibrate.xml b/tests/FixVibrateSetting/res/layout/fix_vibrate.xml deleted file mode 100644 index c505e653e550..000000000000 --- a/tests/FixVibrateSetting/res/layout/fix_vibrate.xml +++ /dev/null @@ -1,46 +0,0 @@ -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical" - > - - <TextView android:id="@+id/current_setting" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="left" - android:layout_marginTop="30dp" - android:layout_marginBottom="50dp" - android:paddingLeft="8dp" - android:paddingTop="4dp" - android:textSize="20sp" - android:textColor="#ffffffff" - /> - - <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="center" - android:orientation="horizontal" - > - <Button android:id="@+id/fix" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/fix" - /> - - <Button android:id="@+id/unfix" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/unfix" - /> - </LinearLayout> - - <Button android:id="@+id/test" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:text="@string/test" - /> - -</LinearLayout> - diff --git a/tests/FixVibrateSetting/src/com/android/fixvibratesetting/FixVibrateSetting.java b/tests/FixVibrateSetting/src/com/android/fixvibratesetting/FixVibrateSetting.java deleted file mode 100644 index 761efe4a8484..000000000000 --- a/tests/FixVibrateSetting/src/com/android/fixvibratesetting/FixVibrateSetting.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2008 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.fixvibratesetting; - -import android.app.Activity; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Intent; -import android.media.AudioManager; -import android.util.Log; -import android.view.View; -import android.widget.TextView; -import android.os.Bundle; - -public class FixVibrateSetting extends Activity implements View.OnClickListener -{ - AudioManager mAudioManager; - NotificationManager mNotificationManager; - TextView mCurrentSetting; - View mFix; - View mUnfix; - View mTest; - - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - setContentView(R.layout.fix_vibrate); - - mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE); - mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); - - mCurrentSetting = (TextView)findViewById(R.id.current_setting); - - mFix = findViewById(R.id.fix); - mFix.setOnClickListener(this); - - mUnfix = findViewById(R.id.unfix); - mUnfix.setOnClickListener(this); - - mTest = findViewById(R.id.test); - mTest.setOnClickListener(this); - } - - @Override - public void onResume() { - super.onResume(); - - update(); - } - - private String getSettingValue(int vibrateType) { - int setting = mAudioManager.getVibrateSetting(vibrateType); - switch (setting) { - case AudioManager.VIBRATE_SETTING_OFF: - return "off"; - case AudioManager.VIBRATE_SETTING_ON: - return "on"; - case AudioManager.VIBRATE_SETTING_ONLY_SILENT: - return "silent-only"; - default: - return "unknown"; - } - } - - public void onClick(View v) { - if (v == mFix) { - fix(); - update(); - } else if (v == mUnfix) { - unfix(); - update(); - } else if (v == mTest) { - test(); - update(); - } - } - - private void update() { - String ringer = getSettingValue(AudioManager.VIBRATE_TYPE_RINGER); - String notification = getSettingValue(AudioManager.VIBRATE_TYPE_NOTIFICATION); - String text = getString(R.string.current_setting, ringer, notification); - mCurrentSetting.setText(text); - } - - private void fix() { - mAudioManager.setVibrateSetting(AudioManager.VIBRATE_TYPE_NOTIFICATION, - AudioManager.VIBRATE_SETTING_ON); - } - - private void unfix() { - mAudioManager.setVibrateSetting(AudioManager.VIBRATE_TYPE_NOTIFICATION, - AudioManager.VIBRATE_SETTING_OFF); - } - - private void test() { - Intent intent = new Intent(this, FixVibrateSetting.class); - PendingIntent pending = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE); - - Notification n = new Notification.Builder(this) - .setSmallIcon(R.drawable.stat_sys_warning) - .setTicker("Test notification") - .setWhen(System.currentTimeMillis()) - .setContentTitle("Test notification") - .setContentText("Test notification") - .setContentIntent(pending) - .setVibrate(new long[] { 0, 700, 500, 1000 }) - .setAutoCancel(true) - .build(); - - mNotificationManager.notify(1, n); - } -} - diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp index 2ccc0fa9e1e7..a2ae56eaeadc 100644 --- a/tests/FlickerTests/Android.bp +++ b/tests/FlickerTests/Android.bp @@ -95,6 +95,7 @@ java_defaults { "flickertestapplib", "flickerlib", "flickerlib-helpers", + "flickerlib-trace_processor_shell", "platform-test-annotations", "wm-flicker-common-app-helpers", "wm-shell-flicker-utils", diff --git a/tests/FlickerTests/AndroidTestTemplate.xml b/tests/FlickerTests/AndroidTestTemplate.xml index 44a824513b91..878038557cf1 100644 --- a/tests/FlickerTests/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AndroidTestTemplate.xml @@ -61,7 +61,9 @@ <option name="shell-timeout" value="6600s"/> <option name="test-timeout" value="6600s"/> <option name="hidden-api-checks" value="false"/> + <!-- TODO(b/288396763): re-enable when PerfettoListener is fixed <option name="device-listeners" value="android.device.collectors.PerfettoListener"/> + --> <!-- PerfettoListener related arguments --> <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true"/> <option name="instrumentation-arg" diff --git a/tests/FlickerTests/manifests/AndroidManifest.xml b/tests/FlickerTests/manifests/AndroidManifest.xml index 1a34d9ea0f83..6bc7cbe88589 100644 --- a/tests/FlickerTests/manifests/AndroidManifest.xml +++ b/tests/FlickerTests/manifests/AndroidManifest.xml @@ -44,8 +44,12 @@ <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> <!-- ActivityOptions.makeCustomTaskAnimation() --> <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS" /> - <!-- Allow the test to write directly to /sdcard/ --> - <application android:requestLegacyExternalStorage="true" android:largeHeap="true"> + <!-- Allow the test to connect to perfetto trace processor --> + <uses-permission android:name="android.permission.INTERNET"/> + <!-- Allow the test to write directly to /sdcard/ and connect to trace processor --> + <application android:requestLegacyExternalStorage="true" + android:networkSecurityConfig="@xml/network_security_config" + android:largeHeap="true"> <uses-library android:name="android.test.runner"/> <uses-library android:name="androidx.window.extensions" android:required="false"/> diff --git a/tests/FlickerTests/res/xml/network_security_config.xml b/tests/FlickerTests/res/xml/network_security_config.xml new file mode 100644 index 000000000000..4bd9ca049f55 --- /dev/null +++ b/tests/FlickerTests/res/xml/network_security_config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<network-security-config> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">localhost</domain> + </domain-config> +</network-security-config> diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt index b18a1a84c61b..4032121d4211 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/CommonAssertions.kt @@ -19,6 +19,8 @@ package com.android.server.wm.flicker import android.tools.common.PlatformConsts +import android.tools.common.Position +import android.tools.common.flicker.subject.layers.LayerTraceEntrySubject import android.tools.common.flicker.subject.region.RegionSubject import android.tools.common.traces.component.ComponentNameMatcher import android.tools.common.traces.component.IComponentNameMatcher @@ -168,14 +170,7 @@ fun LegacyFlickerTest.statusBarLayerIsVisibleAtStartAndEnd() { * the SF trace */ fun LegacyFlickerTest.navBarLayerPositionAtStart() { - assertLayersStart { - val display = - this.entry.displays.firstOrNull { !it.isVirtual } ?: error("There is no display!") - this.visibleRegion(ComponentNameMatcher.NAV_BAR) - .coversExactly( - WindowUtils.getNavigationBarPosition(display, scenario.isGesturalNavigation) - ) - } + assertLayersStart { assertNavBarPosition(this, scenario.isGesturalNavigation) } } /** @@ -183,14 +178,39 @@ fun LegacyFlickerTest.navBarLayerPositionAtStart() { * the SF trace */ fun LegacyFlickerTest.navBarLayerPositionAtEnd() { - assertLayersEnd { - val display = - this.entry.displays.minByOrNull { it.id } - ?: throw RuntimeException("There is no display!") - this.visibleRegion(ComponentNameMatcher.NAV_BAR) - .coversExactly( - WindowUtils.getNavigationBarPosition(display, scenario.isGesturalNavigation) - ) + assertLayersEnd { assertNavBarPosition(this, scenario.isGesturalNavigation) } +} + +private fun assertNavBarPosition(sfState: LayerTraceEntrySubject, isGesturalNavigation: Boolean) { + val display = + sfState.entry.displays.filterNot { it.isOff }.minByOrNull { it.id } + ?: error("There is no display!") + val displayArea = display.layerStackSpace + val navBarPosition = display.navBarPosition(isGesturalNavigation) + val navBarRegion = sfState.visibleRegion(ComponentNameMatcher.NAV_BAR) + + when (navBarPosition) { + Position.TOP -> + navBarRegion + .hasSameTopPosition(displayArea) + .hasSameLeftPosition(displayArea) + .hasSameRightPosition(displayArea) + Position.BOTTOM -> + navBarRegion + .hasSameBottomPosition(displayArea) + .hasSameLeftPosition(displayArea) + .hasSameRightPosition(displayArea) + Position.LEFT -> + navBarRegion + .hasSameLeftPosition(displayArea) + .hasSameTopPosition(displayArea) + .hasSameBottomPosition(displayArea) + Position.RIGHT -> + navBarRegion + .hasSameRightPosition(displayArea) + .hasSameTopPosition(displayArea) + .hasSameBottomPosition(displayArea) + else -> error("Unknown position $navBarPosition") } } @@ -262,10 +282,10 @@ fun LegacyFlickerTest.snapshotStartingWindowLayerCoversExactlyOnApp( snapshotLayers .mapNotNull { snapshotLayer -> snapshotLayer.layer.visibleRegion } .toTypedArray() - val snapshotRegion = RegionSubject(visibleAreas, timestamp) + val snapshotRegion = RegionSubject(visibleAreas, it.timestamp) + val appVisibleRegion = it.visibleRegion(component) // Verify the size of snapshotRegion covers appVisibleRegion exactly in animation. - if (snapshotRegion.region.isNotEmpty) { - val appVisibleRegion = it.visibleRegion(component) + if (snapshotRegion.region.isNotEmpty && appVisibleRegion.region.isNotEmpty) { snapshotRegion.coversExactly(appVisibleRegion.region) } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt index 45176448a9f4..6209a0838d9b 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt @@ -16,9 +16,9 @@ package com.android.server.wm.flicker.activityembedding -import android.tools.device.flicker.legacy.LegacyFlickerTest import android.platform.test.annotations.Presubmit import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.device.flicker.legacy.LegacyFlickerTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper import org.junit.Before @@ -36,10 +36,8 @@ abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest( /** Asserts the background animation layer is never visible during bounds change transition. */ @Presubmit @Test - fun backgroundLayerNeverVisible() { + open fun backgroundLayerNeverVisible() { val backgroundColorLayer = ComponentNameMatcher("", "Animation Background") - flicker.assertLayers { - isInvisible(backgroundColorLayer) - } + flicker.assertLayers { isInvisible(backgroundColorLayer) } } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt index cfc0d8b5f33b..adff5792c42d 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt @@ -22,6 +22,7 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper import androidx.test.filters.RequiresDevice @@ -35,9 +36,8 @@ import org.junit.runners.Parameterized * Test changing split ratio at runtime on a horizona split. * * Setup: Launch A|B in horizontal split with B being the secondary activity, by default A and B - * windows are equal in size. B is on the top and A is on the bottom. - * Transitions: - * Change the split ratio to A:B=0.7:0.3, expect bounds change for both A and B. + * windows are equal in size. B is on the top and A is on the bottom. Transitions: Change the split + * ratio to A:B=0.7:0.3, expect bounds change for both A and B. * * To run this test: `atest FlickerTestsOther:HorizontalSplitChangeRatioTest` */ @@ -46,7 +46,7 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) : - ActivityEmbeddingTestBase(flicker) { + ActivityEmbeddingTestBase(flicker) { /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit = { setup { @@ -54,18 +54,24 @@ class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) : testApp.launchViaIntent(wmHelper) testApp.launchSecondaryActivityHorizontally(wmHelper) startDisplayBounds = - wmHelper.currentState.layerState.physicalDisplayBounds - ?: error("Display not found") - } - transitions { - testApp.changeSecondaryActivityRatio(wmHelper) + wmHelper.currentState.layerState.physicalDisplayBounds ?: error("Display not found") } + transitions { testApp.changeSecondaryActivityRatio(wmHelper) } teardown { tapl.goHome() testApp.exit(wmHelper) } } + @FlakyTest(bugId = 293075402) + @Test + override fun backgroundLayerNeverVisible() = super.backgroundLayerNeverVisible() + + @FlakyTest(bugId = 293075402) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + /** Assert the Main activity window is always visible. */ @Presubmit @Test @@ -85,7 +91,8 @@ class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) : @Test fun secondaryActivityWindowIsAlwaysVisible() { flicker.assertWm { - isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) } + isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + } } /** Assert the Secondary activity window is always visible. */ @@ -101,15 +108,17 @@ class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) : fun secondaryActivityAdjustsHeightRuntime() { flicker.assertLayersStart { val topLayerRegion = - this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) val bottomLayerRegion = - this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) // Compare dimensions of two splits, given we're using default split attributes, // both activities take up the same visible size on the display. check { "height" } - .that(topLayerRegion.region.height).isEqual(bottomLayerRegion.region.height) + .that(topLayerRegion.region.height) + .isEqual(bottomLayerRegion.region.height) check { "width" } - .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width) + .that(topLayerRegion.region.width) + .isEqual(bottomLayerRegion.region.width) topLayerRegion.notOverlaps(bottomLayerRegion.region) // Layers of two activities sum to be fullscreen size on display. topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds) @@ -117,20 +126,20 @@ class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) : flicker.assertLayersEnd { val topLayerRegion = - this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) val bottomLayerRegion = - this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) // Compare dimensions of two splits, given we're using default split attributes, // both activities take up the same visible size on the display. check { "height" } - .that(topLayerRegion.region.height).isLower(bottomLayerRegion.region.height) + .that(topLayerRegion.region.height) + .isLower(bottomLayerRegion.region.height) check { "height" } - .that( - topLayerRegion.region.height / 0.3f - - bottomLayerRegion.region.height / 0.7f) - .isLower(0.1f) + .that(topLayerRegion.region.height / 0.3f - bottomLayerRegion.region.height / 0.7f) + .isLower(0.1f) check { "width" } - .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width) + .that(topLayerRegion.region.width) + .isEqual(bottomLayerRegion.region.width) topLayerRegion.notOverlaps(bottomLayerRegion.region) // Layers of two activities sum to be fullscreen size on display. topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds) @@ -140,6 +149,7 @@ class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) : companion object { /** {@inheritDoc} */ private var startDisplayBounds = Rect.EMPTY + /** * Creates the test configurations. * @@ -150,4 +160,4 @@ class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) : @JvmStatic fun getParams() = LegacyFlickerTestFactory.nonRotationTests() } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt index b0ae7383cf61..ce9c337ff9bd 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt @@ -16,13 +16,13 @@ package com.android.server.wm.flicker.activityembedding.open -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.datatypes.Rect import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper @@ -70,6 +70,8 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest @Ignore("Not applicable to this CUJ.") override fun navBarWindowIsVisibleAtStartAndEnd() {} + @FlakyTest(bugId = 291575593) override fun entireScreenCovered() {} + @Ignore("Not applicable to this CUJ.") override fun statusBarWindowIsAlwaysVisible() {} @Ignore("Not applicable to this CUJ.") override fun statusBarLayerPositionAtStartAndEnd() {} @@ -131,6 +133,7 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest companion object { /** {@inheritDoc} */ private var startDisplayBounds = Rect.EMPTY + /** * Creates the test configurations. * diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt index 5551aa6b2fea..9f9fc23081c5 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt @@ -18,7 +18,6 @@ package com.android.server.wm.flicker.activityembedding.open import android.platform.test.annotations.Presubmit import android.tools.common.datatypes.Rect -import android.tools.common.traces.component.ComponentNameMatcher import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt index 6432f1f10cf1..30e833f433a8 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt @@ -16,16 +16,14 @@ package com.android.server.wm.flicker.activityembedding.open -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.datatypes.Rect -import android.tools.common.datatypes.Region import android.tools.common.flicker.subject.region.RegionSubject -import android.tools.common.traces.component.ComponentNameMatcher import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper @@ -40,8 +38,8 @@ import org.junit.runners.Parameterized * * Setup: Launch Activity A in fullscreen. * - * Transitions: From A launch a trampoline Activity T, T launches secondary Activity B and - * finishes itself, end up in split A|B. + * Transitions: From A launch a trampoline Activity T, T launches secondary Activity B and finishes + * itself, end up in split A|B. * * To run this test: `atest FlickerTestsOther:OpenTrampolineActivityTest` */ @@ -55,12 +53,10 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding tapl.setExpectedRotationCheckEnabled(false) testApp.launchViaIntent(wmHelper) startDisplayBounds = - wmHelper.currentState.layerState.physicalDisplayBounds - ?: error("Can't get display bounds") - } - transitions { - testApp.launchTrampolineActivity(wmHelper) + wmHelper.currentState.layerState.physicalDisplayBounds + ?: error("Can't get display bounds") } + transitions { testApp.launchTrampolineActivity(wmHelper) } teardown { tapl.goHome() testApp.exit(wmHelper) @@ -88,9 +84,7 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding @Presubmit @Test fun mainActivityWindowAlwaysVisible() { - flicker.assertWm { - isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) - } + flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) } } // TODO(b/289140963): After this is fixed, assert the main Activity window is visible @@ -99,12 +93,8 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding @Presubmit @Test fun mainActivityLayerAlwaysVisible() { - flicker.assertLayersStart { - isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) - } - flicker.assertLayersEnd { - isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) - } + flicker.assertLayersStart { isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) } + flicker.assertLayersEnd { isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) } } /** Secondary activity is launched from the trampoline activity. */ @@ -113,10 +103,13 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding fun secondaryActivityWindowLaunchedFromTrampoline() { flicker.assertWm { notContains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) - .then() - .isAppWindowInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) - .then() - .isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + .then() + .isAppWindowInvisible( + ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT, + isOptional = true + ) + .then() + .isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) } } @@ -126,8 +119,8 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding fun secondaryActivityLayerLaunchedFromTrampoline() { flicker.assertLayers { isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) - .then() - .isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + .then() + .isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) } } @@ -137,73 +130,85 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding fun mainActivityWindowGoesFromFullscreenToSplit() { flicker.assertWm { this.invoke("mainActivityStartsInFullscreen") { - it.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + it.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) .coversExactly(startDisplayBounds) - } - // Begin of transition. - .then() - .isAppWindowInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) - .then() - .invoke("mainAndSecondaryInSplit") { - val mainActivityRegion = - RegionSubject( - it.visibleRegion( - ActivityEmbeddingAppHelper - .MAIN_ACTIVITY_COMPONENT).region, - it.timestamp) - val secondaryActivityRegion = - RegionSubject( - it.visibleRegion( - ActivityEmbeddingAppHelper - .SECONDARY_ACTIVITY_COMPONENT).region, - it.timestamp) - check { "height" } - .that(mainActivityRegion.region.height) - .isEqual(secondaryActivityRegion.region.height) - check { "width" } - .that(mainActivityRegion.region.width) - .isEqual(secondaryActivityRegion.region.width) - mainActivityRegion - .plus(secondaryActivityRegion.region) - .coversExactly(startDisplayBounds) - } + } + // Begin of transition. + .then() + .isAppWindowInvisible( + ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT, + isOptional = true + ) + .then() + .invoke("mainAndSecondaryInSplit") { + val mainActivityRegion = + RegionSubject( + it.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + .region, + it.timestamp + ) + val secondaryActivityRegion = + RegionSubject( + it.visibleRegion( + ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT + ) + .region, + it.timestamp + ) + check { "height" } + .that(mainActivityRegion.region.height) + .isEqual(secondaryActivityRegion.region.height) + check { "width" } + .that(mainActivityRegion.region.width) + .isEqual(secondaryActivityRegion.region.width) + mainActivityRegion + .plus(secondaryActivityRegion.region) + .coversExactly(startDisplayBounds) + } } } @FlakyTest(bugId = 290736037) /** Main activity should go from fullscreen to being a split with secondary activity. */ - @Presubmit @Test fun mainActivityLayerGoesFromFullscreenToSplit() { flicker.assertLayers { this.invoke("mainActivityStartsInFullscreen") { - it.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + it.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) .coversExactly(startDisplayBounds) - } - .then() - .isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) - .then() - .isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + } + .then() + .isInvisible( + ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT, + isOptional = true + ) + .then() + .isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) } flicker.assertLayersEnd { - val leftLayerRegion = visibleRegion( - ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + val leftLayerRegion = visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) val rightLayerRegion = - visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) // Compare dimensions of two splits, given we're using default split attributes, // both activities take up the same visible size on the display. check { "height" } - .that(leftLayerRegion.region.height) - .isEqual(rightLayerRegion.region.height) + .that(leftLayerRegion.region.height) + .isEqual(rightLayerRegion.region.height) check { "width" } - .that(leftLayerRegion.region.width) - .isEqual(rightLayerRegion.region.width) + .that(leftLayerRegion.region.width) + .isEqual(rightLayerRegion.region.width) leftLayerRegion.notOverlaps(rightLayerRegion.region) // Layers of two activities sum to be fullscreen size on display. leftLayerRegion.plus(rightLayerRegion.region).coversExactly(startDisplayBounds) } } + @FlakyTest(bugId = 288591571) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() { + super.visibleLayersShownMoreThanOneConsecutiveEntry() + } + companion object { /** {@inheritDoc} */ private var startDisplayBounds = Rect.EMPTY @@ -218,4 +223,4 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding @JvmStatic fun getParams() = LegacyFlickerTestFactory.nonRotationTests() } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt index 9ad3eddd1c4f..359845dc0de6 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt @@ -36,92 +36,84 @@ import org.junit.runners.Parameterized /** * Test launching a secondary Activity into Picture-In-Picture mode. * - * Setup: Start from a split A|B. - * Transition: B enters PIP, observe the window shrink to the bottom right corner on screen. + * Setup: Start from a split A|B. Transition: B enters PIP, observe the window shrink to the bottom + * right corner on screen. * * To run this test: `atest FlickerTestsOther:SecondaryActivityEnterPipTest` - * */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class SecondaryActivityEnterPipTest (flicker: LegacyFlickerTest) : - ActivityEmbeddingTestBase(flicker) { +class SecondaryActivityEnterPipTest(flicker: LegacyFlickerTest) : + ActivityEmbeddingTestBase(flicker) { override val transition: FlickerBuilder.() -> Unit = { setup { tapl.setExpectedRotationCheckEnabled(false) testApp.launchViaIntent(wmHelper) testApp.launchSecondaryActivity(wmHelper) startDisplayBounds = - wmHelper.currentState.layerState.physicalDisplayBounds - ?: error("Can't get display bounds") - } - transitions { - testApp.secondaryActivityEnterPip(wmHelper) + wmHelper.currentState.layerState.physicalDisplayBounds + ?: error("Can't get display bounds") } + transitions { testApp.secondaryActivityEnterPip(wmHelper) } teardown { tapl.goHome() testApp.exit(wmHelper) } } - /** - * Main and secondary activity start from a split each taking half of the screen. - */ + /** Main and secondary activity start from a split each taking half of the screen. */ @Presubmit @Test fun layersStartFromEqualSplit() { flicker.assertLayersStart { - val leftLayerRegion = - visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + val leftLayerRegion = visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) val rightLayerRegion = - visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) // Compare dimensions of two splits, given we're using default split attributes, // both activities take up the same visible size on the display. check { "height" } - .that(leftLayerRegion.region.height).isEqual(rightLayerRegion.region.height) + .that(leftLayerRegion.region.height) + .isEqual(rightLayerRegion.region.height) check { "width" } - .that(leftLayerRegion.region.width).isEqual(rightLayerRegion.region.width) + .that(leftLayerRegion.region.width) + .isEqual(rightLayerRegion.region.width) leftLayerRegion.notOverlaps(rightLayerRegion.region) leftLayerRegion.plus(rightLayerRegion.region).coversExactly(startDisplayBounds) } flicker.assertLayersEnd { visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) - .coversExactly(startDisplayBounds) + .coversExactly(startDisplayBounds) } } - /** - * Main Activity is visible throughout the transition and becomes fullscreen. - */ + /** Main Activity is visible throughout the transition and becomes fullscreen. */ @Presubmit @Test fun mainActivityWindowBecomesFullScreen() { flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) } flicker.assertWmEnd { visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) - .coversExactly(startDisplayBounds) + .coversExactly(startDisplayBounds) } } - /** - * Main Activity is visible throughout the transition and becomes fullscreen. - */ + /** Main Activity is visible throughout the transition and becomes fullscreen. */ @Presubmit @Test fun mainActivityLayerBecomesFullScreen() { flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) - .then() - .isVisible(TRANSITION_SNAPSHOT) - .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) - .then() - .isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + .then() + .isVisible(TRANSITION_SNAPSHOT) + .isInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + .then() + .isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) } flicker.assertLayersEnd { visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) - .coversExactly(startDisplayBounds) + .coversExactly(startDisplayBounds) } } @@ -137,18 +129,15 @@ class SecondaryActivityEnterPipTest (flicker: LegacyFlickerTest) : } flicker.assertWmEnd { val pipWindowRegion = - visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) - check{"height"} - .that(pipWindowRegion.region.height) - .isLower(startDisplayBounds.height / 2) - check{"width"} - .that(pipWindowRegion.region.width).isLower(startDisplayBounds.width) + visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + check { "height" } + .that(pipWindowRegion.region.height) + .isLower(startDisplayBounds.height / 2) + check { "width" }.that(pipWindowRegion.region.width).isLower(startDisplayBounds.width) } } - /** - * During the transition Secondary Activity shrinks to the bottom right corner. - */ + /** During the transition Secondary Activity shrinks to the bottom right corner. */ @Presubmit @Test fun secondaryLayerShrinks() { @@ -163,13 +152,9 @@ class SecondaryActivityEnterPipTest (flicker: LegacyFlickerTest) : } } flicker.assertLayersEnd { - val pipRegion = visibleRegion( - ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) - check { "height" } - .that(pipRegion.region.height) - .isLower(startDisplayBounds.height / 2) - check { "width" } - .that(pipRegion.region.width).isLower(startDisplayBounds.width) + val pipRegion = visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + check { "height" }.that(pipRegion.region.height).isLower(startDisplayBounds.height / 2) + check { "width" }.that(pipRegion.region.width).isLower(startDisplayBounds.width) } } @@ -186,4 +171,4 @@ class SecondaryActivityEnterPipTest (flicker: LegacyFlickerTest) : @JvmStatic fun getParams() = LegacyFlickerTestFactory.nonRotationTests() } -}
\ No newline at end of file +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt index dbbc771809de..288558aeb3b1 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt @@ -16,13 +16,12 @@ package com.android.server.wm.flicker.close -import android.platform.test.annotations.FlakyTest import android.tools.common.flicker.annotation.FlickerServiceCompatible import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -71,12 +70,11 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @FlickerServiceCompatible @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseAppBackButtonTest(flicker: LegacyFlickerTest) : CloseAppTransition(flicker) { +class CloseAppBackButtonTest(flicker: LegacyFlickerTest) : CloseAppTransition(flicker) { /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTestCfArm.kt deleted file mode 100644 index 566f393efaea..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppBackButtonTestCfArm.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.server.wm.flicker.close - -import android.tools.common.flicker.annotation.FlickerServiceCompatible -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@FlickerServiceCompatible -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class CloseAppBackButtonTestCfArm(flicker: LegacyFlickerTest) : CloseAppBackButtonTest(flicker) { - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt index ed930fc8c236..32305c6c9a33 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt @@ -16,13 +16,12 @@ package com.android.server.wm.flicker.close -import android.platform.test.annotations.FlakyTest import android.tools.common.flicker.annotation.FlickerServiceCompatible import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -71,12 +70,11 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @FlickerServiceCompatible @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseAppHomeButtonTest(flicker: LegacyFlickerTest) : CloseAppTransition(flicker) { +class CloseAppHomeButtonTest(flicker: LegacyFlickerTest) : CloseAppTransition(flicker) { /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit get() = { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTestCfArm.kt deleted file mode 100644 index 49ed183c2ccf..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTestCfArm.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.server.wm.flicker.close - -import android.tools.common.flicker.annotation.FlickerServiceCompatible -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@FlickerServiceCompatible -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class CloseAppHomeButtonTestCfArm(flicker: LegacyFlickerTest) : CloseAppHomeButtonTest(flicker) { - companion object { - /** Creates the test configurations. */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt index 8737edb445f1..8d752ccd275e 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/close/CloseAppTransition.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.close -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.flicker.subject.layers.LayersTraceSubject.Companion.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS import android.tools.common.traces.component.ComponentNameMatcher @@ -24,6 +23,7 @@ import android.tools.common.traces.component.ComponentNameMatcher.Companion.LAUN import android.tools.device.apphelpers.StandardAppHelper import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.helpers.setRotation diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt index 883c7e6d5785..11e6bbe4eb13 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt @@ -56,9 +56,7 @@ constructor( launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_rtl_button") } - /** - * Clicks the button to launch the secondary activity in a horizontal split. - */ + /** Clicks the button to launch the secondary activity in a horizontal split. */ fun launchSecondaryActivityHorizontally(wmHelper: WindowManagerStateHelper) { launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_horizontally_button") } @@ -67,7 +65,7 @@ constructor( fun launchThirdActivity(wmHelper: WindowManagerStateHelper) { val launchButton = uiDevice.wait( - Until.findObject(By.res(getPackage(), "launch_third_activity_button")), + Until.findObject(By.res(packageName, "launch_third_activity_button")), FIND_TIMEOUT ) require(launchButton != null) { "Can't find launch third activity button on screen." } @@ -86,17 +84,17 @@ constructor( */ fun launchTrampolineActivity(wmHelper: WindowManagerStateHelper) { val launchButton = - uiDevice.wait( - Until.findObject(By.res(getPackage(), "launch_trampoline_button")), - FIND_TIMEOUT - ) + uiDevice.wait( + Until.findObject(By.res(packageName, "launch_trampoline_button")), + FIND_TIMEOUT + ) require(launchButton != null) { "Can't find launch trampoline activity button on screen." } launchButton.click() wmHelper - .StateSyncBuilder() - .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) - .withActivityRemoved(TRAMPOLINE_ACTIVITY_COMPONENT) - .waitForAndVerify() + .StateSyncBuilder() + .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) + .withActivityRemoved(TRAMPOLINE_ACTIVITY_COMPONENT) + .waitForAndVerify() } /** @@ -105,53 +103,45 @@ constructor( */ fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) { val finishButton = - uiDevice.wait( - Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")), - FIND_TIMEOUT - ) + uiDevice.wait( + Until.findObject(By.res(packageName, "finish_secondary_activity_button")), + FIND_TIMEOUT + ) require(finishButton != null) { "Can't find finish secondary activity button on screen." } finishButton.click() wmHelper - .StateSyncBuilder() - .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT) - .waitForAndVerify() - } + .StateSyncBuilder() + .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT) + .waitForAndVerify() + } - /** - * Clicks the button to toggle the split ratio of secondary activity. - */ + /** Clicks the button to toggle the split ratio of secondary activity. */ fun changeSecondaryActivityRatio(wmHelper: WindowManagerStateHelper) { val launchButton = - uiDevice.wait( - Until.findObject( - By.res(getPackage(), - "toggle_split_ratio_button")), - FIND_TIMEOUT - ) + uiDevice.wait( + Until.findObject(By.res(packageName, "toggle_split_ratio_button")), + FIND_TIMEOUT + ) require(launchButton != null) { "Can't find toggle ratio for secondary activity button on screen." } launchButton.click() wmHelper - .StateSyncBuilder() - .withAppTransitionIdle() - .withTransitionSnapshotGone() - .waitForAndVerify() + .StateSyncBuilder() + .withAppTransitionIdle() + .withTransitionSnapshotGone() + .waitForAndVerify() } fun secondaryActivityEnterPip(wmHelper: WindowManagerStateHelper) { val pipButton = - uiDevice.wait( - Until.findObject(By.res(getPackage(), "secondary_enter_pip_button")), - FIND_TIMEOUT - ) + uiDevice.wait( + Until.findObject(By.res(packageName, "secondary_enter_pip_button")), + FIND_TIMEOUT + ) require(pipButton != null) { "Can't find enter pip button on screen." } pipButton.click() - wmHelper - .StateSyncBuilder() - .withAppTransitionIdle() - .withPipShown() - .waitForAndVerify() + wmHelper.StateSyncBuilder().withAppTransitionIdle().withPipShown().waitForAndVerify() } /** @@ -161,7 +151,7 @@ constructor( fun launchAlwaysExpandActivity(wmHelper: WindowManagerStateHelper) { val launchButton = uiDevice.wait( - Until.findObject(By.res(getPackage(), "launch_always_expand_activity_button")), + Until.findObject(By.res(packageName, "launch_always_expand_activity_button")), FIND_TIMEOUT ) require(launchButton != null) { @@ -171,23 +161,29 @@ constructor( wmHelper .StateSyncBuilder() .withActivityState(ALWAYS_EXPAND_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) - .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_PAUSED) + .withActivityState( + MAIN_ACTIVITY_COMPONENT, + PlatformConsts.STATE_PAUSED, + PlatformConsts.STATE_STOPPED + ) .waitForAndVerify() } private fun launchSecondaryActivityFromButton( - wmHelper: WindowManagerStateHelper, buttonName: String) { + wmHelper: WindowManagerStateHelper, + buttonName: String + ) { val launchButton = - uiDevice.wait(Until.findObject(By.res(getPackage(), buttonName)), FIND_TIMEOUT) + uiDevice.wait(Until.findObject(By.res(packageName, buttonName)), FIND_TIMEOUT) require(launchButton != null) { "Can't find launch secondary activity button : " + buttonName + "on screen." } launchButton.click() wmHelper - .StateSyncBuilder() - .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) - .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) - .waitForAndVerify() + .StateSyncBuilder() + .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) + .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) + .waitForAndVerify() } /** @@ -197,7 +193,7 @@ constructor( fun launchPlaceholderSplit(wmHelper: WindowManagerStateHelper) { val launchButton = uiDevice.wait( - Until.findObject(By.res(getPackage(), "launch_placeholder_split_button")), + Until.findObject(By.res(packageName, "launch_placeholder_split_button")), FIND_TIMEOUT ) require(launchButton != null) { "Can't find launch placeholder split button on screen." } @@ -216,7 +212,7 @@ constructor( fun launchPlaceholderSplitRTL(wmHelper: WindowManagerStateHelper) { val launchButton = uiDevice.wait( - Until.findObject(By.res(getPackage(), "launch_placeholder_split_rtl_button")), + Until.findObject(By.res(packageName, "launch_placeholder_split_rtl_button")), FIND_TIMEOUT ) require(launchButton != null) { "Can't find launch placeholder split button on screen." } @@ -252,7 +248,7 @@ constructor( .toFlickerComponent() val TRAMPOLINE_ACTIVITY_COMPONENT = - ActivityOptions.ActivityEmbedding.TrampolineActivity.COMPONENT.toFlickerComponent() + ActivityOptions.ActivityEmbedding.TrampolineActivity.COMPONENT.toFlickerComponent() @JvmStatic fun getWindowExtensions(): WindowExtensions? { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt index 747cf3742bf7..3146139757c1 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt @@ -41,7 +41,7 @@ constructor( */ fun swipeDown(): Boolean { val gameView = - uiDevice.wait(Until.findObject(By.res(getPackage(), GAME_APP_VIEW_RES)), WAIT_TIME_MS) + uiDevice.wait(Until.findObject(By.res(packageName, GAME_APP_VIEW_RES)), WAIT_TIME_MS) require(gameView != null) { "Mock game app view not found." } val bound = gameView.getVisibleBounds() diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt index d1722521bba8..252f7d3e1bed 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt @@ -40,7 +40,7 @@ constructor( */ open fun openIME(wmHelper: WindowManagerStateHelper) { val editText = - uiDevice.wait(Until.findObject(By.res(getPackage(), "plain_text_input")), FIND_TIMEOUT) + uiDevice.wait(Until.findObject(By.res(packageName, "plain_text_input")), FIND_TIMEOUT) requireNotNull(editText) { "Text field not found, this usually happens when the device " + @@ -67,7 +67,7 @@ constructor( open fun finishActivity(wmHelper: WindowManagerStateHelper) { val finishButton = uiDevice.wait( - Until.findObject(By.res(getPackage(), "finish_activity_btn")), + Until.findObject(By.res(packageName, "finish_activity_btn")), FIND_TIMEOUT ) requireNotNull(finishButton) { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt index 83a41abbd4bd..1a65611757c0 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt @@ -71,7 +71,7 @@ constructor( if (rotation.isRotated()) { imePackageName } else { - getPackage() + packageName } open(expectedPackage) } @@ -79,7 +79,7 @@ constructor( fun startDialogThemedActivity(wmHelper: WindowManagerStateHelper) { val button = uiDevice.wait( - Until.findObject(By.res(getPackage(), "start_dialog_themed_activity_btn")), + Until.findObject(By.res(packageName, "start_dialog_themed_activity_btn")), FIND_TIMEOUT ) @@ -132,7 +132,7 @@ constructor( fun toggleFixPortraitOrientation(wmHelper: WindowManagerStateHelper) { val button = uiDevice.wait( - Until.findObject(By.res(getPackage(), "toggle_fixed_portrait_btn")), + Until.findObject(By.res(packageName, "toggle_fixed_portrait_btn")), FIND_TIMEOUT ) require(button != null) { @@ -140,7 +140,7 @@ constructor( "was left in an unknown state (e.g. Screen turned off)" } button.click() - mInstrumentation.waitForIdleSync() + instrumentation.waitForIdleSync() // Ensure app relaunching transition finish and the IME has shown waitIMEShown(wmHelper) } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt index d83b6d39fcd8..9b539c8641d4 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt @@ -38,7 +38,7 @@ constructor( ActivityOptions.NonResizeablePortraitActivity.COMPONENT.toFlickerComponent() ) : StandardAppHelper(instr, launcherName, component) { - private val gestureHelper: GestureHelper = GestureHelper(mInstrumentation) + private val gestureHelper: GestureHelper = GestureHelper(instrumentation) fun clickRestart(wmHelper: WindowManagerStateHelper) { val restartButton = diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt index c98f1c4b4d29..9895bda7f590 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/MailAppHelper.kt @@ -37,7 +37,7 @@ constructor( fun openMail(rowIdx: Int) { val rowSel = - By.res(getPackage(), "mail_row_item_text").textEndsWith(String.format("%04d", rowIdx)) + By.res(packageName, "mail_row_item_text").textEndsWith(String.format("%04d", rowIdx)) var row: UiObject2? = null for (i in 1..1000) { row = uiDevice.wait(Until.findObject(rowSel), SHORT_WAIT_TIME_MS) @@ -46,7 +46,7 @@ constructor( } require(row != null) { "" } row.click() - uiDevice.wait(Until.gone(By.res(getPackage(), MAIL_LIST_RES_ID)), FIND_TIMEOUT) + uiDevice.wait(Until.gone(By.res(packageName, MAIL_LIST_RES_ID)), FIND_TIMEOUT) } fun scrollDown() { @@ -55,7 +55,7 @@ constructor( } fun waitForMailList(): UiObject2 { - val sel = By.res(getPackage(), MAIL_LIST_RES_ID).scrollable(true) + val sel = By.res(packageName, MAIL_LIST_RES_ID).scrollable(true) val ret = uiDevice.wait(Until.findObject(sel), FIND_TIMEOUT) requireNotNull(ret) { "Unable to find $MAIL_LIST_RES_ID object" } return ret diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt index 5b3d3083fe1c..b2f8d4748c07 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NewTasksAppHelper.kt @@ -36,7 +36,7 @@ constructor( ) : StandardAppHelper(instr, launcherName, component) { fun openNewTask(device: UiDevice, wmHelper: WindowManagerStateHelper) { val button = - device.wait(Until.findObject(By.res(getPackage(), "launch_new_task")), FIND_TIMEOUT) + device.wait(Until.findObject(By.res(packageName, "launch_new_task")), FIND_TIMEOUT) requireNotNull(button) { "Button not found, this usually happens when the device " + diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt index 7665690a3122..e60c20df9967 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/NotificationAppHelper.kt @@ -35,7 +35,7 @@ constructor( ) : StandardAppHelper(instr, launcherName, component) { fun postNotification(wmHelper: WindowManagerStateHelper) { val button = - uiDevice.wait(Until.findObject(By.res(getPackage(), "post_notification")), FIND_TIMEOUT) + uiDevice.wait(Until.findObject(By.res(packageName, "post_notification")), FIND_TIMEOUT) requireNotNull(button) { "Post notification button not found, this usually happens when the device " + diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index c6b86f2689f0..82d2ae098870 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -46,12 +46,14 @@ open class PipAppHelper(instrumentation: Instrumentation) : private val mediaController: MediaController? get() = - mediaSessionManager.getActiveSessions(null).firstOrNull { it.packageName == `package` } + mediaSessionManager.getActiveSessions(null).firstOrNull { + it.packageName == packageName + } - private val gestureHelper: GestureHelper = GestureHelper(mInstrumentation) + private val gestureHelper: GestureHelper = GestureHelper(instrumentation) open fun clickObject(resId: String) { - val selector = By.res(`package`, resId) + val selector = By.res(packageName, resId) val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") obj.click() @@ -250,13 +252,10 @@ open class PipAppHelper(instrumentation: Instrumentation) : waitConditions = arrayOf(ConditionsFactory.hasPipWindow()) ) - val windowRegion = wmHelper.getWindowRegion(this) - wmHelper .StateSyncBuilder() .withWindowSurfaceAppeared(this) .withPipShown() - .withSurfaceVisibleRegion(this, windowRegion) .waitForAndVerify() } @@ -289,7 +288,7 @@ open class PipAppHelper(instrumentation: Instrumentation) : fun checkWithCustomActionsCheckbox() = uiDevice - .findObject(By.res(`package`, WITH_CUSTOM_ACTIONS_BUTTON_ID)) + .findObject(By.res(packageName, WITH_CUSTOM_ACTIONS_BUTTON_ID)) ?.takeIf { it.isCheckable } ?.apply { if (!isChecked) clickObject(WITH_CUSTOM_ACTIONS_BUTTON_ID) } ?: error("'With custom actions' checkbox not found") @@ -305,7 +304,7 @@ open class PipAppHelper(instrumentation: Instrumentation) : ReplaceWith("closePipWindow(wmHelper)") ) open fun closePipWindow() { - closePipWindow(WindowManagerStateHelper(mInstrumentation)) + closePipWindow(WindowManagerStateHelper(instrumentation)) } /** Returns the pip window bounds. */ @@ -389,8 +388,10 @@ open class PipAppHelper(instrumentation: Instrumentation) : Log.d(TAG, "window " + pipAppWindow) if (pipAppWindow == null) return@add false val pipRegion = pipAppWindow.frameRegion - Log.d(TAG, "region " + pipRegion + - " covers " + windowRect.coversMoreThan(pipRegion)) + Log.d( + TAG, + "region " + pipRegion + " covers " + windowRect.coversMoreThan(pipRegion) + ) return@add windowRect.coversMoreThan(pipRegion) } .waitForAndVerify() diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TransferSplashscreenAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TransferSplashscreenAppHelper.kt new file mode 100644 index 000000000000..6311678f1a04 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TransferSplashscreenAppHelper.kt @@ -0,0 +1,32 @@ +/* + * 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.server.wm.flicker.helpers + +import android.app.Instrumentation +import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.device.apphelpers.StandardAppHelper +import android.tools.device.traces.parsers.toFlickerComponent +import com.android.server.wm.flicker.testapp.ActivityOptions + +class TransferSplashscreenAppHelper +@JvmOverloads +constructor( + instr: Instrumentation, + launcherName: String = ActivityOptions.TransferSplashscreenActivity.LABEL, + component: ComponentNameMatcher = + ActivityOptions.TransferSplashscreenActivity.COMPONENT.toFlickerComponent() +) : StandardAppHelper(instr, launcherName, component) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt index 895725c1efef..8be5769f47cf 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/TwoActivitiesAppHelper.kt @@ -40,7 +40,7 @@ constructor( ActivityOptions.SimpleActivity.COMPONENT.toFlickerComponent() fun openSecondActivity(device: UiDevice, wmHelper: WindowManagerStateHelper) { - val launchActivityButton = By.res(getPackage(), LAUNCH_SECOND_ACTIVITY) + val launchActivityButton = By.res(packageName, LAUNCH_SECOND_ACTIVITY) val button = device.wait(Until.findObject(launchActivityButton), FIND_TIMEOUT) requireNotNull(button) { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt index 98446c1a76b2..47a1619e0e9c 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt @@ -24,7 +24,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeEditorPopupDialogAppHelper import org.junit.FixMethodOrder @@ -33,11 +32,10 @@ import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseImeOnDismissPopupDialogTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class CloseImeOnDismissPopupDialogTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val imeTestApp = ImeEditorPopupDialogAppHelper(instrumentation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTestCfArm.kt deleted file mode 100644 index d87a1daebd79..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTestCfArm.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.common.Rotation -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class CloseImeOnDismissPopupDialogTestCfArm(flicker: LegacyFlickerTest) : - CloseImeOnDismissPopupDialogTest(flicker) { - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = - LegacyFlickerTestFactory.nonRotationTests( - supportedRotations = listOf(Rotation.ROTATION_0) - ) - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt index b995d3df8055..48c54eabc5e2 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt @@ -24,7 +24,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeAppHelper import org.junit.FixMethodOrder @@ -37,11 +36,10 @@ import org.junit.runners.Parameterized * Test IME window closing to home transitions. To run this test: `atest * FlickerTests:CloseImeWindowToHomeTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseImeOnGoHomeTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class CloseImeOnGoHomeTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = ImeAppHelper(instrumentation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt index 765bb4cf3067..31d5d7f837d5 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt @@ -23,7 +23,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import org.junit.FixMethodOrder @@ -45,11 +44,10 @@ import org.junit.runners.Parameterized * * To run this test: `atest FlickerTests:CloseImeAutoOpenWindowToHomeTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseImeShownOnAppStartOnGoHomeTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class CloseImeShownOnAppStartOnGoHomeTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt deleted file mode 100644 index 58411cc63004..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTestCfArm.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseImeShownOnAppStartOnGoHomeTestCfArm(flicker: LegacyFlickerTest) : - CloseImeShownOnAppStartOnGoHomeTest(flicker) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt index c87217b30783..180ae5d6f097 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt @@ -23,7 +23,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import org.junit.FixMethodOrder @@ -45,12 +44,10 @@ import org.junit.runners.Parameterized * * To run this test: `atest FlickerTests:CloseImeAutoOpenWindowToAppTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseImeShownOnAppStartToAppOnPressBackTest(flicker: LegacyFlickerTest) : - BaseTest(flicker) { +class CloseImeShownOnAppStartToAppOnPressBackTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt deleted file mode 100644 index 41b06c0b4bd3..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTestCfArm.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class CloseImeShownOnAppStartToAppOnPressBackTestCfArm(flicker: LegacyFlickerTest) : - CloseImeShownOnAppStartToAppOnPressBackTest(flicker) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt index c74870bef102..a0573b7b4b16 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt @@ -23,7 +23,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd @@ -38,11 +37,10 @@ import org.junit.runners.Parameterized * Test IME window closing back to app window transitions. To run this test: `atest * FlickerTests:CloseImeWindowToAppTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseImeToAppOnPressBackTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class CloseImeToAppOnPressBackTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = ImeAppHelper(instrumentation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt deleted file mode 100644 index 104af225ee2e..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTestCfArm.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class CloseImeToAppOnPressBackTestCfArm(flicker: LegacyFlickerTest) : - CloseImeToAppOnPressBackTest(flicker) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt index 21fd590025bc..b44f1a607b05 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.ime -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation @@ -24,7 +23,7 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper @@ -40,11 +39,10 @@ import org.junit.runners.Parameterized * * To run this test: `atest FlickerTests:OpenImeWindowAndCloseTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseImeToHomeOnFinishActivityTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class CloseImeToHomeOnFinishActivityTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val simpleApp = SimpleAppHelper(instrumentation) private val testApp = ImeAppHelper(instrumentation) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt deleted file mode 100644 index 0e1838570bab..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTestCfArm.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class CloseImeToHomeOnFinishActivityTestCfArm(flicker: LegacyFlickerTest) : - CloseImeToHomeOnFinishActivityTest(flicker) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppCfArmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppCfArmTest.kt deleted file mode 100644 index db80001ccd78..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppCfArmTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ShowImeOnAppStartWhenLaunchingAppCfArmTest(flicker: LegacyFlickerTest) : - ShowImeOnAppStartWhenLaunchingAppTest(flicker) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt index 44bd8c8688e9..ea7c7c4276dc 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt @@ -19,15 +19,13 @@ package com.android.server.wm.flicker.ime import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import android.tools.common.Timestamp -import android.tools.common.flicker.subject.exceptions.ExceptionMessageBuilder -import android.tools.common.flicker.subject.exceptions.InvalidPropertyException +import android.tools.common.flicker.subject.layers.LayerTraceEntrySubject import android.tools.common.traces.component.ComponentNameMatcher import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import com.android.server.wm.flicker.helpers.setRotation @@ -43,11 +41,10 @@ import org.junit.runners.Parameterized * (e.g. Launcher activity). To run this test: `atest * FlickerTests:ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: LegacyFlickerTest) : +class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val imeTestApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation) @@ -82,47 +79,43 @@ open class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: Le flicker.snapshotStartingWindowLayerCoversExactlyOnApp(imeTestApp) } + @FlakyTest(bugId = 290767483) @Postsubmit @Test fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() { - // Check if the snapshot appeared during the trace - var imeSnapshotRemovedTimestamp: Timestamp? = null - - val layerTrace = flicker.reader.readLayersTrace() - val layerTraceEntries = layerTrace?.entries?.toList() ?: emptyList() - - layerTraceEntries.zipWithNext { prev, next -> - val prevSnapshotLayerVisible = - ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(prev.visibleLayers) - val nextSnapshotLayerVisible = - ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(next.visibleLayers) - - if ( - imeSnapshotRemovedTimestamp == null && - (prevSnapshotLayerVisible && !nextSnapshotLayerVisible) - ) { - imeSnapshotRemovedTimestamp = next.timestamp - } - } - - // if so, make an assertion - imeSnapshotRemovedTimestamp?.let { timestamp -> - val stateAfterSnapshot = - layerTrace?.getEntryAt(timestamp) ?: error("State not found for $timestamp") - - val imeLayers = - ComponentNameMatcher.IME.filterLayers(stateAfterSnapshot.visibleLayers.toList()) - - require(imeLayers.isNotEmpty()) { "IME layer not found" } - if (imeLayers.any { it.color.a != 1.0f }) { - val errorMsgBuilder = - ExceptionMessageBuilder() - .setTimestamp(timestamp) - .forInvalidProperty("IME layer alpha") - .setExpected("is 1.0") - .setActual("not 1.0") - .addExtraDescription("Filter", ComponentNameMatcher.IME.toLayerIdentifier()) - throw InvalidPropertyException(errorMsgBuilder) + val layerTrace = flicker.reader.readLayersTrace() ?: error("Unable to read layers trace") + + // Find the entries immediately after the IME snapshot has disappeared + val imeSnapshotRemovedEntries = + layerTrace.entries + .asSequence() + .zipWithNext { prev, next -> + if ( + ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(prev.visibleLayers) && + !ComponentNameMatcher.SNAPSHOT.layerMatchesAnyOf(next.visibleLayers) + ) { + next + } else { + null + } + } + .filterNotNull() + + // If we find it, make sure the IME is visible and fully animated in. + imeSnapshotRemovedEntries.forEach { entry -> + val entrySubject = LayerTraceEntrySubject(entry) + val imeLayerSubjects = + entrySubject.subjects.filter { + ComponentNameMatcher.IME.layerMatchesAnyOf(it.layer) && it.isVisible + } + + entrySubject + .check { "InputMethod must exist and be visible" } + .that(imeLayerSubjects.isNotEmpty()) + .isEqual(true) + + imeLayerSubjects.forEach { imeLayerSubject -> + imeLayerSubject.check { "alpha" }.that(imeLayerSubject.layer.color.a).isEqual(1.0f) } } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt deleted file mode 100644 index 6d9ea2209d91..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTestCfArm(flicker: LegacyFlickerTest) : - ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt index ae05e37a84cc..05babd67758c 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt @@ -24,7 +24,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.helpers.reopenAppFromOverview -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import com.android.server.wm.flicker.helpers.setRotation @@ -37,11 +36,10 @@ import org.junit.runners.Parameterized /** * Test IME window opening transitions. To run this test: `atest FlickerTests:ReOpenImeWindowTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class ShowImeOnAppStartWhenLaunchingAppFromOverviewTest(flicker: LegacyFlickerTest) : +class ShowImeOnAppStartWhenLaunchingAppFromOverviewTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm.kt deleted file mode 100644 index 92b3968216e5..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.common.Rotation -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ShowImeOnAppStartWhenLaunchingAppFromOverviewTestCfArm(flicker: LegacyFlickerTest) : - ShowImeOnAppStartWhenLaunchingAppFromOverviewTest(flicker) { - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = - LegacyFlickerTestFactory.nonRotationTests( - supportedRotations = listOf(Rotation.ROTATION_0) - ) - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt index c991651a9d08..aff8e657bec0 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt @@ -24,7 +24,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper @@ -39,12 +38,11 @@ import org.junit.runners.Parameterized * Test IME windows switching with 2-Buttons or gestural navigation. To run this test: `atest * FlickerTests:SwitchImeWindowsFromGestureNavTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Presubmit -open class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest(flicker: LegacyFlickerTest) : +class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = SimpleAppHelper(instrumentation) private val imeTestApp = diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt deleted file mode 100644 index 09bfacc582e5..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.platform.test.annotations.Presubmit -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Presubmit -open class ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTestCfArm(flicker: LegacyFlickerTest) : - ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest(flicker) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt index 976352146fc0..4ffdcea455ba 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt @@ -23,7 +23,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import com.android.server.wm.flicker.helpers.ImeStateInitializeHelper @@ -75,11 +74,10 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class ShowImeOnAppStartWhenLaunchingAppTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class ShowImeOnAppStartWhenLaunchingAppTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation) private val initializeApp = ImeStateInitializeHelper(instrumentation) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt index 7bd58252d3ba..918e1ea3bcd0 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt @@ -23,7 +23,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeAppHelper import org.junit.FixMethodOrder @@ -33,11 +32,10 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** Test IME window opening transitions. To run this test: `atest FlickerTests:OpenImeWindowTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class ShowImeWhenFocusingOnInputFieldTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class ShowImeWhenFocusingOnInputFieldTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = ImeAppHelper(instrumentation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt deleted file mode 100644 index f8c814988200..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTestCfArm.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ShowImeWhenFocusingOnInputFieldTestCfArm(flicker: LegacyFlickerTest) : - ShowImeWhenFocusingOnInputFieldTest(flicker) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt index 0b168baa6622..6ad235ce892e 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt @@ -26,7 +26,6 @@ import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.view.WindowInsets.Type.ime import android.view.WindowInsets.Type.navigationBars import android.view.WindowInsets.Type.statusBars -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import org.junit.Assert.assertFalse @@ -41,12 +40,10 @@ import org.junit.runners.Parameterized * Test IME snapshot mechanism won't apply when transitioning from non-IME focused dialog activity. * To run this test: `atest FlickerTests:LaunchAppShowImeAndDialogThemeAppTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class ShowImeWhileDismissingThemedPopupDialogTest(flicker: LegacyFlickerTest) : - BaseTest(flicker) { +class ShowImeWhileDismissingThemedPopupDialogTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTestCfArm.kt deleted file mode 100644 index ad41b81a6664..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTestCfArm.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.common.Rotation -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ShowImeWhileDismissingThemedPopupDialogTestCfArm(flicker: LegacyFlickerTest) : - ShowImeWhileDismissingThemedPopupDialogTest(flicker) { - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = - LegacyFlickerTestFactory.nonRotationTests( - supportedRotations = listOf(Rotation.ROTATION_0) - ) - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt index 7722c934d11b..f1bc125b99dc 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt @@ -24,7 +24,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.traces.parsers.WindowManagerStateHelper -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd @@ -41,11 +40,10 @@ import org.junit.runners.Parameterized * Test IME window layer will be associated with the app task when going to the overview screen. To * run this test: `atest FlickerTests:OpenImeWindowToOverViewTest` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val imeTestApp = ImeShownOnAppStartHelper(instrumentation, flicker.scenario.startRotation) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt deleted file mode 100644 index 90fbbf9bb240..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTestCfArm.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.server.wm.flicker.ime - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ShowImeWhileEnteringOverviewTestCfArm(flicker: LegacyFlickerTest) : - ShowImeWhileEnteringOverviewTest(flicker) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt index 3461907f71dd..89ab41e49270 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt @@ -23,7 +23,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.traces.parsers.toFlickerComponent -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.TwoActivitiesAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions @@ -53,11 +52,10 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class ActivityTransitionTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class ActivityTransitionTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp: TwoActivitiesAppHelper = TwoActivitiesAppHelper(instrumentation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTestCfArm.kt deleted file mode 100644 index 6bbf40ea10f9..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/ActivityTransitionTestCfArm.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.server.wm.flicker.launch - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ActivityTransitionTestCfArm(flicker: LegacyFlickerTest) : ActivityTransitionTest(flicker) { - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt index ae1f78a5a4ef..48d504141116 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt @@ -16,14 +16,13 @@ package com.android.server.wm.flicker.launch -import android.platform.test.annotations.FlakyTest import android.tools.common.Rotation import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -51,7 +50,6 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -73,8 +71,8 @@ open class OpenAppFromIconColdTest(flicker: LegacyFlickerTest) : tapl .goHome() .switchToAllApps() - .getAppIcon(testApp.launcherName) - .launch(testApp.`package`) + .getAppIcon(testApp.appName) + .launch(testApp.packageName) } teardown { testApp.exit(wmHelper) } } @@ -90,6 +88,7 @@ open class OpenAppFromIconColdTest(flicker: LegacyFlickerTest) : override fun appWindowReplacesLauncherAsTopWindow() { super.appWindowReplacesLauncherAsTopWindow() } + @FlakyTest(bugId = 240916028) @Test override fun appWindowAsTopWindowAtEnd() { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTestCfArm.kt deleted file mode 100644 index 4fd4a61e4adc..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTestCfArm.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.server.wm.flicker.launch - -import android.platform.test.annotations.FlakyTest -import android.tools.common.flicker.annotation.FlickerServiceCompatible -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** Some assertions will fail because of b/264415996 */ -@FlickerServiceCompatible -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class OpenAppFromIconColdTestCfArm(flicker: LegacyFlickerTest) : OpenAppFromIconColdTest(flicker) { - @Test - @FlakyTest - override fun visibleLayersShownMoreThanOneConsecutiveEntry() { - super.visibleLayersShownMoreThanOneConsecutiveEntry() - } - - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt index c95c548e7221..f575fcc0e945 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt @@ -21,7 +21,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import org.junit.FixMethodOrder import org.junit.runner.RunWith import org.junit.runners.MethodSorters @@ -34,11 +33,10 @@ import org.junit.runners.Parameterized * * Notes: Some default assertions are inherited [OpenAppTransition] */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class OpenAppFromIntentColdAfterCameraTest(flicker: LegacyFlickerTest) : +class OpenAppFromIntentColdAfterCameraTest(flicker: LegacyFlickerTest) : OpenAppFromLauncherTransition(flicker) { private val cameraApp = CameraAppHelper(instrumentation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTestCfArm.kt deleted file mode 100644 index 117cff203ff7..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTestCfArm.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.server.wm.flicker.launch - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class OpenAppFromIntentColdAfterCameraTestCfArm(flicker: LegacyFlickerTest) : - OpenAppFromIntentColdAfterCameraTest(flicker) { - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt index e39a578fd321..93d0520d87bc 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt @@ -24,7 +24,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.setRotation import org.junit.FixMethodOrder import org.junit.Test @@ -53,12 +52,11 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @FlickerServiceCompatible @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class OpenAppFromIntentColdTest(flicker: LegacyFlickerTest) : +class OpenAppFromIntentColdTest(flicker: LegacyFlickerTest) : OpenAppFromLauncherTransition(flicker) { /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTestCfArm.kt deleted file mode 100644 index 6d0b6f48d7c6..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTestCfArm.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.server.wm.flicker.launch - -import android.platform.test.annotations.FlakyTest -import android.tools.common.flicker.annotation.FlickerServiceCompatible -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@FlickerServiceCompatible -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class OpenAppFromIntentColdTestCfArm(flicker: LegacyFlickerTest) : - OpenAppFromIntentColdTest(flicker) { - @FlakyTest(bugId = 273696733) - @Test - override fun appLayerReplacesLauncher() = super.appLayerReplacesLauncher() - - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt index d2c38076e72d..78b58f4da065 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt @@ -16,14 +16,13 @@ package com.android.server.wm.flicker.launch -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.flicker.annotation.FlickerServiceCompatible import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.helpers.setRotation import org.junit.FixMethodOrder import org.junit.Test @@ -53,12 +52,11 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @FlickerServiceCompatible @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class OpenAppFromIntentWarmTest(flicker: LegacyFlickerTest) : +class OpenAppFromIntentWarmTest(flicker: LegacyFlickerTest) : OpenAppFromLauncherTransition(flicker) { /** Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTestCfArm.kt deleted file mode 100644 index 3e0958a27aaf..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTestCfArm.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.server.wm.flicker.launch - -import android.tools.common.flicker.annotation.FlickerServiceCompatible -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@FlickerServiceCompatible -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class OpenAppFromIntentWarmTestCfArm(flicker: LegacyFlickerTest) : - OpenAppFromIntentWarmTest(flicker) { - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt index 62fb5704b2e8..4fc9bcb309c5 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLauncherTransition.kt @@ -30,7 +30,7 @@ abstract class OpenAppFromLauncherTransition(flicker: LegacyFlickerTest) : @Presubmit @Test open fun focusChanges() { - flicker.assertEventLog { this.focusChanges("NexusLauncherActivity", testApp.`package`) } + flicker.assertEventLog { this.focusChanges("NexusLauncherActivity", testApp.packageName) } } /** diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenTransition.kt index 687bc1958a5a..cc501e6c4308 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenTransition.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenTransition.kt @@ -16,11 +16,11 @@ package com.android.server.wm.flicker.launch -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.traces.component.ComponentNameMatcher import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.navBarLayerPositionAtEnd import com.android.server.wm.flicker.statusBarLayerPositionAtEnd import org.junit.Assume @@ -46,7 +46,7 @@ abstract class OpenAppFromLockscreenTransition(flicker: LegacyFlickerTest) : @Presubmit @Test open fun focusChanges() { - flicker.assertEventLog { this.focusChanges("", testApp.`package`) } + flicker.assertEventLog { this.focusChanges("", testApp.packageName) } } /** diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt index 7a16060e3370..3f931c48ddbd 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.launch -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.NavBar import android.tools.common.Rotation @@ -25,6 +24,7 @@ import android.tools.common.traces.component.ComponentNameMatcher import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.NonResizeableAppHelper import org.junit.Assume @@ -71,7 +71,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : * Checks that the [ComponentNameMatcher.NAV_BAR] layer starts invisible, becomes visible during * unlocking animation and remains visible at the end */ - @Presubmit + @FlakyTest(bugId = 288341660) @Test fun navBarLayerVisibilityChanges() { Assume.assumeFalse(flicker.scenario.isTablet) @@ -93,7 +93,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : * Checks that the [ComponentNameMatcher.NAV_BAR] starts the transition invisible, then becomes * visible during the unlocking animation and remains visible at the end of the transition */ - @Presubmit + @FlakyTest(bugId = 293581770) @Test fun navBarWindowsVisibilityChanges() { Assume.assumeFalse(flicker.scenario.isTablet) @@ -200,7 +200,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() - @FlakyTest(bugId = 251217585) + @FlakyTest(bugId = 285980483) @Test override fun focusChanges() { super.focusChanges() diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt index eec6bfde8b9f..b85362a07538 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.launch -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation import android.tools.common.flicker.annotation.FlickerServiceCompatible @@ -24,7 +23,7 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.helpers.setRotation import org.junit.FixMethodOrder import org.junit.Test @@ -55,13 +54,11 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @FlickerServiceCompatible @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class OpenAppFromOverviewTest(flicker: LegacyFlickerTest) : - OpenAppFromLauncherTransition(flicker) { +class OpenAppFromOverviewTest(flicker: LegacyFlickerTest) : OpenAppFromLauncherTransition(flicker) { /** Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTestCfArm.kt deleted file mode 100644 index ab6a1ea36222..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTestCfArm.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.server.wm.flicker.launch - -import android.tools.common.flicker.annotation.FlickerServiceCompatible -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** Some assertions will fail because of b/264415996 */ -@FlickerServiceCompatible -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class OpenAppFromOverviewTestCfArm(flicker: LegacyFlickerTest) : - OpenAppFromOverviewTest(flicker) { - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt new file mode 100644 index 000000000000..3d9c0679945d --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt @@ -0,0 +1,87 @@ +/* + * 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.server.wm.flicker.launch + +import android.platform.test.annotations.Presubmit +import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.LegacyFlickerTest +import android.tools.device.flicker.legacy.LegacyFlickerTestFactory +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.helpers.TransferSplashscreenAppHelper +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test cold launching an app from launcher + * + * To run this test: `atest FlickerTests:OpenTransferSplashscreenAppFromLauncherTransition` + * + * Actions: + * ``` + * Inherit from OpenAppFromIconColdTest, Launch an app [testApp] with an animated splash screen + * by clicking it's icon on all apps, and wait for transfer splash screen complete + * ``` + * + * Notes: + * ``` + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [OpenAppTransition] + * 2. Verify no flickering when transfer splash screen to app window. + * ``` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class OpenTransferSplashscreenAppFromLauncherTransition(flicker: LegacyFlickerTest) : + OpenAppFromIconColdTest(flicker) { + override val testApp = TransferSplashscreenAppHelper(instrumentation) + + /** + * Checks that [ComponentNameMatcher.LAUNCHER] window is the top window at the start of the + * transition, and is replaced by [ComponentNameMatcher.SPLASH_SCREEN], then [testApp] remains + * visible until the end + */ + @Presubmit + @Test + fun appWindowAfterSplash() { + flicker.assertWm { + this.isAppWindowOnTop(ComponentNameMatcher.LAUNCHER) + .then() + .isAppWindowOnTop(ComponentNameMatcher.SPLASH_SCREEN) + .then() + .isAppWindowOnTop(testApp) + .isAppWindowInvisible(ComponentNameMatcher.SPLASH_SCREEN) + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams() = LegacyFlickerTestFactory.nonRotationTests() + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt index 39c8ca089dbd..b82a12901681 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt @@ -19,8 +19,8 @@ package com.android.server.wm.flicker.launch import android.app.Instrumentation import android.app.WallpaperManager import android.content.res.Resources -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit +import android.tools.common.flicker.subject.layers.LayersTraceSubject.Companion.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS import android.tools.common.traces.component.ComponentNameMatcher import android.tools.common.traces.component.ComponentNameMatcher.Companion.SPLASH_SCREEN import android.tools.common.traces.component.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER @@ -32,6 +32,7 @@ import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.helpers.WindowUtils import android.tools.device.traces.parsers.toFlickerComponent +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.NewTasksAppHelper @@ -121,7 +122,7 @@ class TaskTransitionTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { @FlakyTest(bugId = 265007895) @Test fun transitionHasColorBackground() { - val backgroundColorLayer = ComponentNameMatcher("", "Animation Background") + val backgroundColorLayer = ComponentNameMatcher("", "animation-background") val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation) flicker.assertLayers { this.invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") { @@ -190,6 +191,16 @@ class TaskTransitionTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() { + flicker.assertLayers { + this.visibleLayersShownMoreThanOneConsecutiveEntry( + VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS + listOf(launchNewTaskApp) + ) + } + } + companion object { private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher { val wallpaperManager = WallpaperManager.getInstance(instrumentation.targetContext) diff --git a/tests/ApkVerityTest/ApkVerityTestApp/feature_split/src/com/android/apkverity/feature_x/DummyActivity.java b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt index fe9126003967..b81439e8a1d1 100644 --- a/tests/ApkVerityTest/ApkVerityTestApp/feature_split/src/com/android/apkverity/feature_x/DummyActivity.java +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.apkverity.feature_x; +package com.android.server.wm.flicker.notification -import android.app.Activity; +import android.tools.common.traces.component.ComponentNameMatcher -/** Placeholder class just to generate some dex */ -public class DummyActivity extends Activity {} +object Consts { + val IMAGE_WALLPAPER = ComponentNameMatcher("", "com.android.systemui.wallpapers.ImageWallpaper") +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OWNERS b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OWNERS new file mode 100644 index 000000000000..68c37bd82faa --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OWNERS @@ -0,0 +1,2 @@ +# Android > Android OS & Apps > System UI > Internal Only > Notifications +# https://b.corp.google.com/issues/new?component=181562&template=686802 diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt index 6f5daeb22bed..6819a38c9067 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt @@ -17,8 +17,10 @@ package com.android.server.wm.flicker.notification import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit import android.platform.test.rule.SettingOverrideRule import android.provider.Settings +import android.tools.common.traces.component.ComponentNameMatcher import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest @@ -107,6 +109,21 @@ open class OpenAppFromLockscreenNotificationColdTest(flicker: LegacyFlickerTest) override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() { + flicker.assertWm { + this.visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf( + ComponentNameMatcher.SPLASH_SCREEN, + ComponentNameMatcher.SNAPSHOT, + ComponentNameMatcher.SECONDARY_HOME_HANDLE, + Consts.IMAGE_WALLPAPER + ) + ) + } + } + companion object { /** * Creates the test configurations. diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt index 483caa71abee..972221640363 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.notification -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.platform.test.rule.SettingOverrideRule import android.provider.Settings @@ -25,6 +24,7 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.statusBarLayerPositionAtEnd import org.junit.ClassRule @@ -139,6 +139,21 @@ class OpenAppFromLockscreenNotificationWarmTest(flicker: LegacyFlickerTest) : override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() { + flicker.assertWm { + this.visibleWindowsShownMoreThanOneConsecutiveEntry( + listOf( + ComponentNameMatcher.SPLASH_SCREEN, + ComponentNameMatcher.SNAPSHOT, + ComponentNameMatcher.SECONDARY_HOME_HANDLE, + Consts.IMAGE_WALLPAPER + ) + ) + } + } + companion object { /** * Creates the test configurations. @@ -159,7 +174,7 @@ class OpenAppFromLockscreenNotificationWarmTest(flicker: LegacyFlickerTest) : val disableUnseenNotifFilterRule = SettingOverrideRule( Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, - /* value= */ "0", + "0", ) } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt index 4e8a697d8902..ffd81716e728 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.notification -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.common.traces.component.ComponentNameMatcher @@ -25,6 +24,7 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.helpers.wakeUpAndGoToHomeScreen +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.ShowWhenLockedAppHelper import org.junit.FixMethodOrder @@ -59,6 +59,10 @@ class OpenAppFromLockscreenNotificationWithOverlayAppTest(flicker: LegacyFlicker get() = { super.transition(this) + transitions { + wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify() + } + setup { device.wakeUpAndGoToHomeScreen() diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt index 50dec3bf2f02..fbdba7b71d0f 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt @@ -23,7 +23,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.statusBarLayerPositionAtEnd import org.junit.FixMethodOrder import org.junit.Ignore @@ -37,7 +36,6 @@ import org.junit.runners.Parameterized * * To run this test: `atest FlickerTests:OpenAppFromNotificationCold` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTestCfArm.kt deleted file mode 100644 index a147171ed936..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTestCfArm.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.server.wm.flicker.notification - -import android.platform.test.annotations.Postsubmit -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Postsubmit -class OpenAppFromNotificationColdTestCfArm(flicker: LegacyFlickerTest) : - OpenAppFromNotificationColdTest(flicker) { - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt index 19a070b1a402..2aa444e45ea2 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt @@ -26,7 +26,6 @@ import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.helpers.wakeUpAndGoToHomeScreen import android.view.WindowInsets import android.view.WindowManager -import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.NotificationAppHelper @@ -49,7 +48,6 @@ import org.junit.runners.Parameterized * * To run this test: `atest FlickerTests:OpenAppFromNotificationWarm` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @@ -69,7 +67,11 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify() testApp.postNotification(wmHelper) device.pressHome() - wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() + wmHelper + .StateSyncBuilder() + .withHomeActivityVisible() + .withWindowSurfaceDisappeared(ComponentNameMatcher.NOTIFICATION_SHADE) + .waitForAndVerify() } transitions { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTestCfArm.kt deleted file mode 100644 index 98356d7d1e87..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTestCfArm.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.server.wm.flicker.notification - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class OpenAppFromNotificationWarmTestCfArm(flicker: LegacyFlickerTest) : - OpenAppFromNotificationWarmTest(flicker) { - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt index 7883910323de..13fcc2ba0b54 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.quickswitch -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.NavBar import android.tools.common.datatypes.Rect @@ -25,7 +24,7 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.NonResizeableAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper @@ -48,11 +47,10 @@ import org.junit.runners.Parameterized * Swipe right from the bottom of the screen to quick switch back to the first app [testApp1] * ``` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class QuickSwitchBetweenTwoAppsBackTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class QuickSwitchBetweenTwoAppsBackTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp1 = SimpleAppHelper(instrumentation) private val testApp2 = NonResizeableAppHelper(instrumentation) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt deleted file mode 100644 index f68cd5c4293c..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTestCfArm.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.server.wm.flicker.quickswitch - -import android.tools.common.NavBar -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class QuickSwitchBetweenTwoAppsBackTestCfArm(flicker: LegacyFlickerTest) : - QuickSwitchBetweenTwoAppsBackTest(flicker) { - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = - LegacyFlickerTestFactory.nonRotationTests( - supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) - ) - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt index 1c4c7cd89e42..c09041518296 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.quickswitch -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.NavBar import android.tools.common.datatypes.Rect @@ -25,7 +24,7 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.NonResizeableAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper @@ -49,11 +48,10 @@ import org.junit.runners.Parameterized * Swipe left from the bottom of the screen to quick switch forward to the second app [testApp2] * ``` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class QuickSwitchBetweenTwoAppsForwardTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class QuickSwitchBetweenTwoAppsForwardTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp1 = SimpleAppHelper(instrumentation) private val testApp2 = NonResizeableAppHelper(instrumentation) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt deleted file mode 100644 index 3de58acccd79..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTestCfArm.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.server.wm.flicker.quickswitch - -import android.tools.common.NavBar -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class QuickSwitchBetweenTwoAppsForwardTestCfArm(flicker: LegacyFlickerTest) : - QuickSwitchBetweenTwoAppsForwardTest(flicker) { - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = - LegacyFlickerTestFactory.nonRotationTests( - supportedNavigationModes = listOf(NavBar.MODE_GESTURAL) - ) - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt index 641745693187..f51be908750a 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.quickswitch -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.NavBar import android.tools.common.Rotation @@ -26,7 +25,7 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.SimpleAppHelper import org.junit.FixMethodOrder @@ -48,11 +47,10 @@ import org.junit.runners.Parameterized * Swipe right from the bottom of the screen to quick switch back to the app * ``` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class QuickSwitchFromLauncherTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { +class QuickSwitchFromLauncherTest(flicker: LegacyFlickerTest) : BaseTest(flicker) { private val testApp = SimpleAppHelper(instrumentation) /** {@inheritDoc} */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt deleted file mode 100644 index 84fc75445175..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTestCfArm.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.server.wm.flicker.quickswitch - -import android.tools.common.NavBar -import android.tools.common.Rotation -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class QuickSwitchFromLauncherTestCfArm(flicker: LegacyFlickerTest) : - QuickSwitchFromLauncherTest(flicker) { - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = - LegacyFlickerTestFactory.nonRotationTests( - supportedNavigationModes = listOf(NavBar.MODE_GESTURAL), - // TODO: Test with 90 rotation - supportedRotations = listOf(Rotation.ROTATION_0) - ) - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt index 842ece38c282..bdbf0d24e624 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt @@ -23,7 +23,6 @@ import android.tools.device.flicker.junit.FlickerParametersRunnerFactory import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.SimpleAppHelper import org.junit.FixMethodOrder import org.junit.Test @@ -81,11 +80,10 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class ChangeAppRotationTest(flicker: LegacyFlickerTest) : RotationTransition(flicker) { +class ChangeAppRotationTest(flicker: LegacyFlickerTest) : RotationTransition(flicker) { override val testApp = SimpleAppHelper(instrumentation) override val transition: FlickerBuilder.() -> Unit get() = { @@ -100,7 +98,7 @@ open class ChangeAppRotationTest(flicker: LegacyFlickerTest) : RotationTransitio @Presubmit @Test fun focusChanges() { - flicker.assertEventLog { this.focusChanges(testApp.`package`) } + flicker.assertEventLog { this.focusChanges(testApp.packageName) } } /** diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTestCfArm.kt deleted file mode 100644 index 1ab5c5ad77b5..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTestCfArm.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.server.wm.flicker.rotation - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ChangeAppRotationTestCfArm(flicker: LegacyFlickerTest) : ChangeAppRotationTest(flicker) { - companion object { - /** - * Creates the test configurations. - * - * See [LegacyFlickerTestFactory.rotationTests] for configuring screen orientation and - * navigation modes. - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = LegacyFlickerTestFactory.rotationTests() - } -} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt index b6ad3cc7fc50..6d3ae43c1472 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt @@ -26,7 +26,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.view.WindowManager -import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.SeamlessRotationAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions import org.junit.FixMethodOrder @@ -88,11 +87,10 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ -@RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class SeamlessAppRotationTest(flicker: LegacyFlickerTest) : RotationTransition(flicker) { +class SeamlessAppRotationTest(flicker: LegacyFlickerTest) : RotationTransition(flicker) { override val testApp = SeamlessRotationAppHelper(instrumentation) /** {@inheritDoc} */ @@ -118,8 +116,8 @@ open class SeamlessAppRotationTest(flicker: LegacyFlickerTest) : RotationTransit flicker.assertWm { this.invoke("isFullScreen") { val appWindow = - it.windowState(testApp.`package`) - ?: error("App window for package ${testApp.`package`} not found") + it.windowState(testApp.packageName) + ?: error("App window for package ${testApp.packageName} not found") val flags = appWindow.windowState.attributes.flags appWindow .check { "isFullScreen" } @@ -136,8 +134,8 @@ open class SeamlessAppRotationTest(flicker: LegacyFlickerTest) : RotationTransit flicker.assertWm { this.invoke("isRotationSeamless") { val appWindow = - it.windowState(testApp.`package`) - ?: error("App window for package ${testApp.`package`} not found") + it.windowState(testApp.packageName) + ?: error("App window for package ${testApp.packageName} not found") val rotationAnimation = appWindow.windowState.attributes.rotationAnimation appWindow .check { "isRotationSeamless" } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTestCfArm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTestCfArm.kt deleted file mode 100644 index 592be05c8182..000000000000 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTestCfArm.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.server.wm.flicker.rotation - -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import android.tools.device.flicker.legacy.LegacyFlickerTestFactory -import com.android.server.wm.flicker.testapp.ActivityOptions -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** This test should fail because of b/264518826 */ -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -open class SeamlessAppRotationTestCfArm(flicker: LegacyFlickerTest) : - SeamlessAppRotationTest(flicker) { - companion object { - /** - * Creates the test configurations for seamless rotation based on the default rotation tests - * from [LegacyFlickerTestFactory.rotationTests], but adding a flag ( - * [ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD]) to indicate if the app should - * starve the UI thread of not - */ - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams() = - LegacyFlickerTestFactory.rotationTests().flatMap { sourceCfg -> - val legacyCfg = sourceCfg as LegacyFlickerTest - val defaultRun = createConfig(legacyCfg, starveUiThread = false) - val busyUiRun = createConfig(legacyCfg, starveUiThread = true) - listOf(defaultRun, busyUiRun) - } - } -} diff --git a/tests/FlickerTests/test-apps/flickerapp/Android.bp b/tests/FlickerTests/test-apps/flickerapp/Android.bp index 75e35ee9c765..e3b23b986c83 100644 --- a/tests/FlickerTests/test-apps/flickerapp/Android.bp +++ b/tests/FlickerTests/test-apps/flickerapp/Android.bp @@ -24,6 +24,9 @@ package { android_test { name: "FlickerTestApp", srcs: ["**/*.java"], + resource_dirs: [ + "res", + ], sdk_version: "current", test_suites: ["device-tests"], static_libs: [ diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index ff9799a1c710..f867c989232d 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -70,7 +70,7 @@ <activity android:name=".SeamlessRotationActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.SeamlessRotationActivity" android:theme="@style/CutoutShortEdges" - android:configChanges="orientation|screenSize" + android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize" android:label="SeamlessActivity" android:exported="true"> <intent-filter> @@ -347,6 +347,17 @@ android:exported="false" android:theme="@style/CutoutShortEdges" android:resizeableActivity="true"/> + <activity + android:name=".TransferSplashscreenActivity" + android:taskAffinity="com.android.server.wm.flicker.testapp.TransferSplashscreenActivity" + android:label="TransferSplashscreenActivity" + android:theme="@style/SplashscreenAppTheme" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> <service android:name=".AssistantInteractionSessionService" android:exported="true" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/drawable/avd_anim.xml b/tests/FlickerTests/test-apps/flickerapp/res/drawable/avd_anim.xml new file mode 100644 index 000000000000..19205d4d1c14 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/drawable/avd_anim.xml @@ -0,0 +1,94 @@ +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector android:height="432dp" android:width="432dp" android:viewportHeight="432" android:viewportWidth="432"> + <group android:name="_R_G"> + <group android:name="_R_G_L_5_G" android:translateX="216" android:translateY="216" android:scaleX="1.5" android:scaleY="1.5"> + <path android:name="_R_G_L_5_G_D_0_P_0" android:fillColor="#555555" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M53.82 -56.7 C53.82,-56.7 43.64,-49.06 43.64,-49.06 C43.64,-49.06 0,-16.34 0,-16.34 C0,-16.34 -43.64,-49.06 -43.64,-49.06 C-43.64,-49.06 -53.82,-56.7 -53.82,-56.7 C-64.6,-64.79 -80,-57.09 -80,-43.61 C-80,-43.61 -80,-29.07 -80,-29.07 C-80,-29.07 -80,49.09 -80,49.09 C-80,55.12 -75.12,60 -69.09,60 C-69.09,60 -43.64,60 -43.64,60 C-43.64,60 -43.64,-1.8 -43.64,-1.8 C-43.64,-1.8 0,30.92 0,30.92 C0,30.92 43.64,-1.8 43.64,-1.8 C43.64,-1.8 43.64,60 43.64,60 C43.64,60 69.09,60 69.09,60 C75.12,60 80,55.12 80,49.09 C80,49.09 80,-29.07 80,-29.07 C80,-29.07 80,-43.61 80,-43.61 C80,-57.09 64.61,-64.79 53.82,-56.7c "/> + </group> + <group android:name="_R_G_L_4_G" android:translateX="216" android:translateY="216" android:scaleX="1.5" android:scaleY="1.5"> + <clip-path android:name="mask_x" android:pathData="M53.82 -56.7 C53.82,-56.7 43.64,-49.06 43.64,-49.06 C43.64,-49.06 0,-16.34 0,-16.34 C0,-16.34 -43.64,-49.06 -43.64,-49.06 C-43.64,-49.06 -53.82,-56.7 -53.82,-56.7 C-64.6,-64.79 -80,-57.09 -80,-43.61 C-80,-43.61 -80,-29.07 -80,-29.07 C-80,-29.07 -80,49.09 -80,49.09 C-80,55.12 -75.12,60 -69.09,60 C-69.09,60 -43.64,60 -43.64,60 C-43.64,60 -43.64,-1.8 -43.64,-1.8 C-43.64,-1.8 0,30.92 0,30.92 C0,30.92 43.64,-1.8 43.64,-1.8 C43.64,-1.8 43.64,60 43.64,60 C43.64,60 69.09,60 69.09,60 C75.12,60 80,55.12 80,49.09 C80,49.09 80,-29.07 80,-29.07 C80,-29.07 80,-43.61 80,-43.61 C80,-57.09 64.61,-64.79 53.82,-56.7c"/> + <path android:name="_R_G_L_4_G_D_0_P_0" android:fillColor="#2684fc" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-80.39 60 C-80.39,60 -105.84,60 -105.84,60 C-111.87,60 -116.75,55.12 -116.75,49.09 C-116.75,49.09 -116.75,-60 -116.75,-60 C-116.75,-60 -80.39,-60 -80.39,-60 C-80.39,-60 -80.39,60 -80.39,60c "/> + </group> + <group android:name="_R_G_L_3_G" android:translateX="216" android:translateY="216" android:scaleX="1.5" android:scaleY="1.5"> + <clip-path android:name="mask_x" android:pathData="M53.82 -56.7 C53.82,-56.7 43.64,-49.06 43.64,-49.06 C43.64,-49.06 0,-16.34 0,-16.34 C0,-16.34 -43.64,-49.06 -43.64,-49.06 C-43.64,-49.06 -53.82,-56.7 -53.82,-56.7 C-64.6,-64.79 -80,-57.09 -80,-43.61 C-80,-43.61 -80,-29.07 -80,-29.07 C-80,-29.07 -80,49.09 -80,49.09 C-80,55.12 -75.12,60 -69.09,60 C-69.09,60 -43.64,60 -43.64,60 C-43.64,60 -43.64,-1.8 -43.64,-1.8 C-43.64,-1.8 0,30.92 0,30.92 C0,30.92 43.64,-1.8 43.64,-1.8 C43.64,-1.8 43.64,60 43.64,60 C43.64,60 69.09,60 69.09,60 C75.12,60 80,55.12 80,49.09 C80,49.09 80,-29.07 80,-29.07 C80,-29.07 80,-43.61 80,-43.61 C80,-57.09 64.61,-64.79 53.82,-56.7c"/> + <path android:name="_R_G_L_3_G_D_0_P_0" android:fillColor="#00ac47" android:fillAlpha="1" android:fillType="nonZero" android:pathData="M80.64 60 C80.64,60 106.09,60 106.09,60 C112.12,60 117,55.12 117,49.09 C117,49.09 117,-60 117,-60 C117,-60 80.64,-60 80.64,-60 C80.64,-60 80.64,60 80.64,60c "/> + </group> + <group android:name="_R_G_L_2_G" android:translateX="216" android:translateY="216" android:scaleX="1.5" android:scaleY="1.5"> + <clip-path android:name="mask_x" android:pathData="M53.82 -56.7 C53.82,-56.7 43.64,-49.06 43.64,-49.06 C43.64,-49.06 0,-16.34 0,-16.34 C0,-16.34 -43.64,-49.06 -43.64,-49.06 C-43.64,-49.06 -53.82,-56.7 -53.82,-56.7 C-64.6,-64.79 -80,-57.09 -80,-43.61 C-80,-43.61 -80,-29.07 -80,-29.07 C-80,-29.07 -80,49.09 -80,49.09 C-80,55.12 -75.12,60 -69.09,60 C-69.09,60 -43.64,60 -43.64,60 C-43.64,60 -43.64,-1.8 -43.64,-1.8 C-43.64,-1.8 0,30.92 0,30.92 C0,30.92 43.64,-1.8 43.64,-1.8 C43.64,-1.8 43.64,60 43.64,60 C43.64,60 69.09,60 69.09,60 C75.12,60 80,55.12 80,49.09 C80,49.09 80,-29.07 80,-29.07 C80,-29.07 80,-43.61 80,-43.61 C80,-57.09 64.61,-64.79 53.82,-56.7c"/> + <path android:name="_R_G_L_2_G_D_0_P_0" android:fillColor="#fe2c25" android:fillAlpha="1" android:fillType="nonZero" android:pathData="M53.82 -104.7 C53.82,-104.7 0,-64.34 0,-64.34 C0,-64.34 -53.82,-104.7 -53.82,-104.7 C-64.6,-112.79 -80,-105.09 -80,-91.61 C-80,-91.61 -80,-77.07 -80,-77.07 C-80,-77.07 0,-17.08 0,-17.08 C0,-17.08 80,-77.07 80,-77.07 C80,-77.07 80,-91.61 80,-91.61 C80,-105.09 64.61,-112.79 53.82,-104.7c "/> + </group> + <group android:name="_R_G_L_1_G" android:translateX="216" android:translateY="216" android:scaleX="1.5" android:scaleY="1.5"> + <clip-path android:name="mask_x" android:pathData="M43.64 -1.8 C43.64,-1.8 43.64,-49.06 43.64,-49.06 C43.64,-49.06 53.82,-56.7 53.82,-56.7 C64.61,-64.79 80,-57.09 80,-43.61 C80,-43.61 80,-1.8 80,-1.8 C80,-1.8 43.64,-1.8 43.64,-1.8c"/> + <path android:name="_R_G_L_1_G_D_0_P_0" android:fillColor="#ffba00" android:fillAlpha="1" android:fillType="nonZero" android:pathData="M80.64 -135 C80.64,-135 117,-135 117,-135 C117,-135 117,-104.07 117,-104.07 C117,-104.07 80.64,-76.8 80.64,-76.8 C80.64,-76.8 80.64,-135 80.64,-135c "/> + </group> + <group android:name="_R_G_L_0_G" android:translateX="216" android:translateY="216" android:scaleX="1.5" android:scaleY="1.5"> + <clip-path android:name="mask_x" android:pathData="M-43.64 -1.8 C-43.64,-1.8 -80,-1.8 -80,-1.8 C-80,-1.8 -80,-43.61 -80,-43.61 C-80,-57.09 -64.6,-64.79 -53.82,-56.7 C-53.82,-56.7 -43.64,-49.06 -43.64,-49.06 C-43.64,-49.06 -43.64,-1.8 -43.64,-1.8c"/> + <path android:name="_R_G_L_0_G_D_0_P_0" android:fillColor="#d70007" android:fillAlpha="1" android:fillType="nonZero" android:pathData=" M-117 -104.07 C-117,-104.07 -117,-135 -117,-135 C-117,-135 -80.64,-135 -80.64,-135 C-80.64,-135 -80.64,-76.8 -80.64,-76.8 C-80.64,-76.8 -117,-104.07 -117,-104.07c "/> + </group> + </group> + <group android:name="time_group"/> + </vector> + </aapt:attr> + <target android:name="_R_G_L_4_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="1567" android:startOffset="233" android:valueFrom="M-80.39 60 C-80.39,60 -105.84,60 -105.84,60 C-111.87,60 -116.75,55.12 -116.75,49.09 C-116.75,49.09 -116.75,-60 -116.75,-60 C-116.75,-60 -80.39,-60 -80.39,-60 C-80.39,-60 -80.39,60 -80.39,60c " android:valueTo=" M-43.64 60 C-43.64,60 -69.09,60 -69.09,60 C-75.12,60 -80,55.12 -80,49.09 C-80,49.09 -80,-60 -80,-60 C-80,-60 -43.64,-60 -43.64,-60 C-43.64,-60 -43.64,60 -43.64,60c " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_3_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="1567" android:startOffset="233" android:valueFrom=" M80.64 60 C80.64,60 106.09,60 106.09,60 C112.12,60 117,55.12 117,49.09 C117,49.09 117,-60 117,-60 C117,-60 80.64,-60 80.64,-60 C80.64,-60 80.64,60 80.64,60c " android:valueTo=" M43.64 60 C43.64,60 69.09,60 69.09,60 C75.12,60 80,55.12 80,49.09 C80,49.09 80,-60 80,-60 C80,-60 43.64,-60 43.64,-60 C43.64,-60 43.64,60 43.64,60c " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_2_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="1567" android:startOffset="233" android:valueFrom="M53.82 -104.7 C53.82,-104.7 0,-64.34 0,-64.34 C0,-64.34 -53.82,-104.7 -53.82,-104.7 C-64.6,-112.79 -80,-105.09 -80,-91.61 C-80,-91.61 -80,-77.07 -80,-77.07 C-80,-77.07 0,-17.08 0,-17.08 C0,-17.08 80,-77.07 80,-77.07 C80,-77.07 80,-91.61 80,-91.61 C80,-105.09 64.61,-112.79 53.82,-104.7c" android:valueTo="M53.82 -56.7 C53.82,-56.7 0,-16.34 0,-16.34 C0,-16.34 -53.82,-56.7 -53.82,-56.7 C-64.6,-64.79 -80,-57.09 -80,-43.61 C-80,-43.61 -80,-29.07 -80,-29.07 C-80,-29.07 0,30.92 0,30.92 C0,30.92 80,-29.07 80,-29.07 C80,-29.07 80,-43.61 80,-43.61 C80,-57.09 64.61,-64.79 53.82,-56.7c " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="1567" android:startOffset="233" android:valueFrom=" M80.64 -135 C80.64,-135 117,-135 117,-135 C117,-135 117,-104.07 117,-104.07 C117,-104.07 80.64,-76.8 80.64,-76.8 C80.64,-76.8 80.64,-135 80.64,-135c " android:valueTo=" M43.64 -60 C43.64,-60 80,-60 80,-60 C80,-60 80,-29.07 80,-29.07 C80,-29.07 43.64,-1.8 43.64,-1.8 C43.64,-1.8 43.64,-60 43.64,-60c " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="pathData" android:duration="1567" android:startOffset="233" android:valueFrom=" M-117 -104.07 C-117,-104.07 -117,-135 -117,-135 C-117,-135 -80.64,-135 -80.64,-135 C-80.64,-135 -80.64,-76.8 -80.64,-76.8 C-80.64,-76.8 -117,-104.07 -117,-104.07c " android:valueTo=" M-80 -29.07 C-80,-29.07 -80,-60 -80,-60 C-80,-60 -43.64,-60 -43.64,-60 C-43.64,-60 -43.64,-1.8 -43.64,-1.8 C-43.64,-1.8 -80,-29.07 -80,-29.07c " android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0,1 1.0,1.0"/> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator android:propertyName="translateX" android:duration="2017" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/> + </set> + </aapt:attr> + </target> +</animated-vector> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml index e51ed29adebf..9b742d96e35b 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml @@ -53,4 +53,11 @@ <style name="no_starting_window" parent="@android:style/Theme.DeviceDefault"> <item name="android:windowDisablePreview">true</item> </style> + + <style name="SplashscreenAppTheme" parent="@android:style/Theme.DeviceDefault"> + <!-- Splashscreen Attributes --> + <item name="android:windowSplashScreenAnimatedIcon">@drawable/avd_anim</item> + <!-- Here we want to match the duration of our AVD --> + <item name="android:windowSplashScreenAnimationDuration">900</item> + </style> </resources> diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java index 2795a6c43015..7c5e9a3f86b5 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java @@ -178,6 +178,12 @@ public class ActivityOptions { FLICKER_APP_PACKAGE + ".LaunchNewActivity"); } + public static class TransferSplashscreenActivity { + public static final String LABEL = "TransferSplashscreenActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".TransferSplashscreenActivity"); + } + public static class Pip { // Test App > Pip Activity public static final String LABEL = "PipActivity"; diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransferSplashscreenActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransferSplashscreenActivity.java new file mode 100644 index 000000000000..0323286a0e21 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransferSplashscreenActivity.java @@ -0,0 +1,52 @@ +/* + * 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.server.wm.flicker.testapp; + +import android.app.Activity; +import android.os.Bundle; +import android.os.SystemClock; +import android.view.View; +import android.view.ViewTreeObserver; +import android.window.SplashScreen; +import android.window.SplashScreenView; + +public class TransferSplashscreenActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final SplashScreen splashScreen = getSplashScreen(); + // Register setOnExitAnimationListener to transfer the splash screen window to client. + splashScreen.setOnExitAnimationListener(this::onSplashScreenExit); + final View content = findViewById(android.R.id.content); + // By register preDrawListener to defer app window draw signal about 500ms, which to ensure + // the splash screen must show when cold launch. + content.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + final long mCreateTime = SystemClock.uptimeMillis(); + @Override + public boolean onPreDraw() { + return SystemClock.uptimeMillis() - mCreateTime > 500; + } + } + ); + } + + private void onSplashScreenExit(SplashScreenView view) { + view.remove(); + } +} diff --git a/tests/ApkVerityTest/Android.bp b/tests/FsVerityTest/Android.bp index f026bea80470..53606a32b185 100644 --- a/tests/ApkVerityTest/Android.bp +++ b/tests/FsVerityTest/Android.bp @@ -22,7 +22,7 @@ package { } java_test_host { - name: "ApkVerityTest", + name: "FsVerityTest", srcs: ["src/**/*.java"], libs: [ "tradefed", @@ -30,8 +30,10 @@ java_test_host { "compatibility-host-util", ], static_libs: [ + "android.security.flags-aconfig-java-host", "block_device_writer_jar", "frameworks-base-hostutils", + "flag-junit-host", ], test_suites: [ "general-tests", @@ -41,14 +43,6 @@ java_test_host { "block_device_writer", ], data: [ - ":ApkVerityTestCertDer", - ":ApkVerityTestApp", - ":ApkVerityTestAppFsvSig", - ":ApkVerityTestAppDm", - ":ApkVerityTestAppDmFsvSig", - ":ApkVerityTestAppSplit", - ":ApkVerityTestAppSplitFsvSig", - ":ApkVerityTestAppSplitDm", - ":ApkVerityTestAppSplitDmFsvSig", + ":FsVerityTestApp", ], } diff --git a/tests/ApkVerityTest/AndroidTest.xml b/tests/FsVerityTest/AndroidTest.xml index 4487cefb4f9c..49cbde0d4611 100644 --- a/tests/ApkVerityTest/AndroidTest.xml +++ b/tests/FsVerityTest/AndroidTest.xml @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> -<configuration description="APK fs-verity integration/regression test"> +<configuration description="fs-verity end-to-end test"> <option name="test-suite-tag" value="apct" /> <object type="module_controller" class="com.android.tradefed.testtype.suite.module.ShippingApiLevelModuleController"> @@ -24,19 +24,9 @@ <!-- This test requires root to write against block device. --> <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> - <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> - <!-- Disable package verifier prevents it holding the target APK's fd that prevents cache - eviction. --> - <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" /> - <option name="restore-settings" value="true" /> - - <!-- Skip in order to prevent reboot that confuses the test flow. --> - <option name="force-skip-system-props" value="true" /> - </target_preparer> - - <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> - <option name="cleanup" value="true" /> - <option name="push" value="ApkVerityTestCert.der->/data/local/tmp/ApkVerityTestCert.der" /> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="FsVerityTestApp.apk"/> + <option name="cleanup-apks" value="true"/> </target_preparer> <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> @@ -48,9 +38,7 @@ <option name="push" value="block_device_writer->/data/local/tmp/block_device_writer" /> </target_preparer> - <!-- Skip on HWASan. TODO(b/232288278): Re-enable --> - <object type="module_controller" class="com.android.tradefed.testtype.suite.module.SkipHWASanModuleController" /> <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > - <option name="jar" value="ApkVerityTest.jar" /> + <option name="jar" value="FsVerityTest.jar" /> </test> </configuration> diff --git a/tests/ApkVerityTest/ApkVerityTestApp/Android.bp b/tests/FsVerityTest/FsVerityTestApp/Android.bp index adf8f9f9d321..43da3ff9fec1 100644 --- a/tests/ApkVerityTest/ApkVerityTestApp/Android.bp +++ b/tests/FsVerityTest/FsVerityTestApp/Android.bp @@ -22,17 +22,8 @@ package { } android_test_helper_app { - name: "ApkVerityTestApp", - manifest: "AndroidManifest.xml", - srcs: ["src/**/*.java"], -} - -android_test_helper_app { - name: "ApkVerityTestAppSplit", - manifest: "feature_split/AndroidManifest.xml", - srcs: ["src/**/*.java"], - aaptflags: [ - "--custom-package com.android.apkverity.feature_x", - "--package-id 0x80", - ], + name: "FsVerityTestApp", + manifest: "AndroidManifest.xml", + srcs: ["src/**/*.java"], + static_libs: ["compatibility-device-util-axt"], } diff --git a/tests/ApkVerityTest/ApkVerityTestApp/AndroidManifest.xml b/tests/FsVerityTest/FsVerityTestApp/AndroidManifest.xml index 0b3ff77c2cdf..42fe49be66d9 100644 --- a/tests/ApkVerityTest/ApkVerityTestApp/AndroidManifest.xml +++ b/tests/FsVerityTest/FsVerityTestApp/AndroidManifest.xml @@ -16,8 +16,12 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.apkverity"> + package="com.android.fsverity"> <application> <activity android:name=".DummyActivity"/> </application> + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.fsverity" + android:label="Helper app of fs-verity test"> + </instrumentation>/> </manifest> diff --git a/tests/FsVerityTest/FsVerityTestApp/src/com/android/fsverity/Helper.java b/tests/FsVerityTest/FsVerityTestApp/src/com/android/fsverity/Helper.java new file mode 100644 index 000000000000..2ed4fec4a93c --- /dev/null +++ b/tests/FsVerityTest/FsVerityTestApp/src/com/android/fsverity/Helper.java @@ -0,0 +1,108 @@ +/* + * 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.fsverity; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.content.Context; +import android.security.FileIntegrityManager; +import android.util.Log; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Test helper that works with the host-side test to set up a test file, and to verify fs-verity + * verification is done expectedly. + */ +public class Helper { + private static final String TAG = "FsVerityTest"; + + private static final String FILENAME = "test.file"; + + private static final long BLOCK_SIZE = 4096; + + @Test + public void prepareTest() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + android.os.Bundle testArgs = InstrumentationRegistry.getArguments(); + + String basename = testArgs.getString("basename"); + context.deleteFile(basename); + + assertThat(testArgs).isNotNull(); + int fileSize = Integer.parseInt(testArgs.getString("fileSize")); + Log.d(TAG, "Preparing test file with size " + fileSize); + + byte[] bytes = new byte[8192]; + Arrays.fill(bytes, (byte) '1'); + try (FileOutputStream os = context.openFileOutput(basename, Context.MODE_PRIVATE)) { + for (int i = 0; i < fileSize; i += bytes.length) { + if (i + bytes.length > fileSize) { + os.write(bytes, 0, fileSize % bytes.length); + } else { + os.write(bytes); + } + } + } + + // Enable fs-verity + FileIntegrityManager fim = context.getSystemService(FileIntegrityManager.class); + fim.setupFsVerity(context.getFileStreamPath(basename)); + } + + @Test + public void verifyFileRead() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + + // Collect indices that the backing blocks are supposed to be corrupted. + android.os.Bundle testArgs = InstrumentationRegistry.getArguments(); + assertThat(testArgs).isNotNull(); + String filePath = testArgs.getString("filePath"); + String csv = testArgs.getString("brokenBlockIndicesCsv"); + Log.d(TAG, "brokenBlockIndicesCsv: " + csv); + String[] strings = csv.split(","); + var corrupted = new ArrayList(strings.length); + for (int i = 0; i < strings.length; i++) { + corrupted.add(Integer.parseInt(strings[i])); + } + + // Expect the read to succeed or fail per the prior. + try (var file = new RandomAccessFile(filePath, "r")) { + long total_blocks = (file.length() + BLOCK_SIZE - 1) / BLOCK_SIZE; + for (int i = 0; i < (int) total_blocks; i++) { + file.seek(i * BLOCK_SIZE); + if (corrupted.contains(i)) { + Log.d(TAG, "Expecting read at block #" + i + " to fail"); + assertThrows(IOException.class, () -> file.read()); + } else { + assertThat(file.readByte()).isEqualTo('1'); + } + } + } + } +} diff --git a/tests/ApkVerityTest/OWNERS b/tests/FsVerityTest/OWNERS index d67285ede44a..d67285ede44a 100644 --- a/tests/ApkVerityTest/OWNERS +++ b/tests/FsVerityTest/OWNERS diff --git a/tests/ApkVerityTest/TEST_MAPPING b/tests/FsVerityTest/TEST_MAPPING index 72d96148c69f..39944bed3f60 100644 --- a/tests/ApkVerityTest/TEST_MAPPING +++ b/tests/FsVerityTest/TEST_MAPPING @@ -1,11 +1,11 @@ { - "presubmit": [ + "postsubmit": [ { - "name": "ApkVerityTest" + "name": "FsVerityTest" }, // nextgen test only runs during postsubmit. { - "name": "ApkVerityTest", + "name": "FsVerityTest", "keywords": ["nextgen"] } ] diff --git a/tests/ApkVerityTest/block_device_writer/Android.bp b/tests/FsVerityTest/block_device_writer/Android.bp index 0002447d17f2..0002447d17f2 100644 --- a/tests/ApkVerityTest/block_device_writer/Android.bp +++ b/tests/FsVerityTest/block_device_writer/Android.bp diff --git a/tests/ApkVerityTest/block_device_writer/block_device_writer.cpp b/tests/FsVerityTest/block_device_writer/block_device_writer.cpp index 02dfd732a716..02dfd732a716 100644 --- a/tests/ApkVerityTest/block_device_writer/block_device_writer.cpp +++ b/tests/FsVerityTest/block_device_writer/block_device_writer.cpp diff --git a/tests/ApkVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java b/tests/FsVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java index 9be02ec3be86..9be02ec3be86 100644 --- a/tests/ApkVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java +++ b/tests/FsVerityTest/block_device_writer/src/com/android/blockdevicewriter/BlockDeviceWriter.java diff --git a/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java b/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java new file mode 100644 index 000000000000..be479f205ff2 --- /dev/null +++ b/tests/FsVerityTest/src/com/android/fsverity/FsVerityHostTest.java @@ -0,0 +1,103 @@ +/* + * 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.fsverity; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.annotations.RootPermissionTest; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.host.HostFlagsValueProvider; +import android.security.Flags; + +import com.android.blockdevicewriter.BlockDeviceWriter; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import com.android.tradefed.testtype.junit4.DeviceTestRunOptions; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * This test verifies fs-verity works end-to-end. There is a corresponding helper app. + * + * <p>The helper app uses a FileIntegrityManager API to enable fs-verity to a file. The host test + * here * tampers with the file's backing storage, then tells the helper app to read and expect + * success/failure on read. + * + * <p>In order to make sure a block of the file is readable only if the underlying block on disk + * stay intact, the test needs to bypass the filesystem and tampers with the corresponding physical + * address against the block device. + */ +@RootPermissionTest +@RunWith(DeviceJUnit4ClassRunner.class) +@RequiresFlagsEnabled(Flags.FLAG_FSVERITY_API) +public class FsVerityHostTest extends BaseHostJUnit4Test { + private static final String TARGET_PACKAGE = "com.android.fsverity"; + + private static final String BASENAME = "test.file"; + private static final String TARGET_PATH = "/data/data/" + TARGET_PACKAGE + "/files/" + BASENAME; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + HostFlagsValueProvider.createCheckFlagsRule(this::getDevice); + + @Test + public void testFsVeritySmallFile() throws Exception { + prepareTest(10000); + + ITestDevice device = getDevice(); + BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 0); + BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 8192); + BlockDeviceWriter.dropCaches(device); + + verifyRead(TARGET_PATH, "0,2"); + } + + @Test + public void testFsVerityLargerFileWithOneMoreMerkleTreeLevel() throws Exception { + prepareTest(128 * 4096 + 1); + + ITestDevice device = getDevice(); + BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 4096); + BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 100 * 4096); + BlockDeviceWriter.damageFileAgainstBlockDevice(device, TARGET_PATH, 128 * 4096 + 1); + BlockDeviceWriter.dropCaches(device); + + verifyRead(TARGET_PATH, "1,100,128"); + } + + private void prepareTest(int fileSize) throws Exception { + DeviceTestRunOptions options = new DeviceTestRunOptions(TARGET_PACKAGE); + options.setTestClassName(TARGET_PACKAGE + ".Helper"); + options.setTestMethodName("prepareTest"); + options.addInstrumentationArg("basename", BASENAME); + options.addInstrumentationArg("fileSize", String.valueOf(fileSize)); + assertThat(runDeviceTests(options)).isTrue(); + } + + private void verifyRead(String path, String indicesCsv) throws Exception { + DeviceTestRunOptions options = new DeviceTestRunOptions(TARGET_PACKAGE); + options.setTestClassName(TARGET_PACKAGE + ".Helper"); + options.setTestMethodName("verifyFileRead"); + options.addInstrumentationArg("brokenBlockIndicesCsv", indicesCsv); + options.addInstrumentationArg("filePath", TARGET_PATH); + assertThat(runDeviceTests(options)).isTrue(); + } +} diff --git a/tests/ApkVerityTest/testdata/Android.bp b/tests/FsVerityTest/testdata/Android.bp index ccfc4c99a347..2d578d36423d 100644 --- a/tests/ApkVerityTest/testdata/Android.bp +++ b/tests/FsVerityTest/testdata/Android.bp @@ -37,51 +37,3 @@ filegroup { name: "ApkVerityTestCertDer", srcs: ["ApkVerityTestCert.der"], } - -filegroup { - name: "ApkVerityTestAppDm", - srcs: ["ApkVerityTestApp.dm"], -} - -filegroup { - name: "ApkVerityTestAppSplitDm", - srcs: ["ApkVerityTestAppSplit.dm"], -} - -genrule_defaults { - name: "apk_verity_sig_gen_default", - tools: ["fsverity"], - tool_files: [":ApkVerityTestKeyPem", ":ApkVerityTestCertPem"], - cmd: "$(location fsverity) sign $(in) $(out) " + - "--key=$(location :ApkVerityTestKeyPem) " + - "--cert=$(location :ApkVerityTestCertPem) " + - "> /dev/null", -} - -genrule { - name: "ApkVerityTestAppFsvSig", - defaults: ["apk_verity_sig_gen_default"], - srcs: [":ApkVerityTestApp"], - out: ["ApkVerityTestApp.apk.fsv_sig"], -} - -genrule { - name: "ApkVerityTestAppDmFsvSig", - defaults: ["apk_verity_sig_gen_default"], - srcs: [":ApkVerityTestAppDm"], - out: ["ApkVerityTestApp.dm.fsv_sig"], -} - -genrule { - name: "ApkVerityTestAppSplitFsvSig", - defaults: ["apk_verity_sig_gen_default"], - srcs: [":ApkVerityTestAppSplit"], - out: ["ApkVerityTestAppSplit.apk.fsv_sig"], -} - -genrule { - name: "ApkVerityTestAppSplitDmFsvSig", - defaults: ["apk_verity_sig_gen_default"], - srcs: [":ApkVerityTestAppSplitDm"], - out: ["ApkVerityTestAppSplit.dm.fsv_sig"], -} diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestCert.der b/tests/FsVerityTest/testdata/ApkVerityTestCert.der Binary files differindex fe9029b53aa1..fe9029b53aa1 100644 --- a/tests/ApkVerityTest/testdata/ApkVerityTestCert.der +++ b/tests/FsVerityTest/testdata/ApkVerityTestCert.der diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestCert.pem b/tests/FsVerityTest/testdata/ApkVerityTestCert.pem index 6c0b7b1f635a..6c0b7b1f635a 100644 --- a/tests/ApkVerityTest/testdata/ApkVerityTestCert.pem +++ b/tests/FsVerityTest/testdata/ApkVerityTestCert.pem diff --git a/tests/ApkVerityTest/testdata/ApkVerityTestKey.pem b/tests/FsVerityTest/testdata/ApkVerityTestKey.pem index f0746c162421..f0746c162421 100644 --- a/tests/ApkVerityTest/testdata/ApkVerityTestKey.pem +++ b/tests/FsVerityTest/testdata/ApkVerityTestKey.pem diff --git a/tests/HandwritingIme/src/com/google/android/test/handwritingime/HandwritingIme.java b/tests/HandwritingIme/src/com/google/android/test/handwritingime/HandwritingIme.java index 8380dcf4b4a4..d939d91cd527 100644 --- a/tests/HandwritingIme/src/com/google/android/test/handwritingime/HandwritingIme.java +++ b/tests/HandwritingIme/src/com/google/android/test/handwritingime/HandwritingIme.java @@ -20,6 +20,8 @@ import android.annotation.Nullable; import android.graphics.PointF; import android.graphics.RectF; import android.inputmethodservice.InputMethodService; +import android.os.CancellationSignal; +import android.os.Handler; import android.util.Log; import android.view.MotionEvent; import android.view.View; @@ -31,7 +33,9 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.HandwritingGesture; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InsertGesture; +import android.view.inputmethod.InsertModeGesture; import android.view.inputmethod.JoinOrSplitGesture; +import android.view.inputmethod.PreviewableHandwritingGesture; import android.view.inputmethod.RemoveSpaceGesture; import android.view.inputmethod.SelectGesture; import android.widget.AdapterView; @@ -47,11 +51,14 @@ import java.util.function.IntConsumer; public class HandwritingIme extends InputMethodService { private static final int OP_NONE = 0; + // ------- PreviewableHandwritingGesture BEGIN ----- private static final int OP_SELECT = 1; private static final int OP_DELETE = 2; + // ------- PreviewableHandwritingGesture END ----- private static final int OP_INSERT = 3; private static final int OP_REMOVE_SPACE = 4; private static final int OP_JOIN_OR_SPLIT = 5; + private static final int OP_INSERT_MODE = 6; private InkView mInk; @@ -70,6 +77,10 @@ public class HandwritingIme extends InputMethodService { private final IntConsumer mResultConsumer = value -> Log.d(TAG, "Gesture result: " + value); + private CancellationSignal mCancellationSignal = new CancellationSignal(); + private boolean mUsePreview; + private CheckBox mGesturePreviewCheckbox; + interface HandwritingFinisher { void finish(); } @@ -98,73 +109,107 @@ public class HandwritingIme extends InputMethodService { private void onStylusEvent(@Nullable MotionEvent event) { // TODO Hookup recognizer here + HandwritingGesture gesture; switch (event.getAction()) { - case MotionEvent.ACTION_UP: { - if (areRichGesturesEnabled()) { - HandwritingGesture gesture = null; - switch (mRichGestureMode) { - case OP_SELECT: - gesture = new SelectGesture.Builder() - .setGranularity(mRichGestureGranularity) - .setSelectionArea(getSanitizedRectF(mRichGestureStartPoint.x, - mRichGestureStartPoint.y, event.getX(), event.getY())) - .setFallbackText("fallback text") - .build(); - break; - case OP_DELETE: - gesture = new DeleteGesture.Builder() - .setGranularity(mRichGestureGranularity) - .setDeletionArea(getSanitizedRectF(mRichGestureStartPoint.x, - mRichGestureStartPoint.y, event.getX(), event.getY())) - .setFallbackText("fallback text") - .build(); - break; - case OP_INSERT: - gesture = new InsertGesture.Builder() - .setInsertionPoint(new PointF( - mRichGestureStartPoint.x, mRichGestureStartPoint.y)) - .setTextToInsert(" ") - .setFallbackText("fallback text") - .build(); - break; - case OP_REMOVE_SPACE: - gesture = new RemoveSpaceGesture.Builder() - .setPoints( - new PointF(mRichGestureStartPoint.x, - mRichGestureStartPoint.y), - new PointF(event.getX(), event.getY())) - .setFallbackText("fallback text") - .build(); - break; - case OP_JOIN_OR_SPLIT: - gesture = new JoinOrSplitGesture.Builder() - .setJoinOrSplitPoint(new PointF( - mRichGestureStartPoint.x, mRichGestureStartPoint.y)) - .setFallbackText("fallback text") - .build(); - break; + case MotionEvent.ACTION_MOVE: + if (mUsePreview && areRichGesturesEnabled()) { + gesture = computeGesture(event, true /* isPreview */); + if (gesture == null) { + Log.e(TAG, "Preview not supported for gesture: " + mRichGestureMode); + return; } + performGesture(gesture, true /* isPreview */); + } + break; + case MotionEvent.ACTION_UP: + if (areRichGesturesEnabled()) { + gesture = computeGesture(event, false /* isPreview */); if (gesture == null) { // This shouldn't happen Log.e(TAG, "Unrecognized gesture mode: " + mRichGestureMode); return; } - performGesture(gesture); + performGesture(gesture, false /* isPreview */); } else { // insert random ASCII char sendKeyChar((char) (56 + new Random().nextInt(66))); } return; - } case MotionEvent.ACTION_DOWN: { if (areRichGesturesEnabled()) { mRichGestureStartPoint = new PointF(event.getX(), event.getY()); } - return; } } } + private HandwritingGesture computeGesture(MotionEvent event, boolean isPreview) { + HandwritingGesture gesture = null; + switch (mRichGestureMode) { + case OP_SELECT: + gesture = new SelectGesture.Builder() + .setGranularity(mRichGestureGranularity) + .setSelectionArea(getSanitizedRectF(mRichGestureStartPoint.x, + mRichGestureStartPoint.y, event.getX(), event.getY())) + .setFallbackText("fallback text") + .build(); + break; + case OP_DELETE: + gesture = new DeleteGesture.Builder() + .setGranularity(mRichGestureGranularity) + .setDeletionArea(getSanitizedRectF(mRichGestureStartPoint.x, + mRichGestureStartPoint.y, event.getX(), event.getY())) + .setFallbackText("fallback text") + .build(); + break; + case OP_INSERT: + gesture = new InsertGesture.Builder() + .setInsertionPoint(new PointF( + mRichGestureStartPoint.x, mRichGestureStartPoint.y)) + .setTextToInsert(" ") + .setFallbackText("fallback text") + .build(); + break; + case OP_REMOVE_SPACE: + if (isPreview) { + break; + } + gesture = new RemoveSpaceGesture.Builder() + .setPoints( + new PointF(mRichGestureStartPoint.x, + mRichGestureStartPoint.y), + new PointF(event.getX(), event.getY())) + .setFallbackText("fallback text") + .build(); + break; + case OP_JOIN_OR_SPLIT: + if (isPreview) { + break; + } + gesture = new JoinOrSplitGesture.Builder() + .setJoinOrSplitPoint(new PointF( + mRichGestureStartPoint.x, mRichGestureStartPoint.y)) + .setFallbackText("fallback text") + .build(); + break; + case OP_INSERT_MODE: + if (isPreview) { + break; + } + mCancellationSignal = new CancellationSignal(); + InsertModeGesture img = new InsertModeGesture.Builder() + .setInsertionPoint(new PointF( + mRichGestureStartPoint.x, mRichGestureStartPoint.y)) + .setFallbackText("fallback text") + .setCancellationSignal(mCancellationSignal) + .build(); + gesture = img; + new Handler().postDelayed(() -> img.getCancellationSignal().cancel(), 5000); + break; + } + return gesture; + } + /** * sanitize values to support rectangles in all cases. */ @@ -193,10 +238,14 @@ public class HandwritingIme extends InputMethodService { return rectF; } - private void performGesture(HandwritingGesture gesture) { + private void performGesture(HandwritingGesture gesture, boolean isPreview) { InputConnection ic = getCurrentInputConnection(); if (getCurrentInputStarted() && ic != null) { - ic.performHandwritingGesture(gesture, Runnable::run, mResultConsumer); + if (isPreview) { + ic.previewHandwritingGesture((PreviewableHandwritingGesture) gesture, null); + } else { + ic.performHandwritingGesture(gesture, Runnable::run, mResultConsumer); + } } else { // This shouldn't happen Log.e(TAG, "No active InputConnection"); @@ -216,12 +265,21 @@ public class HandwritingIme extends InputMethodService { layout.addView(getRichGestureActionsSpinner()); layout.addView(getRichGestureGranularitySpinner()); layout.addView(getBoundsInfoCheckBoxes()); + layout.addView(getPreviewCheckBox()); layout.setBackgroundColor(getColor(R.color.holo_green_light)); view.addView(layout); return view; } + private View getPreviewCheckBox() { + mGesturePreviewCheckbox = new CheckBox(this); + mGesturePreviewCheckbox.setText("Use Gesture Previews (for Previewable Gestures)"); + mGesturePreviewCheckbox.setOnCheckedChangeListener( + (buttonView, isChecked) -> mUsePreview = isChecked); + return mGesturePreviewCheckbox; + } + private View getRichGestureActionsSpinner() { if (mRichGestureModeSpinner != null) { return mRichGestureModeSpinner; @@ -236,6 +294,7 @@ public class HandwritingIme extends InputMethodService { "Rich gesture INSERT", "Rich gesture REMOVE SPACE", "Rich gesture JOIN OR SPLIT", + "Rich gesture INSERT MODE", }; ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, items); @@ -245,8 +304,13 @@ public class HandwritingIme extends InputMethodService { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { mRichGestureMode = position; - mRichGestureGranularitySpinner.setEnabled( - mRichGestureMode == OP_SELECT || mRichGestureMode == OP_DELETE); + boolean supportsGranularityAndPreview = + mRichGestureMode == OP_SELECT || mRichGestureMode == OP_DELETE; + mRichGestureGranularitySpinner.setEnabled(supportsGranularityAndPreview); + mGesturePreviewCheckbox.setEnabled(supportsGranularityAndPreview); + if (!supportsGranularityAndPreview) { + mUsePreview = false; + } Log.d(TAG, "Setting RichGesture Mode " + mRichGestureMode); } diff --git a/tests/HwAccelerationTest/AndroidManifest.xml b/tests/HwAccelerationTest/AndroidManifest.xml index 80c7a21dc11b..db3a992b9c7b 100644 --- a/tests/HwAccelerationTest/AndroidManifest.xml +++ b/tests/HwAccelerationTest/AndroidManifest.xml @@ -789,6 +789,15 @@ </intent-filter> </activity> + <activity android:name="BackdropBlurActivity" + android:label="RenderEffect/BackdropBlur" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="com.android.test.hwui.TEST"/> + </intent-filter> + </activity> + <activity android:name="BlurActivity" android:label="RenderEffect/Blur" android:exported="true"> diff --git a/tests/HwAccelerationTest/res/drawable/robot_repeated.xml b/tests/HwAccelerationTest/res/drawable/robot_repeated.xml new file mode 100644 index 000000000000..bbb15b71c3a5 --- /dev/null +++ b/tests/HwAccelerationTest/res/drawable/robot_repeated.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<bitmap xmlns:android="http://schemas.android.com/apk/res/android" + android:src="@drawable/robot" + android:tileMode="repeat" android:gravity="fill" />
\ No newline at end of file diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/BackdropBlurActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/BackdropBlurActivity.java new file mode 100644 index 000000000000..8086b29df7cd --- /dev/null +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/BackdropBlurActivity.java @@ -0,0 +1,119 @@ +/* + * 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.test.hwui; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Outline; +import android.graphics.RenderEffect; +import android.graphics.Shader; +import android.os.Bundle; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ScrollView; + +@SuppressWarnings({"UnusedDeclaration"}) +public class BackdropBlurActivity extends Activity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final ScrollView scrollView = new ScrollView(this); + final FrameLayout innerFrame = new FrameLayout(this); + final View backgroundView = new View(this); + backgroundView.setBackgroundResource(R.drawable.robot_repeated); + innerFrame.addView(backgroundView, ViewGroup.LayoutParams.MATCH_PARENT, 10000); + scrollView.addView(innerFrame, + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + final FrameLayout contentView = new FrameLayout(this); + contentView.addView(scrollView, + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + contentView.addView(new BackdropBlurView(this), 300, 300); + setContentView(contentView); + } + + private static class BackdropBlurView extends View { + private final float mBlurRadius = 60f; + private final float mSaturation = 1.8f; + + private float mDownOffsetX; + private float mDownOffsetY; + + BackdropBlurView(Context c) { + super(c); + + // init RenderEffect. + final RenderEffect blurEffect = RenderEffect.createBlurEffect( + mBlurRadius, mBlurRadius, + null, Shader.TileMode.MIRROR // TileMode.MIRROR is better for blur. + ); + + final ColorMatrix colorMatrix = new ColorMatrix(); + colorMatrix.setSaturation(mSaturation); + final RenderEffect effect = RenderEffect.createColorFilterEffect( + new ColorMatrixColorFilter(colorMatrix), blurEffect + ); + setBackdropRenderEffect(effect); + + // clip to a round outline. + setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View v, Outline outline) { + outline.setOval(0, 0, v.getWidth(), v.getHeight()); + } + }); + setClipToOutline(true); + + animate().setInterpolator(new DecelerateInterpolator(2.0f)); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + canvas.drawColor(0x99F0F0F0); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mDownOffsetX = event.getRawX() - getTranslationX(); + mDownOffsetY = event.getRawY() - getTranslationY(); + animate().scaleX(1.5f).scaleY(1.5f).start(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + animate().scaleX(1f).scaleY(1f).start(); + break; + case MotionEvent.ACTION_MOVE: + setTranslationX(event.getRawX() - mDownOffsetX); + setTranslationY(event.getRawY() - mDownOffsetY); + break; + } + return true; + } + } +} diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/BitmapsAlphaActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/BitmapsAlphaActivity.java index ef49c7fd00a9..cb16191423ce 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/BitmapsAlphaActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/BitmapsAlphaActivity.java @@ -48,15 +48,15 @@ public class BitmapsAlphaActivity extends Activity { BitmapsView(Context c) { super(c); - Log.d("OpenGLRenderer", "Loading sunset1, default options"); + Log.d("HWUI", "Loading sunset1, default options"); mBitmap1 = BitmapFactory.decodeResource(c.getResources(), R.drawable.sunset1); - Log.d("OpenGLRenderer", "Loading sunset2, default options"); + Log.d("HWUI", "Loading sunset2, default options"); mBitmap2 = BitmapFactory.decodeResource(c.getResources(), R.drawable.sunset2); - Log.d("OpenGLRenderer", "Loading sunset3, forcing ARGB-8888"); + Log.d("HWUI", "Loading sunset3, forcing ARGB-8888"); BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inPreferredConfig = Bitmap.Config.ARGB_8888; mBitmap3 = BitmapFactory.decodeResource(c.getResources(), R.drawable.sunset3, opts); - Log.d("OpenGLRenderer", " has bitmap alpha? " + mBitmap3.hasAlpha()); + Log.d("HWUI", " has bitmap alpha? " + mBitmap3.hasAlpha()); mBitmapPaint = new Paint(); } @@ -65,7 +65,7 @@ public class BitmapsAlphaActivity extends Activity { protected void onDraw(Canvas canvas) { super.onDraw(canvas); - Log.d("OpenGLRenderer", "================= Draw"); + Log.d("HWUI", "================= Draw"); canvas.translate(120.0f, 50.0f); canvas.drawBitmap(mBitmap1, 0.0f, 0.0f, mBitmapPaint); diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/ClearActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/ClearActivity.java index 1c82e9bbdf9b..dbfb4ca7c8fe 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/ClearActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/ClearActivity.java @@ -77,7 +77,7 @@ public class ClearActivity extends Activity { canvas.drawPath(mPath, mClearPaint); } canvas.restore(); - canvas.drawText("OpenGLRenderer", 50.0f, 50.0f, mClearPaint); + canvas.drawText("HWUI", 50.0f, 50.0f, mClearPaint); mClearPaint.setColor(0xff000000); canvas.drawRect(800.0f, 100.0f, 900.0f, 200.0f, mClearPaint); mClearPaint.setColor(0x0000ff00); diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/ColorBitmapActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/ColorBitmapActivity.java index e2d17cdbe9e6..1f4c6c53b260 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/ColorBitmapActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/ColorBitmapActivity.java @@ -17,6 +17,7 @@ package com.android.test.hwui; import android.app.Activity; +import android.content.pm.ActivityInfo; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorSpace; @@ -72,6 +73,10 @@ public class ColorBitmapActivity extends Activity implements SurfaceHolder.Callb private int[] mGradientEndColors = {0xFFFFFFFF, 0xFFFF0000, 0xFF00FF00, 0xFF0000FF}; private String[] mGradientColorNames = {"Grayscale", "Red", "Green", "Blue"}; + private int mColorMode = ActivityInfo.COLOR_MODE_DEFAULT; + private int[] mColorModes = {ActivityInfo.COLOR_MODE_DEFAULT, ActivityInfo.COLOR_MODE_HDR}; + private String[] mColorModeNames = {"DEFAULT", "HDR"}; + private final ExecutorService mBufferFenceExecutor = Executors.newFixedThreadPool(1); private final ExecutorService mBufferExecutor = Executors.newFixedThreadPool(1); @@ -139,6 +144,15 @@ public class ColorBitmapActivity extends Activity implements SurfaceHolder.Callb gradientColorSpinner .setOnItemSelectedListener(new GradientColorOnItemSelectedListener()); + ArrayAdapter<String> colorModeAdapter = new ArrayAdapter<>( + this, android.R.layout.simple_spinner_item, mColorModeNames); + + colorModeAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + Spinner colorModeSpinner = new Spinner(this); + colorModeSpinner.setAdapter(colorModeAdapter); + colorModeSpinner.setOnItemSelectedListener(new ColorModeOnItemSelectedListener()); + mGradientBuffer = getGradientBuffer().get(); LinearLayout linearLayout = new LinearLayout(this); @@ -169,6 +183,10 @@ public class ColorBitmapActivity extends Activity implements SurfaceHolder.Callb LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + spinnerLayout.addView(colorModeSpinner, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + linearLayout.addView(spinnerLayout, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); @@ -187,6 +205,8 @@ public class ColorBitmapActivity extends Activity implements SurfaceHolder.Callb linearLayout.addView(mSurfaceView, new LinearLayout.LayoutParams(WIDTH, HEIGHT)); setContentView(linearLayout); + + getWindow().setColorMode(mColorMode); } catch (Exception e) { throw new RuntimeException(e); } @@ -312,4 +332,22 @@ public class ColorBitmapActivity extends Activity implements SurfaceHolder.Callb } } + + private final class ColorModeOnItemSelectedListener + implements AdapterView.OnItemSelectedListener { + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + ColorBitmapActivity.this.mColorMode = ColorBitmapActivity.this.mColorModes[position]; + ColorBitmapActivity.this.getMainExecutor() + .execute(() -> { + ColorBitmapActivity.this.getWindow().setColorMode(mColorMode); + }); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + + } + } } diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/QuickRejectActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/QuickRejectActivity.java index 5192bfe84fef..11a2a4161a8b 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/QuickRejectActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/QuickRejectActivity.java @@ -53,30 +53,30 @@ public class QuickRejectActivity extends Activity { super.onDraw(canvas); int count = canvas.getSaveCount(); - Log.d("OpenGLRenderer", "count=" + count); + Log.d("HWUI", "count=" + count); count = canvas.save(); - Log.d("OpenGLRenderer", "count after save=" + count); + Log.d("HWUI", "count after save=" + count); count = canvas.getSaveCount(); - Log.d("OpenGLRenderer", "getSaveCount after save=" + count); + Log.d("HWUI", "getSaveCount after save=" + count); canvas.restore(); count = canvas.getSaveCount(); - Log.d("OpenGLRenderer", "count after restore=" + count); + Log.d("HWUI", "count after restore=" + count); canvas.save(); - Log.d("OpenGLRenderer", "count after save=" + canvas.getSaveCount()); + Log.d("HWUI", "count after save=" + canvas.getSaveCount()); canvas.save(); - Log.d("OpenGLRenderer", "count after save=" + canvas.getSaveCount()); + Log.d("HWUI", "count after save=" + canvas.getSaveCount()); canvas.save(); - Log.d("OpenGLRenderer", "count after save=" + canvas.getSaveCount()); + Log.d("HWUI", "count after save=" + canvas.getSaveCount()); canvas.restoreToCount(count); count = canvas.getSaveCount(); - Log.d("OpenGLRenderer", "count after restoreToCount=" + count); + Log.d("HWUI", "count after restoreToCount=" + count); count = canvas.saveLayer(0, 0, 10, 10, mBitmapPaint, Canvas.ALL_SAVE_FLAG); - Log.d("OpenGLRenderer", "count after saveLayer=" + count); + Log.d("HWUI", "count after saveLayer=" + count); count = canvas.getSaveCount(); - Log.d("OpenGLRenderer", "getSaveCount after saveLayer=" + count); + Log.d("HWUI", "getSaveCount after saveLayer=" + count); canvas.restore(); count = canvas.getSaveCount(); - Log.d("OpenGLRenderer", "count after restore=" + count); + Log.d("HWUI", "count after restore=" + count); canvas.save(); canvas.clipRect(0.0f, 0.0f, 40.0f, 40.0f); diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp index 4fa6fbe1d4e1..96b685dc356a 100644 --- a/tests/Input/Android.bp +++ b/tests/Input/Android.bp @@ -19,13 +19,26 @@ android_test { platform_apis: true, certificate: "platform", static_libs: [ + "androidx.test.core", "androidx.test.ext.junit", + "androidx.test.ext.truth", "androidx.test.rules", + "androidx.test.runner", + "androidx.test.uiautomator_uiautomator", + "servicestests-utils", + "frameworks-base-testutils", + "hamcrest-library", + "kotlin-test", "mockito-target-minus-junit4", + "platform-test-annotations", "services.core.unboosted", "testables", + "testng", "truth-prebuilt", - "androidx.test.uiautomator_uiautomator", + ], + libs: [ + "android.test.mock", + "android.test.base", ], test_suites: ["device-tests"], } diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml index 20f564e80b3d..3b723ddf811f 100644 --- a/tests/Input/AndroidManifest.xml +++ b/tests/Input/AndroidManifest.xml @@ -16,11 +16,14 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.test.input"> + + <uses-permission android:name="android.permission.INJECT_EVENTS"/> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/> <uses-permission android:name="android.permission.MONITOR_INPUT"/> + <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/> - <uses-permission android:name="android.permission.INJECT_EVENTS"/> - <application android:label="InputTest"> + <application android:label="InputTest" android:debuggable="true"> <activity android:name=".UnresponsiveGestureMonitorActivity" android:label="Unresponsive gesture monitor" diff --git a/tests/Input/res/raw/dummy_keyboard_layout.kcm b/tests/Input/res/raw/dummy_keyboard_layout.kcm new file mode 100644 index 000000000000..ea6bc980b7b6 --- /dev/null +++ b/tests/Input/res/raw/dummy_keyboard_layout.kcm @@ -0,0 +1,311 @@ +# 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. + +# +# English (US) keyboard layout. +# Unlike the default (generic) keyboard layout, English (US) does not contain any +# special ALT characters. +# + +type OVERLAY + +### ROW 1 + +key GRAVE { + label: '`' + base: '`' + shift: '~' +} + +key 1 { + label: '1' + base: '1' + shift: '!' +} + +key 2 { + label: '2' + base: '2' + shift: '@' +} + +key 3 { + label: '3' + base: '3' + shift: '#' +} + +key 4 { + label: '4' + base: '4' + shift: '$' +} + +key 5 { + label: '5' + base: '5' + shift: '%' +} + +key 6 { + label: '6' + base: '6' + shift: '^' +} + +key 7 { + label: '7' + base: '7' + shift: '&' +} + +key 8 { + label: '8' + base: '8' + shift: '*' +} + +key 9 { + label: '9' + base: '9' + shift: '(' +} + +key 0 { + label: '0' + base: '0' + shift: ')' +} + +key MINUS { + label: '-' + base: '-' + shift: '_' +} + +key EQUALS { + label: '=' + base: '=' + shift: '+' +} + +### ROW 2 + +key Q { + label: 'Q' + base: 'q' + shift, capslock: 'Q' +} + +key W { + label: 'W' + base: 'w' + shift, capslock: 'W' +} + +key E { + label: 'E' + base: 'e' + shift, capslock: 'E' +} + +key R { + label: 'R' + base: 'r' + shift, capslock: 'R' +} + +key T { + label: 'T' + base: 't' + shift, capslock: 'T' +} + +key Y { + label: 'Y' + base: 'y' + shift, capslock: 'Y' +} + +key U { + label: 'U' + base: 'u' + shift, capslock: 'U' +} + +key I { + label: 'I' + base: 'i' + shift, capslock: 'I' +} + +key O { + label: 'O' + base: 'o' + shift, capslock: 'O' +} + +key P { + label: 'P' + base: 'p' + shift, capslock: 'P' +} + +key LEFT_BRACKET { + label: '[' + base: '[' + shift: '{' +} + +key RIGHT_BRACKET { + label: ']' + base: ']' + shift: '}' +} + +key BACKSLASH { + label: '\\' + base: '\\' + shift: '|' +} + +### ROW 3 + +key A { + label: 'A' + base: 'a' + shift, capslock: 'A' +} + +key S { + label: 'S' + base: 's' + shift, capslock: 'S' +} + +key D { + label: 'D' + base: 'd' + shift, capslock: 'D' +} + +key F { + label: 'F' + base: 'f' + shift, capslock: 'F' +} + +key G { + label: 'G' + base: 'g' + shift, capslock: 'G' +} + +key H { + label: 'H' + base: 'h' + shift, capslock: 'H' +} + +key J { + label: 'J' + base: 'j' + shift, capslock: 'J' +} + +key K { + label: 'K' + base: 'k' + shift, capslock: 'K' +} + +key L { + label: 'L' + base: 'l' + shift, capslock: 'L' +} + +key SEMICOLON { + label: ';' + base: ';' + shift: ':' +} + +key APOSTROPHE { + label: '\'' + base: '\'' + shift: '"' +} + +### ROW 4 + +key Z { + label: 'Z' + base: 'z' + shift, capslock: 'Z' +} + +key X { + label: 'X' + base: 'x' + shift, capslock: 'X' +} + +key C { + label: 'C' + base: 'c' + shift, capslock: 'C' +} + +key V { + label: 'V' + base: 'v' + shift, capslock: 'V' +} + +key B { + label: 'B' + base: 'b' + shift, capslock: 'B' +} + +key N { + label: 'N' + base: 'n' + shift, capslock: 'N' +} + +key M { + label: 'M' + base: 'm' + shift, capslock: 'M' +} + +key COMMA { + label: ',' + base: ',' + shift: '<' +} + +key PERIOD { + label: '.' + base: '.' + shift: '>' +} + +key SLASH { + label: '/' + base: '/' + shift: '?' +} diff --git a/tests/Input/res/raw/input_port_associations.xml b/tests/Input/res/raw/input_port_associations.xml new file mode 100644 index 000000000000..b10d541f942c --- /dev/null +++ b/tests/Input/res/raw/input_port_associations.xml @@ -0,0 +1,4 @@ +<ports> + <port display="0" input="USB1" /> + <port display="1" input="USB2" /> +</ports>
\ No newline at end of file diff --git a/tests/Input/res/raw/input_port_associations_bad_displayport.xml b/tests/Input/res/raw/input_port_associations_bad_displayport.xml new file mode 100644 index 000000000000..8eeb1f58ef9e --- /dev/null +++ b/tests/Input/res/raw/input_port_associations_bad_displayport.xml @@ -0,0 +1,3 @@ +<ports> + <port display="a" input="USB1" /> +</ports>
\ No newline at end of file diff --git a/tests/Input/res/raw/input_port_associations_bad_xml.xml b/tests/Input/res/raw/input_port_associations_bad_xml.xml new file mode 100644 index 000000000000..cf6e12486239 --- /dev/null +++ b/tests/Input/res/raw/input_port_associations_bad_xml.xml @@ -0,0 +1,3 @@ +<ports> + <port Garbage data inside xml> +</ports>
\ No newline at end of file diff --git a/tests/Input/res/xml/keyboard_layouts.xml b/tests/Input/res/xml/keyboard_layouts.xml new file mode 100644 index 000000000000..5f3fcd6eaed0 --- /dev/null +++ b/tests/Input/res/xml/keyboard_layouts.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<keyboard-layouts xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <keyboard-layout + android:name="keyboard_layout_english_uk" + android:label="English (UK)" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="en-Latn-GB" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_english_us" + android:label="English (US)" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="en-Latn,en-Latn-US" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_english_us_intl" + android:label="English (International)" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="en-Latn-US" + android:keyboardLayoutType="extended" /> + + <keyboard-layout + android:name="keyboard_layout_english_us_dvorak" + android:label="English (Dvorak)" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="en-Latn-US" + android:keyboardLayoutType="dvorak" /> + + <keyboard-layout + android:name="keyboard_layout_german" + android:label="German" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="de-Latn" + android:keyboardLayoutType="qwertz" /> + + <keyboard-layout + android:name="keyboard_layout_french" + android:label="French" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="fr-Latn-FR" + android:keyboardLayoutType="azerty" /> + + <keyboard-layout + android:name="keyboard_layout_russian_qwerty" + android:label="Russian" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="ru-Cyrl" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_russian" + android:label="Russian" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="ru-Cyrl" /> + + <keyboard-layout + android:name="keyboard_layout_english_without_script_code" + android:label="English(No script code)" + android:keyboardLayout="@raw/dummy_keyboard_layout" + android:keyboardLocale="en" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_vendorId:1,productId:1" + android:label="vendorId:1,productId:1" + android:keyboardLayout="@raw/dummy_keyboard_layout" + androidprv:vendorId="1" + androidprv:productId="1" /> + +</keyboard-layouts> diff --git a/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt b/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt new file mode 100644 index 000000000000..90dff47ab706 --- /dev/null +++ b/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt @@ -0,0 +1,246 @@ +/* + * 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 android.hardware.input + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.BatteryState +import android.os.Handler +import android.os.HandlerExecutor +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import androidx.test.core.app.ApplicationProvider +import com.android.server.testutils.any +import java.util.concurrent.Executor +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoJUnitRunner + +/** + * Tests for [InputManager.InputDeviceBatteryListener]. + * + * Build/Install/Run: + * atest InputTests:InputDeviceBatteryListenerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class InputDeviceBatteryListenerTest { + @get:Rule + val rule = MockitoJUnit.rule()!! + + private lateinit var testLooper: TestLooper + private var registeredListener: IInputDeviceBatteryListener? = null + private val monitoredDevices = mutableListOf<Int>() + private lateinit var executor: Executor + private lateinit var context: Context + private lateinit var inputManager: InputManager + + @Mock + private lateinit var iInputManagerMock: IInputManager + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + + @Before + fun setUp() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + testLooper = TestLooper() + executor = HandlerExecutor(Handler(testLooper.looper)) + registeredListener = null + monitoredDevices.clear() + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManagerMock) + inputManager = InputManager(context) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + // Handle battery listener registration. + doAnswer { + val deviceId = it.getArgument(0) as Int + val listener = it.getArgument(1) as IInputDeviceBatteryListener + if (registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder()) { + // There can only be one registered battery listener per process. + fail("Trying to register a new listener when one already exists") + } + if (monitoredDevices.contains(deviceId)) { + fail("Trying to start monitoring a device that was already being monitored") + } + monitoredDevices.add(deviceId) + registeredListener = listener + null + }.`when`(iInputManagerMock).registerBatteryListener(anyInt(), any()) + + // Handle battery listener being unregistered. + doAnswer { + val deviceId = it.getArgument(0) as Int + val listener = it.getArgument(1) as IInputDeviceBatteryListener + if (registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder()) { + fail("Trying to unregister a listener that is not registered") + } + if (!monitoredDevices.remove(deviceId)) { + fail("Trying to stop monitoring a device that is not being monitored") + } + if (monitoredDevices.isEmpty()) { + registeredListener = null + } + }.`when`(iInputManagerMock).unregisterBatteryListener(anyInt(), any()) + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + private fun notifyBatteryStateChanged( + deviceId: Int, + isPresent: Boolean = true, + status: Int = BatteryState.STATUS_FULL, + capacity: Float = 1.0f, + eventTime: Long = 12345L + ) { + registeredListener!!.onBatteryStateChanged(IInputDeviceBatteryState().apply { + this.deviceId = deviceId + this.updateTime = eventTime + this.isPresent = isPresent + this.status = status + this.capacity = capacity + }) + } + + @Test + fun testListenerIsNotifiedCorrectly() { + var callbackCount = 0 + + // Add a battery listener to monitor battery changes. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor) { + deviceId: Int, eventTime: Long, batteryState: BatteryState -> + callbackCount++ + assertEquals(1, deviceId) + assertEquals(true, batteryState.isPresent) + assertEquals(BatteryState.STATUS_DISCHARGING, batteryState.status) + assertEquals(0.5f, batteryState.capacity) + assertEquals(8675309L, eventTime) + } + + // Adding the listener should register the callback with InputManagerService. + assertNotNull(registeredListener) + assertTrue(monitoredDevices.contains(1)) + + // Notifying battery change for a different device should not trigger the listener. + notifyBatteryStateChanged(deviceId = 2) + testLooper.dispatchAll() + assertEquals(0, callbackCount) + + // Notifying battery change for the registered device will notify the listener. + notifyBatteryStateChanged(1 /*deviceId*/, true /*isPresent*/, + BatteryState.STATUS_DISCHARGING, 0.5f /*capacity*/, 8675309L /*eventTime*/) + testLooper.dispatchNext() + assertEquals(1, callbackCount) + } + + @Test + fun testMultipleListeners() { + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ } + val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ } + + // Monitor battery changes for three devices. The first callback monitors devices 1 and 3, + // while the second callback monitors devices 2 and 3. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1) + assertEquals(1, monitoredDevices.size) + inputManager.addInputDeviceBatteryListener(2 /*deviceId*/, executor, callback2) + assertEquals(2, monitoredDevices.size) + inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback1) + assertEquals(3, monitoredDevices.size) + inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback2) + assertEquals(3, monitoredDevices.size) + + // Notifying battery change for each of the devices should trigger the registered callbacks. + notifyBatteryStateChanged(deviceId = 1) + testLooper.dispatchNext() + assertEquals(1, callbackCount1) + assertEquals(0, callbackCount2) + + notifyBatteryStateChanged(deviceId = 2) + testLooper.dispatchNext() + assertEquals(1, callbackCount1) + assertEquals(1, callbackCount2) + + notifyBatteryStateChanged(deviceId = 3) + testLooper.dispatchNext() + testLooper.dispatchNext() + assertEquals(2, callbackCount1) + assertEquals(2, callbackCount2) + + // Stop monitoring devices 1 and 2. + inputManager.removeInputDeviceBatteryListener(1 /*deviceId*/, callback1) + assertEquals(2, monitoredDevices.size) + inputManager.removeInputDeviceBatteryListener(2 /*deviceId*/, callback2) + assertEquals(1, monitoredDevices.size) + + // Ensure device 3 continues to be monitored. + notifyBatteryStateChanged(deviceId = 3) + testLooper.dispatchNext() + testLooper.dispatchNext() + assertEquals(3, callbackCount1) + assertEquals(3, callbackCount2) + + // Stop monitoring all devices. + inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback1) + assertEquals(1, monitoredDevices.size) + inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback2) + assertEquals(0, monitoredDevices.size) + } + + @Test + fun testAdditionalListenersNotifiedImmediately() { + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ } + val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ } + + // Add a battery listener and send the latest battery state. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1) + assertEquals(1, monitoredDevices.size) + notifyBatteryStateChanged(deviceId = 1) + testLooper.dispatchNext() + assertEquals(1, callbackCount1) + + // Add a second listener for the same device that already has the latest battery state. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback2) + assertEquals(1, monitoredDevices.size) + + // Ensure that this listener is notified immediately. + testLooper.dispatchNext() + assertEquals(1, callbackCount2) + } +} diff --git a/tests/Input/src/android/hardware/input/InputDeviceLightsManagerTest.java b/tests/Input/src/android/hardware/input/InputDeviceLightsManagerTest.java new file mode 100644 index 000000000000..080186e4a2c1 --- /dev/null +++ b/tests/Input/src/android/hardware/input/InputDeviceLightsManagerTest.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static android.hardware.lights.LightsRequest.Builder; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.hardware.lights.Light; +import android.hardware.lights.LightState; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightsRequest; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.util.ArrayMap; +import android.view.InputDevice; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Tests for {@link InputDeviceLightsManager}. + * + * Build/Install/Run: + * atest InputTests:InputDeviceLightsManagerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class InputDeviceLightsManagerTest { + private static final String TAG = "InputDeviceLightsManagerTest"; + + private static final int DEVICE_ID = 1000; + private static final int PLAYER_ID = 3; + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private InputManager mInputManager; + + @Mock private IInputManager mIInputManagerMock; + private InputManagerGlobal.TestSession mInputManagerGlobalSession; + + @Before + public void setUp() throws Exception { + final Context context = spy( + new ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext())); + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{DEVICE_ID}); + + when(mIInputManagerMock.getInputDevice(eq(DEVICE_ID))).thenReturn( + createInputDevice(DEVICE_ID)); + + mInputManagerGlobalSession = InputManagerGlobal.createTestSession(mIInputManagerMock); + mInputManager = new InputManager(context); + when(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(mInputManager); + + ArrayMap<Integer, LightState> lightStatesById = new ArrayMap<>(); + doAnswer(invocation -> { + final int[] lightIds = (int[]) invocation.getArguments()[1]; + final LightState[] lightStates = + (LightState[]) invocation.getArguments()[2]; + for (int i = 0; i < lightIds.length; i++) { + lightStatesById.put(lightIds[i], lightStates[i]); + } + return null; + }).when(mIInputManagerMock).setLightStates(eq(DEVICE_ID), + any(int[].class), any(LightState[].class), any(IBinder.class)); + + doAnswer(invocation -> { + int lightId = (int) invocation.getArguments()[1]; + if (lightStatesById.containsKey(lightId)) { + return lightStatesById.get(lightId); + } + return new LightState(0); + }).when(mIInputManagerMock).getLightState(eq(DEVICE_ID), anyInt()); + } + + @After + public void tearDown() { + if (mInputManagerGlobalSession != null) { + mInputManagerGlobalSession.close(); + } + } + + private InputDevice createInputDevice(int id) { + return new InputDevice.Builder() + .setId(id) + .setName("Test Device " + id) + .build(); + } + + private void mockLights(Light[] lights) throws Exception { + // Mock the Lights returned form InputManagerService + when(mIInputManagerMock.getLights(eq(DEVICE_ID))).thenReturn( + new ArrayList(Arrays.asList(lights))); + } + + @Test + public void testGetInputDeviceLights() throws Exception { + InputDevice device = mInputManager.getInputDevice(DEVICE_ID); + assertNotNull(device); + + Light[] mockedLights = { + new Light(1 /* id */, "Light1", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + Light.LIGHT_CAPABILITY_BRIGHTNESS), + new Light(2 /* id */, "Light2", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + Light.LIGHT_CAPABILITY_COLOR_RGB), + new Light(3 /* id */, "Light3", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + 0 /* capabilities */) + }; + mockLights(mockedLights); + + LightsManager lightsManager = device.getLightsManager(); + List<Light> lights = lightsManager.getLights(); + verify(mIInputManagerMock).getLights(eq(DEVICE_ID)); + assertEquals(lights, Arrays.asList(mockedLights)); + } + + @Test + public void testControlMultipleLights() throws Exception { + InputDevice device = mInputManager.getInputDevice(DEVICE_ID); + assertNotNull(device); + + Light[] mockedLights = { + new Light(1 /* id */, "Light1", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + Light.LIGHT_CAPABILITY_COLOR_RGB), + new Light(2 /* id */, "Light2", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + Light.LIGHT_CAPABILITY_COLOR_RGB), + new Light(3 /* id */, "Light3", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + Light.LIGHT_CAPABILITY_COLOR_RGB), + new Light(4 /* id */, "Light4", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + Light.LIGHT_CAPABILITY_COLOR_RGB) + }; + mockLights(mockedLights); + + LightsManager lightsManager = device.getLightsManager(); + List<Light> lightList = lightsManager.getLights(); + LightState[] states = new LightState[]{new LightState(0xf1), new LightState(0xf2), + new LightState(0xf3)}; + // Open a session to request turn 3/4 lights on: + LightsManager.LightsSession session = lightsManager.openSession(); + session.requestLights(new Builder() + .addLight(lightsManager.getLights().get(0), states[0]) + .addLight(lightsManager.getLights().get(1), states[1]) + .addLight(lightsManager.getLights().get(2), states[2]) + .build()); + IBinder token = session.getToken(); + + verify(mIInputManagerMock).openLightSession(eq(DEVICE_ID), + any(String.class), eq(token)); + verify(mIInputManagerMock).setLightStates(eq(DEVICE_ID), eq(new int[]{1, 2, 3}), + eq(states), eq(token)); + + // Then all 3 should turn on. + assertThat(lightsManager.getLightState(lightsManager.getLights().get(0)).getColor()) + .isEqualTo(0xf1); + assertThat(lightsManager.getLightState(lightsManager.getLights().get(1)).getColor()) + .isEqualTo(0xf2); + assertThat(lightsManager.getLightState(lightsManager.getLights().get(2)).getColor()) + .isEqualTo(0xf3); + + // And the 4th should remain off. + assertThat(lightsManager.getLightState(lightsManager.getLights().get(3)).getColor()) + .isEqualTo(0x00); + + // close session + session.close(); + verify(mIInputManagerMock).closeLightSession(eq(DEVICE_ID), eq(token)); + } + + @Test + public void testControlPlayerIdLight() throws Exception { + InputDevice device = mInputManager.getInputDevice(DEVICE_ID); + assertNotNull(device); + + Light[] mockedLights = { + new Light(1 /* id */, "Light1", 0 /* ordinal */, Light.LIGHT_TYPE_PLAYER_ID, + 0 /* capabilities */), + new Light(2 /* id */, "Light2", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + Light.LIGHT_CAPABILITY_COLOR_RGB | Light.LIGHT_CAPABILITY_BRIGHTNESS), + new Light(3 /* id */, "Light3", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + Light.LIGHT_CAPABILITY_BRIGHTNESS) + }; + mockLights(mockedLights); + + LightsManager lightsManager = device.getLightsManager(); + List<Light> lightList = lightsManager.getLights(); + LightState[] states = new LightState[]{new LightState(0xf1, PLAYER_ID)}; + // Open a session to request set Player ID light: + LightsManager.LightsSession session = lightsManager.openSession(); + session.requestLights(new Builder() + .addLight(lightsManager.getLights().get(0), states[0]) + .build()); + IBinder token = session.getToken(); + + verify(mIInputManagerMock).openLightSession(eq(DEVICE_ID), + any(String.class), eq(token)); + verify(mIInputManagerMock).setLightStates(eq(DEVICE_ID), eq(new int[]{1}), + eq(states), eq(token)); + + // Verify the light state + assertThat(lightsManager.getLightState(lightsManager.getLights().get(0)).getColor()) + .isEqualTo(0xf1); + assertThat(lightsManager.getLightState(lightsManager.getLights().get(0)).getPlayerId()) + .isEqualTo(PLAYER_ID); + + // close session + session.close(); + verify(mIInputManagerMock).closeLightSession(eq(DEVICE_ID), eq(token)); + } + + @Test + public void testLightCapabilities() throws Exception { + Light light = new Light(1 /* id */, "Light1", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + Light.LIGHT_CAPABILITY_COLOR_RGB | Light.LIGHT_CAPABILITY_BRIGHTNESS); + assertThat(light.getType()).isEqualTo(Light.LIGHT_TYPE_INPUT); + assertThat(light.getCapabilities()).isEqualTo(Light.LIGHT_CAPABILITY_COLOR_RGB + | Light.LIGHT_CAPABILITY_BRIGHTNESS); + assertTrue(light.hasBrightnessControl()); + assertTrue(light.hasRgbControl()); + } + + @Test + public void testLightsRequest() throws Exception { + Light light1 = new Light(1 /* id */, "Light1", 0 /* ordinal */, Light.LIGHT_TYPE_INPUT, + 0 /* capabilities */); + Light light2 = new Light(2 /* id */, "Light2", 0 /* ordinal */, Light.LIGHT_TYPE_PLAYER_ID, + 0 /* capabilities */); + LightState state1 = new LightState(0xf1); + LightState state2 = new LightState(0xf2, PLAYER_ID); + LightsRequest request = new Builder().addLight(light1, state1) + .addLight(light2, state2).build(); + + // Covers the LightsRequest.getLights + assertThat(request.getLights().size()).isEqualTo(2); + assertThat(request.getLights().get(0)).isEqualTo(1); + assertThat(request.getLights().get(1)).isEqualTo(2); + + // Covers the LightsRequest.getLightStates + assertThat(request.getLightStates().size()).isEqualTo(2); + assertThat(request.getLightStates().get(0)).isEqualTo(state1); + assertThat(request.getLightStates().get(1)).isEqualTo(state2); + + // Covers the LightsRequest.getLightsAndStates + assertThat(request.getLightsAndStates().size()).isEqualTo(2); + assertThat(request.getLightsAndStates().containsKey(light1)).isTrue(); + assertThat(request.getLightsAndStates().get(light1)).isEqualTo(state1); + assertThat(request.getLightsAndStates().get(light2)).isEqualTo(state2); + } + +} diff --git a/tests/Input/src/android/hardware/input/InputDeviceSensorManagerTest.java b/tests/Input/src/android/hardware/input/InputDeviceSensorManagerTest.java new file mode 100644 index 000000000000..0e3c200699d2 --- /dev/null +++ b/tests/Input/src/android/hardware/input/InputDeviceSensorManagerTest.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2020 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 android.hardware.input; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertTrue; +import static junit.framework.TestCase.fail; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.platform.test.annotations.Presubmit; +import android.view.InputDevice; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.annotations.GuardedBy; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.MockitoRule; + +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * Tests for {@link InputDeviceSensorManager}. + * + * Build/Install/Run: + * atest InputTests:InputDeviceSensorManagerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class InputDeviceSensorManagerTest { + private static final String TAG = "InputDeviceSensorManagerTest"; + + private static final int DEVICE_ID = 1000; + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private InputManager mInputManager; + private IInputSensorEventListener mIInputSensorEventListener; + private final Object mLock = new Object(); + + @Mock private IInputManager mIInputManagerMock; + private InputManagerGlobal.TestSession mInputManagerGlobalSession; + + @Before + public void setUp() throws Exception { + final Context context = spy( + new ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext())); + mInputManagerGlobalSession = InputManagerGlobal.createTestSession(mIInputManagerMock); + mInputManager = new InputManager(context); + when(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(mInputManager); + + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{DEVICE_ID}); + + when(mIInputManagerMock.getInputDevice(eq(DEVICE_ID))).thenReturn( + createInputDeviceWithSensor(DEVICE_ID)); + + when(mIInputManagerMock.getSensorList(eq(DEVICE_ID))).thenReturn(new InputSensorInfo[] { + createInputSensorInfo(DEVICE_ID, Sensor.TYPE_ACCELEROMETER), + createInputSensorInfo(DEVICE_ID, Sensor.TYPE_GYROSCOPE)}); + + when(mIInputManagerMock.enableSensor(eq(DEVICE_ID), anyInt(), anyInt(), anyInt())) + .thenReturn(true); + + when(mIInputManagerMock.registerSensorListener(any())).thenReturn(true); + } + + @After + public void tearDown() { + if (mInputManagerGlobalSession != null) { + mInputManagerGlobalSession.close(); + } + } + + private class InputTestSensorEventListener implements SensorEventListener { + @GuardedBy("mLock") + private final BlockingQueue<SensorEvent> mEvents = new LinkedBlockingQueue<>(); + InputTestSensorEventListener() { + super(); + } + + public SensorEvent waitForSensorEvent() { + try { + return mEvents.poll(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + fail("unexpectedly interrupted while waiting for SensorEvent"); + return null; + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + synchronized (mLock) { + try { + mEvents.put(event); + } catch (InterruptedException ex) { + fail("interrupted while adding a SensorEvent to the queue"); + } + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + } + + private InputDevice createInputDeviceWithSensor(int id) { + return new InputDevice.Builder() + .setId(id) + .setName("Test Device " + id) + .setHasSensor(true) + .build(); + } + + private InputSensorInfo createInputSensorInfo(int id, int type) { + InputSensorInfo info = new InputSensorInfo("name", "vendor", 0 /* version */, + 0 /* handle */, type, 100.0f /*maxRange */, 0.02f /* resolution */, + 0.8f /* power */, 1000 /* minDelay */, 0 /* fifoReservedEventCount */, + 0 /* fifoMaxEventCount */, "" /* stringType */, "" /* requiredPermission */, + 0 /* maxDelay */, 0 /* flags */, id); + return info; + } + + private InputDevice getSensorDevice(int[] deviceIds) { + for (int deviceId : deviceIds) { + InputDevice device = mInputManager.getInputDevice(deviceId); + if (device.hasSensor()) { + return device; + } + } + return null; + } + + @Test + public void getInputDeviceSensors_withExpectedType() throws Exception { + InputDevice device = getSensorDevice(mInputManager.getInputDeviceIds()); + assertNotNull(device); + + SensorManager sensorManager = device.getSensorManager(); + List<Sensor> accelList = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER); + verify(mIInputManagerMock).getSensorList(eq(DEVICE_ID)); + assertEquals(1, accelList.size()); + assertEquals(DEVICE_ID, accelList.get(0).getId()); + assertEquals(Sensor.TYPE_ACCELEROMETER, accelList.get(0).getType()); + + List<Sensor> gyroList = sensorManager.getSensorList(Sensor.TYPE_GYROSCOPE); + verify(mIInputManagerMock).getSensorList(eq(DEVICE_ID)); + assertEquals(1, gyroList.size()); + assertEquals(DEVICE_ID, gyroList.get(0).getId()); + assertEquals(Sensor.TYPE_GYROSCOPE, gyroList.get(0).getType()); + + } + + @Test + public void getInputDeviceSensors_withUnexpectedType() throws Exception { + InputDevice device = getSensorDevice(mInputManager.getInputDeviceIds()); + + assertNotNull(device); + SensorManager sensorManager = device.getSensorManager(); + + List<Sensor> gameRotationList = sensorManager.getSensorList( + Sensor.TYPE_GAME_ROTATION_VECTOR); + verify(mIInputManagerMock).getSensorList(eq(DEVICE_ID)); + assertEquals(0, gameRotationList.size()); + + List<Sensor> gravityList = sensorManager.getSensorList(Sensor.TYPE_GRAVITY); + verify(mIInputManagerMock).getSensorList(eq(DEVICE_ID)); + assertEquals(0, gravityList.size()); + } + + @Test + public void testInputDeviceSensorListener() throws Exception { + InputDevice device = getSensorDevice(mInputManager.getInputDeviceIds()); + assertNotNull(device); + + SensorManager sensorManager = device.getSensorManager(); + Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + assertEquals(Sensor.TYPE_ACCELEROMETER, sensor.getType()); + + doAnswer(invocation -> { + mIInputSensorEventListener = invocation.getArgument(0); + assertNotNull(mIInputSensorEventListener); + return true; + }).when(mIInputManagerMock).registerSensorListener(any()); + + InputTestSensorEventListener listener = new InputTestSensorEventListener(); + assertTrue(sensorManager.registerListener(listener, sensor, + SensorManager.SENSOR_DELAY_NORMAL)); + verify(mIInputManagerMock).registerSensorListener(any()); + verify(mIInputManagerMock).enableSensor(eq(DEVICE_ID), eq(sensor.getType()), + anyInt(), anyInt()); + + float[] values = new float[] {0.12f, 9.8f, 0.2f}; + mIInputSensorEventListener.onInputSensorChanged(DEVICE_ID, Sensor.TYPE_ACCELEROMETER, + SensorManager.SENSOR_STATUS_ACCURACY_HIGH, /* timestamp */ 0x1234abcd, values); + + SensorEvent event = listener.waitForSensorEvent(); + assertNotNull(event); + assertEquals(0x1234abcd, event.timestamp); + assertEquals(values.length, event.values.length); + for (int i = 0; i < values.length; i++) { + assertEquals(values[i], event.values[i], 0.001f); + } + + sensorManager.unregisterListener(listener); + verify(mIInputManagerMock).disableSensor(eq(DEVICE_ID), eq(sensor.getType())); + } + +} diff --git a/tests/Input/src/android/hardware/input/InputManagerTest.kt b/tests/Input/src/android/hardware/input/InputManagerTest.kt new file mode 100644 index 000000000000..152dde94f006 --- /dev/null +++ b/tests/Input/src/android/hardware/input/InputManagerTest.kt @@ -0,0 +1,153 @@ +/* + * 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 android.hardware.input + +import android.content.Context +import android.content.ContextWrapper +import android.content.res.Resources +import android.platform.test.annotations.Presubmit +import android.view.Display +import android.view.DisplayInfo +import android.view.InputDevice +import androidx.test.core.app.ApplicationProvider +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.eq +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoJUnitRunner + +/** + * Tests for [InputManager]. + * + * Build/Install/Run: + * atest InputTests:InputManagerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class InputManagerTest { + + companion object { + const val DEVICE_ID = 42 + const val SECOND_DEVICE_ID = 96 + const val THIRD_DEVICE_ID = 99 + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + private lateinit var devicesChangedListener: IInputDevicesChangedListener + private val deviceGenerationMap = mutableMapOf<Int /*deviceId*/, Int /*generation*/>() + private lateinit var context: Context + private lateinit var inputManager: InputManager + + @Mock + private lateinit var iInputManager: IInputManager + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + + @Before + fun setUp() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + inputManager = InputManager(context) + `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) + `when`(iInputManager.inputDeviceIds).then { + deviceGenerationMap.keys.toIntArray() + } + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + private fun notifyDeviceChanged( + deviceId: Int, + associatedDisplayId: Int, + usiVersion: HostUsiVersion?, + ) { + val generation = deviceGenerationMap[deviceId]?.plus(1) + ?: throw IllegalArgumentException("Device $deviceId was never added!") + deviceGenerationMap[deviceId] = generation + + `when`(iInputManager.getInputDevice(deviceId)) + .thenReturn(createInputDevice(deviceId, associatedDisplayId, usiVersion, generation)) + val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) } + if (::devicesChangedListener.isInitialized) { + devicesChangedListener.onInputDevicesChanged(list.toIntArray()) + } + } + + private fun addInputDevice( + deviceId: Int, + associatedDisplayId: Int, + usiVersion: HostUsiVersion?, + ) { + deviceGenerationMap[deviceId] = 0 + notifyDeviceChanged(deviceId, associatedDisplayId, usiVersion) + } + + @Test + fun testUsiVersionDisplayAssociation() { + addInputDevice(DEVICE_ID, Display.DEFAULT_DISPLAY, null) + addInputDevice(SECOND_DEVICE_ID, Display.INVALID_DISPLAY, HostUsiVersion(9, 8)) + addInputDevice(THIRD_DEVICE_ID, 42, HostUsiVersion(3, 1)) + + val usiVersion = inputManager.getHostUsiVersion(createDisplay(42)) + assertNotNull(usiVersion) + assertEquals(3, usiVersion!!.majorVersion) + assertEquals(1, usiVersion.minorVersion) + } + + @Test + fun testUsiVersionFallBackToDisplayConfig() { + addInputDevice(DEVICE_ID, Display.DEFAULT_DISPLAY, null) + + `when`(iInputManager.getHostUsiVersionFromDisplayConfig(eq(42))) + .thenReturn(HostUsiVersion(9, 8)) + val usiVersion = inputManager.getHostUsiVersion(createDisplay(42)) + assertEquals(HostUsiVersion(9, 8), usiVersion) + } +} + +private fun createInputDevice( + deviceId: Int, + associatedDisplayId: Int, + usiVersion: HostUsiVersion? = null, + generation: Int = -1, +): InputDevice = + InputDevice.Builder() + .setId(deviceId) + .setName("Device $deviceId") + .setDescriptor("descriptor $deviceId") + .setAssociatedDisplayId(associatedDisplayId) + .setUsiVersion(usiVersion) + .setGeneration(generation) + .build() + +private fun createDisplay(displayId: Int): Display { + val res: Resources? = null + return Display(null /* global */, displayId, DisplayInfo(), res) +} diff --git a/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt b/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt new file mode 100644 index 000000000000..23135b2550b0 --- /dev/null +++ b/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt @@ -0,0 +1,176 @@ +/* + * 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 android.hardware.input + +import android.content.Context +import android.content.ContextWrapper +import android.os.Handler +import android.os.HandlerExecutor +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import androidx.test.core.app.ApplicationProvider +import com.android.server.testutils.any +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoJUnitRunner +import java.util.concurrent.Executor +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail + +/** + * Tests for [InputManager.KeyboardBacklightListener]. + * + * Build/Install/Run: + * atest InputTests:KeyboardBacklightListenerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class KeyboardBacklightListenerTest { + @get:Rule + val rule = MockitoJUnit.rule()!! + + private lateinit var testLooper: TestLooper + private var registeredListener: IKeyboardBacklightListener? = null + private lateinit var executor: Executor + private lateinit var context: Context + private lateinit var inputManager: InputManager + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + + @Mock + private lateinit var iInputManagerMock: IInputManager + + @Before + fun setUp() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManagerMock) + testLooper = TestLooper() + executor = HandlerExecutor(Handler(testLooper.looper)) + registeredListener = null + inputManager = InputManager(context) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + // Handle keyboard backlight listener registration. + doAnswer { + val listener = it.getArgument(0) as IKeyboardBacklightListener + if (registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder()) { + // There can only be one registered keyboard backlight listener per process. + fail("Trying to register a new listener when one already exists") + } + registeredListener = listener + null + }.`when`(iInputManagerMock).registerKeyboardBacklightListener(any()) + + // Handle keyboard backlight listener being unregistered. + doAnswer { + val listener = it.getArgument(0) as IKeyboardBacklightListener + if (registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder()) { + fail("Trying to unregister a listener that is not registered") + } + registeredListener = null + null + }.`when`(iInputManagerMock).unregisterKeyboardBacklightListener(any()) + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + private fun notifyKeyboardBacklightChanged( + deviceId: Int, + brightnessLevel: Int, + maxBrightnessLevel: Int = 10, + isTriggeredByKeyPress: Boolean = true + ) { + registeredListener!!.onBrightnessChanged(deviceId, IKeyboardBacklightState().apply { + this.brightnessLevel = brightnessLevel + this.maxBrightnessLevel = maxBrightnessLevel + }, isTriggeredByKeyPress) + } + + @Test + fun testListenerIsNotifiedCorrectly() { + var callbackCount = 0 + + // Add a keyboard backlight listener + inputManager.registerKeyboardBacklightListener(executor) { + deviceId: Int, + keyboardBacklightState: KeyboardBacklightState, + isTriggeredByKeyPress: Boolean -> + callbackCount++ + assertEquals(1, deviceId) + assertEquals(2, keyboardBacklightState.brightnessLevel) + assertEquals(10, keyboardBacklightState.maxBrightnessLevel) + assertEquals(true, isTriggeredByKeyPress) + } + + // Adding the listener should register the callback with InputManagerService. + assertNotNull(registeredListener) + + // Notifying keyboard backlight change will notify the listener. + notifyKeyboardBacklightChanged(1 /*deviceId*/, 2 /* brightnessLevel */) + testLooper.dispatchNext() + assertEquals(1, callbackCount) + } + + @Test + fun testMultipleListeners() { + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.KeyboardBacklightListener { _, _, _ -> callbackCount1++ } + val callback2 = InputManager.KeyboardBacklightListener { _, _, _ -> callbackCount2++ } + + // Add both keyboard backlight listeners + inputManager.registerKeyboardBacklightListener(executor, callback1) + inputManager.registerKeyboardBacklightListener(executor, callback2) + + // Adding the listeners should register the callback with InputManagerService. + assertNotNull(registeredListener) + + // Notifying keyboard backlight change trigger the both callbacks. + notifyKeyboardBacklightChanged(1 /*deviceId*/, 1 /* brightnessLevel */) + testLooper.dispatchAll() + assertEquals(1, callbackCount1) + assertEquals(1, callbackCount2) + + inputManager.unregisterKeyboardBacklightListener(callback2) + // Notifying keyboard backlight change should still trigger callback1. + notifyKeyboardBacklightChanged(1 /*deviceId*/, 2 /* brightnessLevel */) + testLooper.dispatchAll() + assertEquals(2, callbackCount1) + + // Unregister all listeners, should remove registered listener from InputManagerService + inputManager.unregisterKeyboardBacklightListener(callback1) + assertNull(registeredListener) + } +} diff --git a/tests/Input/src/android/hardware/input/VirtualKeyEventTest.java b/tests/Input/src/android/hardware/input/VirtualKeyEventTest.java new file mode 100644 index 000000000000..37cc9b70dd1b --- /dev/null +++ b/tests/Input/src/android/hardware/input/VirtualKeyEventTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.testng.Assert.assertThrows; + +import android.view.KeyEvent; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualKeyEventTest { + + @Test + public void keyEvent_emptyBuilder() { + assertThrows(IllegalArgumentException.class, () -> new VirtualKeyEvent.Builder().build()); + } + + @Test + public void keyEvent_noKeyCode() { + assertThrows(IllegalArgumentException.class, + () -> new VirtualKeyEvent.Builder().setAction(VirtualKeyEvent.ACTION_DOWN).build()); + } + + @Test + public void keyEvent_noAction() { + assertThrows(IllegalArgumentException.class, + () -> new VirtualKeyEvent.Builder().setKeyCode(KeyEvent.KEYCODE_A).build()); + } + + @Test + public void keyEvent_created() { + final VirtualKeyEvent event = new VirtualKeyEvent.Builder() + .setAction(VirtualKeyEvent.ACTION_DOWN) + .setKeyCode(KeyEvent.KEYCODE_A).build(); + assertWithMessage("Incorrect key code").that(event.getKeyCode()).isEqualTo( + KeyEvent.KEYCODE_A); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualKeyEvent.ACTION_DOWN); + } +} diff --git a/tests/Input/src/android/hardware/input/VirtualMouseButtonEventTest.java b/tests/Input/src/android/hardware/input/VirtualMouseButtonEventTest.java new file mode 100644 index 000000000000..789e0bb2ff56 --- /dev/null +++ b/tests/Input/src/android/hardware/input/VirtualMouseButtonEventTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.testng.Assert.assertThrows; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualMouseButtonEventTest { + + @Test + public void buttonEvent_emptyBuilder() { + assertThrows(IllegalArgumentException.class, + () -> new VirtualMouseButtonEvent.Builder().build()); + } + + @Test + public void buttonEvent_noButtonCode() { + assertThrows(IllegalArgumentException.class, () -> new VirtualMouseButtonEvent.Builder() + .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_RELEASE).build()); + } + + @Test + public void buttonEvent_noAction() { + assertThrows(IllegalArgumentException.class, () -> new VirtualMouseButtonEvent.Builder() + .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK).build()); + } + + @Test + public void buttonEvent_created() { + final VirtualMouseButtonEvent event = new VirtualMouseButtonEvent.Builder() + .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_PRESS) + .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK).build(); + assertWithMessage("Incorrect button code").that(event.getButtonCode()).isEqualTo( + VirtualMouseButtonEvent.BUTTON_BACK); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualMouseButtonEvent.ACTION_BUTTON_PRESS); + } +} diff --git a/tests/Input/src/android/hardware/input/VirtualMouseRelativeEventTest.java b/tests/Input/src/android/hardware/input/VirtualMouseRelativeEventTest.java new file mode 100644 index 000000000000..c0508162869b --- /dev/null +++ b/tests/Input/src/android/hardware/input/VirtualMouseRelativeEventTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualMouseRelativeEventTest { + + @Test + public void relativeEvent_created() { + final VirtualMouseRelativeEvent event = new VirtualMouseRelativeEvent.Builder() + .setRelativeX(-5f) + .setRelativeY(8f).build(); + assertWithMessage("Incorrect x value").that(event.getRelativeX()).isEqualTo(-5f); + assertWithMessage("Incorrect y value").that(event.getRelativeY()).isEqualTo(8f); + } +} diff --git a/tests/Input/src/android/hardware/input/VirtualMouseScrollEventTest.java b/tests/Input/src/android/hardware/input/VirtualMouseScrollEventTest.java new file mode 100644 index 000000000000..2259c740da7e --- /dev/null +++ b/tests/Input/src/android/hardware/input/VirtualMouseScrollEventTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.testng.Assert.assertThrows; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualMouseScrollEventTest { + + @Test + public void scrollEvent_xOutOfRange() { + assertThrows(IllegalArgumentException.class, () -> new VirtualMouseScrollEvent.Builder() + .setXAxisMovement(1.5f) + .setYAxisMovement(1.0f)); + } + + @Test + public void scrollEvent_yOutOfRange() { + assertThrows(IllegalArgumentException.class, () -> new VirtualMouseScrollEvent.Builder() + .setXAxisMovement(0.5f) + .setYAxisMovement(1.1f)); + } + + @Test + public void scrollEvent_created() { + final VirtualMouseScrollEvent event = new VirtualMouseScrollEvent.Builder() + .setXAxisMovement(-1f) + .setYAxisMovement(1f).build(); + assertWithMessage("Incorrect x value").that(event.getXAxisMovement()).isEqualTo(-1f); + assertWithMessage("Incorrect y value").that(event.getYAxisMovement()).isEqualTo(1f); + } +} + diff --git a/tests/Input/src/android/hardware/input/VirtualTouchEventTest.java b/tests/Input/src/android/hardware/input/VirtualTouchEventTest.java new file mode 100644 index 000000000000..100aba5ab362 --- /dev/null +++ b/tests/Input/src/android/hardware/input/VirtualTouchEventTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 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 android.hardware.input; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.testng.Assert.assertThrows; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class VirtualTouchEventTest { + + @Test + public void touchEvent_emptyBuilder() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder().build()); + } + + @Test + public void touchEvent_noAction() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build()); + } + + @Test + public void touchEvent_noPointerId() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .build()); + } + + @Test + public void touchEvent_noToolType() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build()); + } + + @Test + public void touchEvent_noX() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setY(1f) + .setPointerId(1) + .build()); + } + + + @Test + public void touchEvent_noY() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setPointerId(1) + .build()); + } + + @Test + public void touchEvent_created() { + final VirtualTouchEvent event = new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build(); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualTouchEvent.ACTION_DOWN); + assertWithMessage("Incorrect tool type").that(event.getToolType()).isEqualTo( + VirtualTouchEvent.TOOL_TYPE_FINGER); + assertWithMessage("Incorrect x").that(event.getX()).isEqualTo(0f); + assertWithMessage("Incorrect y").that(event.getY()).isEqualTo(1f); + assertWithMessage("Incorrect pointer id").that(event.getPointerId()).isEqualTo(1); + } + + @Test + public void touchEvent_created_withPressureAndAxis() { + final VirtualTouchEvent event = new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_DOWN) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .setPointerId(1) + .setPressure(0.5f) + .setMajorAxisSize(10f) + .build(); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualTouchEvent.ACTION_DOWN); + assertWithMessage("Incorrect tool type").that(event.getToolType()).isEqualTo( + VirtualTouchEvent.TOOL_TYPE_FINGER); + assertWithMessage("Incorrect x").that(event.getX()).isEqualTo(0f); + assertWithMessage("Incorrect y").that(event.getY()).isEqualTo(1f); + assertWithMessage("Incorrect pointer id").that(event.getPointerId()).isEqualTo(1); + assertWithMessage("Incorrect pressure").that(event.getPressure()).isEqualTo(0.5f); + assertWithMessage("Incorrect major axis size").that(event.getMajorAxisSize()).isEqualTo( + 10f); + } + + @Test + public void touchEvent_cancelUsedImproperly() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_CANCEL) + .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build()); + } + + /** + * The combination of TOOL_TYPE_PALM with anything else than ACTION_CANCEL should throw an + * exception. This is due to an underlying implementation detail. See documentation of {@link + * VirtualTouchEvent} + * for details. + */ + @Test + public void touchEvent_palmUsedImproperly() { + assertThrows(IllegalArgumentException.class, () -> new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_MOVE) + .setToolType(VirtualTouchEvent.TOOL_TYPE_PALM) + .setX(0f) + .setY(1f) + .setPointerId(1) + .build()); + } + + @Test + public void touchEvent_palmAndCancelUsedProperly() { + final VirtualTouchEvent event = new VirtualTouchEvent.Builder() + .setAction(VirtualTouchEvent.ACTION_CANCEL) + .setToolType(VirtualTouchEvent.TOOL_TYPE_PALM) + .setX(0f) + .setY(1f) + .setPointerId(1) + .setPressure(0.5f) + .setMajorAxisSize(10f) + .build(); + assertWithMessage("Incorrect action").that(event.getAction()).isEqualTo( + VirtualTouchEvent.ACTION_CANCEL); + assertWithMessage("Incorrect tool type").that(event.getToolType()).isEqualTo( + VirtualTouchEvent.TOOL_TYPE_PALM); + assertWithMessage("Incorrect x").that(event.getX()).isEqualTo(0f); + assertWithMessage("Incorrect y").that(event.getY()).isEqualTo(1f); + assertWithMessage("Incorrect pointer id").that(event.getPointerId()).isEqualTo(1); + assertWithMessage("Incorrect pressure").that(event.getPressure()).isEqualTo(0.5f); + assertWithMessage("Incorrect major axis size").that(event.getMajorAxisSize()).isEqualTo( + 10f); + } +} diff --git a/tests/Input/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt b/tests/Input/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt new file mode 100644 index 000000000000..ad481dff810c --- /dev/null +++ b/tests/Input/src/com/android/server/input/AmbientKeyboardBacklightControllerTests.kt @@ -0,0 +1,355 @@ +/* + * Copyright 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.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.content.res.Resources +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.hardware.display.DisplayManagerInternal +import android.hardware.input.InputSensorInfo +import android.os.Handler +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.util.TypedValue +import android.view.Display +import android.view.DisplayInfo +import androidx.test.core.app.ApplicationProvider +import com.android.internal.R +import com.android.server.LocalServices +import com.android.server.input.AmbientKeyboardBacklightController.HYSTERESIS_THRESHOLD +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.spy +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit + +/** + * Tests for {@link AmbientKeyboardBacklightController}. + * + * Build/Install/Run: + * atest InputTests:AmbientKeyboardBacklightControllerTests + */ +@Presubmit +class AmbientKeyboardBacklightControllerTests { + + companion object { + const val DEFAULT_DISPLAY_UNIQUE_ID = "uniqueId_1" + const val SENSOR_NAME = "test_sensor_name" + const val SENSOR_TYPE = "test_sensor_type" + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + private lateinit var context: Context + private lateinit var testLooper: TestLooper + private lateinit var ambientController: AmbientKeyboardBacklightController + + @Mock + private lateinit var resources: Resources + + @Mock + private lateinit var lightSensorInfo: InputSensorInfo + + @Mock + private lateinit var sensorManager: SensorManager + + @Mock + private lateinit var displayManagerInternal: DisplayManagerInternal + private lateinit var lightSensor: Sensor + + private var currentDisplayInfo = DisplayInfo() + private var lastBrightnessCallback: Int = 0 + private var listenerRegistered: Boolean = false + private var listenerRegistrationCount: Int = 0 + + @Before + fun setup() { + context = spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + `when`(context.resources).thenReturn(resources) + setupBrightnessSteps() + setupSensor() + testLooper = TestLooper() + ambientController = AmbientKeyboardBacklightController(context, testLooper.looper) + ambientController.systemRunning() + testLooper.dispatchAll() + } + + private fun setupBrightnessSteps() { + val brightnessValues = intArrayOf(100, 200, 0) + val decreaseThresholds = intArrayOf(-1, 900, 1900) + val increaseThresholds = intArrayOf(1000, 2000, -1) + `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightBrightnessValues)) + .thenReturn(brightnessValues) + `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightDecreaseLuxThreshold)) + .thenReturn(decreaseThresholds) + `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightIncreaseLuxThreshold)) + .thenReturn(increaseThresholds) + `when`( + resources.getValue( + eq(R.dimen.config_autoKeyboardBrightnessSmoothingConstant), + any(TypedValue::class.java), + anyBoolean() + ) + ).then { + val args = it.arguments + val outValue = args[1] as TypedValue + outValue.data = java.lang.Float.floatToRawIntBits(1.0f) + Unit + } + } + + private fun setupSensor() { + LocalServices.removeServiceForTest(DisplayManagerInternal::class.java) + LocalServices.addService(DisplayManagerInternal::class.java, displayManagerInternal) + currentDisplayInfo.uniqueId = DEFAULT_DISPLAY_UNIQUE_ID + `when`(displayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn( + currentDisplayInfo + ) + val sensorData = DisplayManagerInternal.AmbientLightSensorData(SENSOR_NAME, SENSOR_TYPE) + `when`(displayManagerInternal.getAmbientLightSensorData(Display.DEFAULT_DISPLAY)) + .thenReturn(sensorData) + + `when`(lightSensorInfo.name).thenReturn(SENSOR_NAME) + `when`(lightSensorInfo.stringType).thenReturn(SENSOR_TYPE) + lightSensor = Sensor(lightSensorInfo) + `when`(context.getSystemService(eq(Context.SENSOR_SERVICE))).thenReturn(sensorManager) + `when`(sensorManager.getSensorList(anyInt())).thenReturn(listOf(lightSensor)) + `when`( + sensorManager.registerListener( + any(), + eq(lightSensor), + anyInt(), + any(Handler::class.java) + ) + ).then { + listenerRegistered = true + listenerRegistrationCount++ + true + } + `when`( + sensorManager.unregisterListener( + any(SensorEventListener::class.java), + eq(lightSensor) + ) + ).then { + listenerRegistered = false + Unit + } + } + + private fun setupSensorWithInitialLux(luxValue: Float) { + ambientController.registerAmbientBacklightListener { brightnessValue: Int -> + lastBrightnessCallback = brightnessValue + } + sendAmbientLuxValue(luxValue) + testLooper.dispatchAll() + } + + @Test + fun testInitialAmbientLux_sendsCallbackImmediately() { + setupSensorWithInitialLux(500F) + + assertEquals( + "Should receive immediate callback for first lux change", + 100, + lastBrightnessCallback + ) + } + + @Test + fun testBrightnessIncrease_afterInitialLuxChanges() { + setupSensorWithInitialLux(500F) + + // Current state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1] + repeat(HYSTERESIS_THRESHOLD) { + sendAmbientLuxValue(1500F) + } + testLooper.dispatchAll() + + assertEquals( + "Should receive brightness change callback for increasing lux change", + 200, + lastBrightnessCallback + ) + } + + @Test + fun testBrightnessDecrease_afterInitialLuxChanges() { + setupSensorWithInitialLux(1500F) + + // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900] + repeat(HYSTERESIS_THRESHOLD) { + sendAmbientLuxValue(500F) + } + testLooper.dispatchAll() + + assertEquals( + "Should receive brightness change callback for decreasing lux change", + 100, + lastBrightnessCallback + ) + } + + @Test + fun testRegisterAmbientListener_throwsExceptionOnRegisteringDuplicate() { + val ambientListener = + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + ambientController.registerAmbientBacklightListener(ambientListener) + + assertThrows(IllegalStateException::class.java) { + ambientController.registerAmbientBacklightListener( + ambientListener + ) + } + } + + @Test + fun testUnregisterAmbientListener_throwsExceptionOnUnregisteringNonExistent() { + val ambientListener = + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + assertThrows(IllegalStateException::class.java) { + ambientController.unregisterAmbientBacklightListener( + ambientListener + ) + } + } + + @Test + fun testSensorListenerRegistered_onRegisterUnregisterAmbientListener() { + assertEquals( + "Should not have a sensor listener registered at init", + 0, + listenerRegistrationCount + ) + assertFalse("Should not have a sensor listener registered at init", listenerRegistered) + + val ambientListener1 = + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + ambientController.registerAmbientBacklightListener(ambientListener1) + assertEquals( + "Should register a new sensor listener", 1, listenerRegistrationCount + ) + assertTrue("Should have sensor listener registered", listenerRegistered) + + val ambientListener2 = + AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener { } + ambientController.registerAmbientBacklightListener(ambientListener2) + assertEquals( + "Should not register a new sensor listener when adding a second ambient listener", + 1, + listenerRegistrationCount + ) + assertTrue("Should have sensor listener registered", listenerRegistered) + + ambientController.unregisterAmbientBacklightListener(ambientListener1) + assertTrue("Should have sensor listener registered", listenerRegistered) + + ambientController.unregisterAmbientBacklightListener(ambientListener2) + assertFalse( + "Should not have sensor listener registered if there are no ambient listeners", + listenerRegistered + ) + } + + @Test + fun testDisplayChange_shouldNotReRegisterListener_ifUniqueIdSame() { + setupSensorWithInitialLux(0F) + + val count = listenerRegistrationCount + ambientController.onDisplayChanged(Display.DEFAULT_DISPLAY) + testLooper.dispatchAll() + + assertEquals( + "Should not re-register listener on display change if unique is same", + count, + listenerRegistrationCount + ) + } + + @Test + fun testDisplayChange_shouldReRegisterListener_ifUniqueIdChanges() { + setupSensorWithInitialLux(0F) + + val count = listenerRegistrationCount + currentDisplayInfo.uniqueId = "xyz" + ambientController.onDisplayChanged(Display.DEFAULT_DISPLAY) + testLooper.dispatchAll() + + assertEquals( + "Should re-register listener on display change if unique id changed", + count + 1, + listenerRegistrationCount + ) + } + + @Test + fun testBrightnessDoesntChange_betweenIncreaseAndDecreaseThresholds() { + setupSensorWithInitialLux(1001F) + + // Previous state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1] + // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900] + lastBrightnessCallback = -1 + repeat(HYSTERESIS_THRESHOLD) { + sendAmbientLuxValue(999F) + } + testLooper.dispatchAll() + + assertEquals( + "Should not receive any callback for brightness change", + -1, + lastBrightnessCallback + ) + } + + @Test + fun testBrightnessDoesntChange_onChangeOccurringLessThanHysteresisThreshold() { + setupSensorWithInitialLux(1001F) + + // Previous state: Step 1 [value = 100, increaseThreshold = 1000, decreaseThreshold = -1] + // Current state: Step 2 [value = 200, increaseThreshold = 2000, decreaseThreshold = 900] + lastBrightnessCallback = -1 + repeat(HYSTERESIS_THRESHOLD - 1) { + sendAmbientLuxValue(2001F) + } + testLooper.dispatchAll() + + assertEquals( + "Should not receive any callback for brightness change", + -1, + lastBrightnessCallback + ) + } + + private fun sendAmbientLuxValue(luxValue: Float) { + ambientController.onSensorChanged(SensorEvent(lightSensor, 0, 0, floatArrayOf(luxValue))) + } +} diff --git a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt new file mode 100644 index 000000000000..f2724e605553 --- /dev/null +++ b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt @@ -0,0 +1,898 @@ +/* + * 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.server.input + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.hardware.BatteryState.STATUS_CHARGING +import android.hardware.BatteryState.STATUS_DISCHARGING +import android.hardware.BatteryState.STATUS_FULL +import android.hardware.BatteryState.STATUS_UNKNOWN +import android.hardware.input.HostUsiVersion +import android.hardware.input.IInputDeviceBatteryListener +import android.hardware.input.IInputDeviceBatteryState +import android.hardware.input.IInputDevicesChangedListener +import android.hardware.input.IInputManager +import android.hardware.input.InputManager +import android.hardware.input.InputManagerGlobal +import android.os.Binder +import android.os.IBinder +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.testing.TestableContext +import android.view.InputDevice +import androidx.test.core.app.ApplicationProvider +import com.android.server.input.BatteryController.BluetoothBatteryManager +import com.android.server.input.BatteryController.BluetoothBatteryManager.BluetoothBatteryListener +import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS +import com.android.server.input.BatteryController.UEventBatteryListener +import com.android.server.input.BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.TypeSafeMatcher +import org.hamcrest.core.IsEqual.equalTo +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.notNull +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` +import org.mockito.hamcrest.MockitoHamcrest +import org.mockito.junit.MockitoJUnit +import org.mockito.verification.VerificationMode + +private fun createInputDevice( + deviceId: Int, + hasBattery: Boolean = true, + supportsUsi: Boolean = false, + generation: Int = -1, +): InputDevice = + InputDevice.Builder() + .setId(deviceId) + .setName("Device $deviceId") + .setDescriptor("descriptor $deviceId") + .setExternal(true) + .setHasBattery(hasBattery) + .setUsiVersion(if (supportsUsi) HostUsiVersion(1, 0) else null) + .setGeneration(generation) + .build() + +// Returns a matcher that helps match member variables of a class. +private fun <T, U> memberMatcher( + member: String, + memberProvider: (T) -> U, + match: Matcher<U> +): TypeSafeMatcher<T> = + object : TypeSafeMatcher<T>() { + + override fun matchesSafely(item: T?): Boolean { + return match.matches(memberProvider(item!!)) + } + + override fun describeMismatchSafely(item: T?, mismatchDescription: Description?) { + match.describeMismatch(item, mismatchDescription) + } + + override fun describeTo(description: Description?) { + match.describeTo(description?.appendText("matches member $member")) + } + } + +// Returns a matcher for IInputDeviceBatteryState that optionally matches some arguments. +private fun matchesState( + deviceId: Int, + isPresent: Boolean = true, + status: Int? = null, + capacity: Float? = null, + eventTime: Long? = null +): Matcher<IInputDeviceBatteryState> { + val batteryStateMatchers = mutableListOf<Matcher<IInputDeviceBatteryState>>( + memberMatcher("deviceId", { it.deviceId }, equalTo(deviceId)), + memberMatcher("isPresent", { it.isPresent }, equalTo(isPresent)) + ) + if (eventTime != null) { + batteryStateMatchers.add(memberMatcher("updateTime", { it.updateTime }, equalTo(eventTime))) + } + if (status != null) { + batteryStateMatchers.add(memberMatcher("status", { it.status }, equalTo(status))) + } + if (capacity != null) { + batteryStateMatchers.add(memberMatcher("capacity", { it.capacity }, equalTo(capacity))) + } + return Matchers.allOf(batteryStateMatchers) +} + +private fun isInvalidBatteryState(deviceId: Int): Matcher<IInputDeviceBatteryState> = + matchesState(deviceId, isPresent = false, status = STATUS_UNKNOWN, capacity = Float.NaN) + +// Helpers used to verify interactions with a mocked battery listener. +private fun IInputDeviceBatteryListener.verifyNotified( + deviceId: Int, + mode: VerificationMode = times(1), + isPresent: Boolean = true, + status: Int? = null, + capacity: Float? = null, + eventTime: Long? = null +) { + verifyNotified(matchesState(deviceId, isPresent, status, capacity, eventTime), mode) +} + +private fun IInputDeviceBatteryListener.verifyNotified( + matcher: Matcher<IInputDeviceBatteryState>, + mode: VerificationMode = times(1) +) { + verify(this, mode).onBatteryStateChanged(MockitoHamcrest.argThat(matcher)) +} + +private fun createMockListener(): IInputDeviceBatteryListener { + val listener = mock(IInputDeviceBatteryListener::class.java) + val binder = mock(Binder::class.java) + `when`(listener.asBinder()).thenReturn(binder) + return listener +} + +/** + * Tests for {@link InputDeviceBatteryController}. + * + * Build/Install/Run: + * atest InputTests:InputDeviceBatteryControllerTests + */ +@Presubmit +class BatteryControllerTests { + companion object { + const val PID = 42 + const val DEVICE_ID = 13 + const val SECOND_DEVICE_ID = 11 + const val USI_DEVICE_ID = 101 + const val SECOND_USI_DEVICE_ID = 102 + const val BT_DEVICE_ID = 100001 + const val SECOND_BT_DEVICE_ID = 100002 + const val TIMESTAMP = 123456789L + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + @Mock + private lateinit var native: NativeInputManagerService + @Mock + private lateinit var iInputManager: IInputManager + @Mock + private lateinit var uEventManager: UEventManager + @Mock + private lateinit var bluetoothBatteryManager: BluetoothBatteryManager + + private lateinit var batteryController: BatteryController + private lateinit var context: TestableContext + private lateinit var testLooper: TestLooper + private lateinit var devicesChangedListener: IInputDevicesChangedListener + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + private val deviceGenerationMap = mutableMapOf<Int /*deviceId*/, Int /*generation*/>() + + @Before + fun setup() { + context = TestableContext(ApplicationProvider.getApplicationContext()) + testLooper = TestLooper() + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + val inputManager = InputManager(context) + context.addMockSystemService(InputManager::class.java, inputManager) + `when`(iInputManager.inputDeviceIds).then { + deviceGenerationMap.keys.toIntArray() + } + addInputDevice(DEVICE_ID) + addInputDevice(SECOND_DEVICE_ID) + + batteryController = BatteryController(context, native, testLooper.looper, uEventManager, + bluetoothBatteryManager) + batteryController.systemRunning() + val listenerCaptor = ArgumentCaptor.forClass(IInputDevicesChangedListener::class.java) + verify(iInputManager).registerInputDevicesChangedListener(listenerCaptor.capture()) + devicesChangedListener = listenerCaptor.value + testLooper.dispatchAll() + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + private fun notifyDeviceChanged( + deviceId: Int, + hasBattery: Boolean = true, + supportsUsi: Boolean = false + ) { + val generation = deviceGenerationMap[deviceId]?.plus(1) + ?: throw IllegalArgumentException("Device $deviceId was never added!") + deviceGenerationMap[deviceId] = generation + + `when`(iInputManager.getInputDevice(deviceId)) + .thenReturn(createInputDevice(deviceId, hasBattery, supportsUsi, generation)) + val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) } + if (::devicesChangedListener.isInitialized) { + devicesChangedListener.onInputDevicesChanged(list.toIntArray()) + } + } + + private fun addInputDevice( + deviceId: Int, + hasBattery: Boolean = true, + supportsUsi: Boolean = false, + ) { + deviceGenerationMap[deviceId] = 0 + notifyDeviceChanged(deviceId, hasBattery, supportsUsi) + } + + private fun createBluetoothDevice(address: String): BluetoothDevice { + return context.getSystemService(BluetoothManager::class.java)!! + .adapter.getRemoteDevice(address) + } + + @Test + fun testRegisterAndUnregisterBinderLifecycle() { + val listener = createMockListener() + // Ensure the binder lifecycle is tracked when registering a listener. + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder()).linkToDeath(notNull(), anyInt()) + batteryController.registerBatteryListener(SECOND_DEVICE_ID, listener, PID) + verify(listener.asBinder(), times(1)).linkToDeath(notNull(), anyInt()) + + // Ensure the binder lifecycle stops being tracked when all devices stopped being monitored. + batteryController.unregisterBatteryListener(SECOND_DEVICE_ID, listener, PID) + verify(listener.asBinder(), never()).unlinkToDeath(notNull(), anyInt()) + batteryController.unregisterBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder()).unlinkToDeath(notNull(), anyInt()) + } + + @Test + fun testOneListenerPerProcess() { + val listener1 = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener1, PID) + verify(listener1.asBinder()).linkToDeath(notNull(), anyInt()) + + // If a second listener is added for the same process, a security exception is thrown. + val listener2 = createMockListener() + try { + batteryController.registerBatteryListener(DEVICE_ID, listener2, PID) + fail("Expected security exception when registering more than one listener per process") + } catch (ignored: SecurityException) { + } + } + + @Test + fun testProcessDeathRemovesListener() { + val deathRecipient = ArgumentCaptor.forClass(IBinder.DeathRecipient::class.java) + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder()).linkToDeath(deathRecipient.capture(), anyInt()) + + // When the binder dies, the callback is unregistered. + deathRecipient.value!!.binderDied(listener.asBinder()) + verify(listener.asBinder()).unlinkToDeath(notNull(), anyInt()) + + // It is now possible to register the same listener again. + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder(), times(2)).linkToDeath(notNull(), anyInt()) + } + + @Test + fun testRegisteringListenerNotifiesStateImmediately() { + `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_FULL) + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(100) + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + listener.verifyNotified(DEVICE_ID, status = STATUS_FULL, capacity = 1.0f) + + `when`(native.getBatteryStatus(SECOND_DEVICE_ID)).thenReturn(STATUS_CHARGING) + `when`(native.getBatteryCapacity(SECOND_DEVICE_ID)).thenReturn(78) + batteryController.registerBatteryListener(SECOND_DEVICE_ID, listener, PID) + listener.verifyNotified(SECOND_DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) + } + + @Test + fun testListenersNotifiedOnUEventNotification() { + `when`(native.getBatteryDevicePath(DEVICE_ID)).thenReturn("/sys/dev/test/device1") + `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING) + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) + val listener = createMockListener() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + // The device paths for UEvent notifications do not include the "/sys" prefix, so verify + // that the added listener is configured to match the path without that prefix. + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/test/device1")) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) + + // If the battery state has changed when an UEvent is sent, the listeners are notified. + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f, + eventTime = TIMESTAMP) + + // If the battery state has not changed when an UEvent is sent, the listeners are not + // notified. + clearInvocations(listener) + uEventListener.value!!.onBatteryUEvent(TIMESTAMP + 1) + verifyNoMoreInteractions(listener) + + batteryController.unregisterBatteryListener(DEVICE_ID, listener, PID) + verify(uEventManager).removeListener(uEventListener.capture()) + assertEquals("The same observer must be registered and unregistered", + uEventListener.allValues[0], uEventListener.allValues[1]) + } + + @Test + fun testBatteryPresenceChanged() { + `when`(native.getBatteryDevicePath(DEVICE_ID)).thenReturn("/test/device1") + `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING) + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) + val listener = createMockListener() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/test/device1")) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) + + // If the battery presence for the InputDevice changes, the listener is notified. + notifyDeviceChanged(DEVICE_ID, hasBattery = false) + testLooper.dispatchNext() + listener.verifyNotified(isInvalidBatteryState(DEVICE_ID)) + // Since the battery is no longer present, the UEventListener should be removed. + verify(uEventManager).removeListener(uEventListener.value) + + // If the battery becomes present again, the listener is notified. + notifyDeviceChanged(DEVICE_ID, hasBattery = true) + testLooper.dispatchNext() + listener.verifyNotified(DEVICE_ID, mode = times(2), status = STATUS_CHARGING, + capacity = 0.78f) + // Ensure that a new UEventListener was added. + verify(uEventManager, times(2)) + .addListener(uEventListener.capture(), eq("DEVPATH=/test/device1")) + } + + @Test + fun testStartPollingWhenListenerIsRegistered() { + val listener = createMockListener() + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + listener.verifyNotified(DEVICE_ID, capacity = 0.78f) + + // Assume there is a change in the battery state. Ensure the listener is not notified + // while the polling period has not elapsed. + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) + testLooper.moveTimeForward(1) + testLooper.dispatchAll() + listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f) + + // Move the time forward so that the polling period has elapsed. + // The listener should be notified. + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS - 1) + assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle) + testLooper.dispatchNext() + listener.verifyNotified(DEVICE_ID, capacity = 0.80f) + + // Move the time forward so that another polling period has elapsed. + // The battery should still be polled, but there is no change so listeners are not notified. + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle) + testLooper.dispatchNext() + listener.verifyNotified(DEVICE_ID, mode = times(1), capacity = 0.80f) + } + + @Test + fun testNoPollingWhenTheDeviceIsNotInteractive() { + batteryController.onInteractiveChanged(false /*interactive*/) + + val listener = createMockListener() + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + listener.verifyNotified(DEVICE_ID, capacity = 0.78f) + + // The battery state changed, but we should not be polling for battery changes when the + // device is not interactive. + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle) + testLooper.dispatchAll() + listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f) + + // The device is now interactive. Battery state polling begins immediately. + batteryController.onInteractiveChanged(true /*interactive*/) + testLooper.dispatchNext() + listener.verifyNotified(DEVICE_ID, capacity = 0.80f) + + // Ensure that we continue to poll for battery changes. + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(90) + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle) + testLooper.dispatchNext() + listener.verifyNotified(DEVICE_ID, capacity = 0.90f) + } + + @Test + fun testGetBatteryState() { + `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING) + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) + val batteryState = batteryController.getBatteryState(DEVICE_ID) + assertThat("battery state matches", batteryState, + matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f)) + } + + @Test + fun testGetBatteryStateWithListener() { + val listener = createMockListener() + `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING) + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) + + // If getBatteryState() is called when a listener is monitoring the device and there is a + // change in the battery state, the listener is also notified. + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) + val batteryState = batteryController.getBatteryState(DEVICE_ID) + assertThat("battery matches state", batteryState, + matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f)) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f) + } + + @Test + fun testUsiDeviceIsMonitoredPersistently() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + + // Even though there is no listener added for this device, it is being monitored. + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // Add and remove a listener for the device. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + batteryController.unregisterBatteryListener(USI_DEVICE_ID, listener, PID) + + // The device is still being monitored. + verify(uEventManager, never()).removeListener(uEventListener.value) + } + + @Test + fun testNoPollingWhenUsiDevicesAreMonitored() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + `when`(native.getBatteryDevicePath(SECOND_USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device2") + addInputDevice(SECOND_USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle) + + // Add a listener. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + + testLooper.moveTimeForward(POLLING_PERIOD_MILLIS) + assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle) + } + + @Test + fun testExpectedFlowForUsiBattery() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // A USI device's battery state is not valid until the first UEvent notification. + // Add a listener, and ensure it is notified that the battery state is not present. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + + // Ensure that querying for battery state also returns the same invalid result. + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // There is a UEvent signaling a battery change. The battery state is now valid. + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + + // There is another UEvent notification. The battery state is now updated. + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(64) + uEventListener.value!!.onBatteryUEvent(TIMESTAMP + 1) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)) + + // The battery state is still valid after a millisecond. + testLooper.moveTimeForward(1) + testLooper.dispatchAll() + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)) + + // The battery is no longer present after the timeout expires. + testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 1) + testLooper.dispatchNext() + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID), times(2)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + } + + @Test + fun testStylusPresenceExtendsValidUsiBatteryState() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // There is a UEvent signaling a battery change. The battery state is now valid. + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + + // Stylus presence is detected before the validity timeout expires. + testLooper.moveTimeForward(100) + testLooper.dispatchAll() + batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) + + // Ensure that timeout was extended, and the battery state is now valid for longer. + testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 100) + testLooper.dispatchAll() + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + + // Ensure the validity period expires after the expected amount of time. + testLooper.moveTimeForward(100) + testLooper.dispatchNext() + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + } + + @Test + fun testStylusPresenceMakesUsiBatteryStateValid() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // The USI battery state is initially invalid. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // A stylus presence is detected. This validates the battery state. + batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) + + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)) + } + + @Test + fun testStylusPresenceDoesNotMakeUsiBatteryStateValidAtBoot() { + `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device") + // At boot, the USI device always reports a capacity value of 0. + `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_UNKNOWN) + `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(0) + + addInputDevice(USI_DEVICE_ID, supportsUsi = true) + testLooper.dispatchNext() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + verify(uEventManager) + .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device")) + + // The USI battery state is initially invalid. + val listener = createMockListener() + batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID) + listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID)) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // Since the capacity reported is 0, stylus presence does not validate the battery state. + batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP) + + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + isInvalidBatteryState(USI_DEVICE_ID)) + + // However, if a UEvent reports a battery capacity of 0, the battery state is now valid. + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + listener.verifyNotified(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f) + assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID), + matchesState(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f)) + } + + @Test + fun testRegisterBluetoothListenerForMonitoredBluetoothDevices() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn("11:22:33:44:55:66") + addInputDevice(BT_DEVICE_ID) + testLooper.dispatchNext() + addInputDevice(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + + // Listen to a non-Bluetooth device and ensure that the BT battery listener is not added + // when there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, never()).addBatteryListener(any()) + + val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) + + // The BT battery listener is added when the first BT input device is monitored. + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + + // The BT listener is only added once for all BT devices. + batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, times(1)).addBatteryListener(any()) + + // The BT listener is only removed when there are no monitored BT devices. + batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, never()).removeBatteryListener(any()) + + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn(null) + notifyDeviceChanged(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + verify(bluetoothBatteryManager).removeBatteryListener(bluetoothListener.value) + } + + @Test + fun testNotifiesBluetoothBatteryChanges() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21) + addInputDevice(BT_DEVICE_ID) + val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) + val listener = createMockListener() + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) + + // When the state has not changed, the listener is not notified again. + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 21) + listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) + + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 25) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f) + } + + @Test + fun testBluetoothBatteryIsPrioritized() { + `when`(native.getBatteryDevicePath(BT_DEVICE_ID)).thenReturn("/sys/dev/bt_device") + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21) + `when`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(98) + addInputDevice(BT_DEVICE_ID) + val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) + val listener = createMockListener() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + + // When the device is first monitored and both native and BT battery is available, + // the latter is used. + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + verify(uEventManager).addListener(uEventListener.capture(), any()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) + assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID), + matchesState(BT_DEVICE_ID, capacity = 0.21f)) + + // If only the native battery state changes the listener is not notified. + `when`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(97) + uEventListener.value!!.onBatteryUEvent(TIMESTAMP) + listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) + assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID), + matchesState(BT_DEVICE_ID, capacity = 0.21f)) + } + + @Test + fun testFallBackToNativeBatteryStateWhenBluetoothStateInvalid() { + `when`(native.getBatteryDevicePath(BT_DEVICE_ID)).thenReturn("/sys/dev/bt_device") + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21) + `when`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(98) + addInputDevice(BT_DEVICE_ID) + val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) + val listener = createMockListener() + val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) + + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + verify(uEventManager).addListener(uEventListener.capture(), any()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) + + // Fall back to the native state when BT is off. + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.98f) + + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 22) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) + + // Fall back to the native state when BT battery is unknown. + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_UNKNOWN) + listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.98f) + } + + @Test + fun testRegisterBluetoothMetadataListenerForMonitoredBluetoothDevices() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn("11:22:33:44:55:66") + addInputDevice(BT_DEVICE_ID) + testLooper.dispatchNext() + addInputDevice(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + + // Listen to a non-Bluetooth device and ensure that the metadata listener is not added when + // there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, never()).addMetadataListener(any(), any()) + + val metadataListener1 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val metadataListener2 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + + // The metadata listener is added when the first BT input device is monitored. + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener1.capture()) + + // There is one metadata listener added for each BT device. + batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("11:22:33:44:55:66"), metadataListener2.capture()) + + // The metadata listener is removed when the device is no longer monitored. + batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .removeMetadataListener("AA:BB:CC:DD:EE:FF", metadataListener1.value) + + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn(null) + notifyDeviceChanged(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + verify(bluetoothBatteryManager) + .removeMetadataListener("11:22:33:44:55:66", metadataListener2.value) + } + + @Test + fun testNotifiesBluetoothMetadataBatteryChanges() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("21".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f, status = STATUS_UNKNOWN) + + // When the state has not changed, the listener is not notified again. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "21".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "25".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_UNKNOWN) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "true".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_CHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "false".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_DISCHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, null) + listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.25f, + status = STATUS_UNKNOWN) + } + + @Test + fun testBluetoothMetadataBatteryIsPrioritized() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21) + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("22".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) + + // A change in the Bluetooth battery level has no effect while there is a valid battery + // level obtained through the metadata. + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 23) + listener.verifyNotified(BT_DEVICE_ID, mode = never(), capacity = 0.23f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "24".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.24f) + + // When the battery level from the metadata is no longer valid, we fall back to using the + // Bluetooth battery level. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, null) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.23f) + } +} diff --git a/tests/Input/src/com/android/server/input/ConfigurationProcessorTest.java b/tests/Input/src/com/android/server/input/ConfigurationProcessorTest.java new file mode 100644 index 000000000000..9a49d91b1019 --- /dev/null +++ b/tests/Input/src/com/android/server/input/ConfigurationProcessorTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2018 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.server.input; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.InputStream; +import java.util.Map; + +/** + * Build/Install/Run: + * atest ConfigurationProcessorTest + */ +@RunWith(AndroidJUnit4.class) +public class ConfigurationProcessorTest { + + private Context mContext; + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + } + + @Test + public void testGetInputPortAssociations() { + final int res = com.android.test.input.R.raw.input_port_associations; + InputStream xml = mContext.getResources().openRawResource(res); + Map<String, Integer> associations = null; + try { + associations = ConfigurationProcessor.processInputPortAssociations(xml); + } catch (Exception e) { + fail("Could not process xml file for input associations"); + } + assertNotNull(associations); + assertEquals(2, associations.size()); + assertEquals(0, associations.get("USB1").intValue()); + assertEquals(1, associations.get("USB2").intValue()); + } + + @Test + public void testGetInputPortAssociationsBadDisplayport() { + final int res = + com.android.test.input.R.raw.input_port_associations_bad_displayport; + InputStream xml = mContext.getResources().openRawResource(res); + Map<String, Integer> associations = null; + try { + associations = ConfigurationProcessor.processInputPortAssociations(xml); + } catch (Exception e) { + fail("Could not process xml file for input associations"); + } + assertNotNull(associations); + assertEquals(0, associations.size()); + } + + @Test + public void testGetInputPortAssociationsEmptyConfig() { + final int res = com.android.test.input.R.raw.input_port_associations_bad_xml; + InputStream xml = mContext.getResources().openRawResource(res); + try { + ConfigurationProcessor.processInputPortAssociations(xml); + fail("Parsing should fail, because xml contains bad data"); + } catch (Exception e) { + // This is expected + } + } +} diff --git a/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java b/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java new file mode 100644 index 000000000000..1b98887199e3 --- /dev/null +++ b/tests/Input/src/com/android/server/input/FocusEventDebugViewTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 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.server.input; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; +import android.view.ViewConfiguration; + +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest FocusEventDebugViewTest + */ +@RunWith(AndroidJUnit4.class) +public class FocusEventDebugViewTest { + + private FocusEventDebugView mFocusEventDebugView; + private FocusEventDebugView.RotaryInputValueView mRotaryInputValueView; + private FocusEventDebugView.RotaryInputGraphView mRotaryInputGraphView; + private float mScaledVerticalScrollFactor; + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getContext(); + mScaledVerticalScrollFactor = + ViewConfiguration.get(context).getScaledVerticalScrollFactor(); + InputManagerService mockService = mock(InputManagerService.class); + when(mockService.monitorInput(anyString(), anyInt())) + .thenReturn(InputChannel.openInputChannelPair("FocusEventDebugViewTest")[1]); + + mRotaryInputValueView = new FocusEventDebugView.RotaryInputValueView(context); + mRotaryInputGraphView = new FocusEventDebugView.RotaryInputGraphView(context); + mFocusEventDebugView = new FocusEventDebugView(context, mockService, + () -> mRotaryInputValueView, () -> mRotaryInputGraphView); + } + + @Test + public void startsRotaryInputValueViewWithDefaultValue() { + assertEquals("+0.0", mRotaryInputValueView.getText()); + } + + @Test + public void startsRotaryInputGraphViewWithDefaultFrameCenter() { + assertEquals(0, mRotaryInputGraphView.getFrameCenterPosition(), 0.01); + } + + @Test + public void handleRotaryInput_updatesRotaryInputValueViewWithScrollValue() { + mFocusEventDebugView.handleUpdateShowRotaryInput(true); + + mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(0.5f)); + + assertEquals(String.format("+%.1f", 0.5f * mScaledVerticalScrollFactor), + mRotaryInputValueView.getText()); + } + + @Test + public void handleRotaryInput_translatesRotaryInputGraphViewWithHighScrollValue() { + mFocusEventDebugView.handleUpdateShowRotaryInput(true); + + mFocusEventDebugView.handleRotaryInput(createRotaryMotionEvent(1000f)); + + assertTrue(mRotaryInputGraphView.getFrameCenterPosition() > 0); + } + + @Test + public void updateActivityStatus_setsAndRemovesColorFilter() { + // It should not be active initially. + assertNull(mRotaryInputValueView.getBackground().getColorFilter()); + + mRotaryInputValueView.updateActivityStatus(true); + // It should be active after rotary input. + assertNotNull(mRotaryInputValueView.getBackground().getColorFilter()); + + mRotaryInputValueView.updateActivityStatus(false); + // It should not be active after waiting for mUpdateActivityStatusCallback. + assertNull(mRotaryInputValueView.getBackground().getColorFilter()); + } + + private MotionEvent createRotaryMotionEvent(float scrollAxisValue) { + PointerCoords pointerCoords = new PointerCoords(); + pointerCoords.setAxisValue(MotionEvent.AXIS_SCROLL, scrollAxisValue); + PointerProperties pointerProperties = new PointerProperties(); + + return MotionEvent.obtain( + /* downTime */ 0, + /* eventTime */ 0, + /* action */ MotionEvent.ACTION_SCROLL, + /* pointerCount */ 1, + /* pointerProperties */ new PointerProperties[] {pointerProperties}, + /* pointerCoords */ new PointerCoords[] {pointerCoords}, + /* metaState */ 0, + /* buttonState */ 0, + /* xPrecision */ 0, + /* yPrecision */ 0, + /* deviceId */ 0, + /* edgeFlags */ 0, + /* source */ InputDevice.SOURCE_ROTARY_ENCODER, + /* flags */ 0 + ); + } +} diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt new file mode 100644 index 000000000000..93a558287bd6 --- /dev/null +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -0,0 +1,402 @@ +/* + * 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.server.input + + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.display.DisplayViewport +import android.hardware.input.InputManager +import android.hardware.input.InputManagerGlobal +import android.os.IInputConstants +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.provider.Settings +import android.test.mock.MockContentResolver +import android.view.Display +import android.view.PointerIcon +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.util.test.FakeSettingsProvider +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.junit.MockitoJUnit +import org.mockito.stubbing.OngoingStubbing +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * Tests for {@link InputManagerService}. + * + * Build/Install/Run: + * atest InputTests:InputManagerServiceTests + */ +@Presubmit +class InputManagerServiceTests { + + @get:Rule + val mockitoRule = MockitoJUnit.rule()!! + + @get:Rule + val fakeSettingsProviderRule = FakeSettingsProvider.rule()!! + + @Mock + private lateinit var native: NativeInputManagerService + + @Mock + private lateinit var wmCallbacks: InputManagerService.WindowManagerCallbacks + + @Mock + private lateinit var uEventManager: UEventManager + + private lateinit var service: InputManagerService + private lateinit var localService: InputManagerInternal + private lateinit var context: Context + private lateinit var testLooper: TestLooper + private lateinit var contentResolver: MockContentResolver + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + + @Before + fun setup() { + context = spy(ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext())) + contentResolver = MockContentResolver(context) + contentResolver.addProvider(Settings.AUTHORITY, FakeSettingsProvider()) + whenever(context.contentResolver).thenReturn(contentResolver) + testLooper = TestLooper() + service = + InputManagerService(object : InputManagerService.Injector( + context, testLooper.looper, uEventManager) { + override fun getNativeService( + service: InputManagerService? + ): NativeInputManagerService { + return native + } + + override fun registerLocalService(service: InputManagerInternal?) { + localService = service!! + } + }) + inputManagerGlobalSession = InputManagerGlobal.createTestSession(service) + val inputManager = InputManager(context) + whenever(context.getSystemService(InputManager::class.java)).thenReturn(inputManager) + whenever(context.getSystemService(Context.INPUT_SERVICE)).thenReturn(inputManager) + + assertTrue("Local service must be registered", this::localService.isInitialized) + service.setWindowManagerCallbacks(wmCallbacks) + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + @Test + fun testStart() { + verifyZeroInteractions(native) + + service.start() + verify(native).start() + } + + @Test + fun testInputSettingsUpdatedOnSystemRunning() { + verifyZeroInteractions(native) + + service.systemRunning() + + verify(native).setPointerSpeed(anyInt()) + verify(native).setTouchpadPointerSpeed(anyInt()) + verify(native).setTouchpadNaturalScrollingEnabled(anyBoolean()) + verify(native).setTouchpadTapToClickEnabled(anyBoolean()) + verify(native).setTouchpadRightClickZoneEnabled(anyBoolean()) + verify(native).setShowTouches(anyBoolean()) + verify(native).reloadPointerIcons() + verify(native).setMotionClassifierEnabled(anyBoolean()) + verify(native).setMaximumObscuringOpacityForTouch(anyFloat()) + verify(native).setStylusPointerIconEnabled(anyBoolean()) + verify(native).setKeyRepeatConfiguration(anyInt(), anyInt()) + } + + @Test + fun testPointerDisplayUpdatesWhenDisplayViewportsChanged() { + val displayId = 123 + whenever(wmCallbacks.pointerDisplayId).thenReturn(displayId) + val viewports = listOf<DisplayViewport>() + localService.setDisplayViewports(viewports) + verify(native).setDisplayViewports(any(Array<DisplayViewport>::class.java)) + verify(native).setPointerDisplayId(displayId) + + val x = 42f + val y = 314f + service.onPointerDisplayIdChanged(displayId, x, y) + testLooper.dispatchNext() + verify(wmCallbacks).notifyPointerDisplayIdChanged(displayId, x, y) + } + + @Test + fun testSetVirtualMousePointerDisplayId() { + // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked + // until the native callback happens. + var countDownLatch = CountDownLatch(1) + val overrideDisplayId = 123 + Thread { + assertTrue("Setting virtual pointer display should succeed", + localService.setVirtualMousePointerDisplayId(overrideDisplayId)) + countDownLatch.countDown() + }.start() + assertFalse("Setting virtual pointer display should block", + countDownLatch.await(100, TimeUnit.MILLISECONDS)) + + val x = 42f + val y = 314f + service.onPointerDisplayIdChanged(overrideDisplayId, x, y) + testLooper.dispatchNext() + verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, x, y) + assertTrue("Native callback unblocks calling thread", + countDownLatch.await(100, TimeUnit.MILLISECONDS)) + verify(native).setPointerDisplayId(overrideDisplayId) + + // Ensure that setting the same override again succeeds immediately. + assertTrue("Setting the same virtual mouse pointer displayId again should succeed", + localService.setVirtualMousePointerDisplayId(overrideDisplayId)) + + // Ensure that we did not query WM for the pointerDisplayId when setting the override + verify(wmCallbacks, never()).pointerDisplayId + + // Unset the virtual mouse pointer displayId, and ensure that we query WM for the new + // pointer displayId and the calling thread is blocked until the native callback happens. + countDownLatch = CountDownLatch(1) + val pointerDisplayId = 42 + `when`(wmCallbacks.pointerDisplayId).thenReturn(pointerDisplayId) + Thread { + assertTrue("Unsetting virtual mouse pointer displayId should succeed", + localService.setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY)) + countDownLatch.countDown() + }.start() + assertFalse("Unsetting virtual mouse pointer displayId should block", + countDownLatch.await(100, TimeUnit.MILLISECONDS)) + + service.onPointerDisplayIdChanged(pointerDisplayId, x, y) + testLooper.dispatchNext() + verify(wmCallbacks).notifyPointerDisplayIdChanged(pointerDisplayId, x, y) + assertTrue("Native callback unblocks calling thread", + countDownLatch.await(100, TimeUnit.MILLISECONDS)) + verify(native).setPointerDisplayId(pointerDisplayId) + } + + @Test + fun testSetVirtualMousePointerDisplayId_unsuccessfulUpdate() { + // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked + // until the native callback happens. + val countDownLatch = CountDownLatch(1) + val overrideDisplayId = 123 + Thread { + assertFalse("Setting virtual pointer display should be unsuccessful", + localService.setVirtualMousePointerDisplayId(overrideDisplayId)) + countDownLatch.countDown() + }.start() + assertFalse("Setting virtual pointer display should block", + countDownLatch.await(100, TimeUnit.MILLISECONDS)) + + val x = 42f + val y = 314f + // Assume the native callback updates the pointerDisplayId to the incorrect value. + service.onPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y) + testLooper.dispatchNext() + verify(wmCallbacks).notifyPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y) + assertTrue("Native callback unblocks calling thread", + countDownLatch.await(100, TimeUnit.MILLISECONDS)) + verify(native).setPointerDisplayId(overrideDisplayId) + } + + @Test + fun testSetVirtualMousePointerDisplayId_competingRequests() { + val firstRequestSyncLatch = CountDownLatch(1) + doAnswer { + firstRequestSyncLatch.countDown() + }.`when`(native).setPointerDisplayId(anyInt()) + + val firstRequestLatch = CountDownLatch(1) + val firstOverride = 123 + Thread { + assertFalse("Setting virtual pointer display from thread 1 should be unsuccessful", + localService.setVirtualMousePointerDisplayId(firstOverride)) + firstRequestLatch.countDown() + }.start() + assertFalse("Setting virtual pointer display should block", + firstRequestLatch.await(100, TimeUnit.MILLISECONDS)) + + assertTrue("Wait for first thread's request should succeed", + firstRequestSyncLatch.await(100, TimeUnit.MILLISECONDS)) + + val secondRequestLatch = CountDownLatch(1) + val secondOverride = 42 + Thread { + assertTrue("Setting virtual mouse pointer from thread 2 should be successful", + localService.setVirtualMousePointerDisplayId(secondOverride)) + secondRequestLatch.countDown() + }.start() + assertFalse("Setting virtual mouse pointer should block", + secondRequestLatch.await(100, TimeUnit.MILLISECONDS)) + + val x = 42f + val y = 314f + // Assume the native callback updates directly to the second request. + service.onPointerDisplayIdChanged(secondOverride, x, y) + testLooper.dispatchNext() + verify(wmCallbacks).notifyPointerDisplayIdChanged(secondOverride, x, y) + assertTrue("Native callback unblocks first thread", + firstRequestLatch.await(100, TimeUnit.MILLISECONDS)) + assertTrue("Native callback unblocks second thread", + secondRequestLatch.await(100, TimeUnit.MILLISECONDS)) + verify(native, times(2)).setPointerDisplayId(anyInt()) + } + + @Test + fun onDisplayRemoved_resetAllAdditionalInputProperties() { + setVirtualMousePointerDisplayIdAndVerify(10) + + localService.setPointerIconVisible(false, 10) + verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) + localService.setPointerAcceleration(5f, 10) + verify(native).setPointerAcceleration(eq(5f)) + + service.onDisplayRemoved(10) + verify(native).displayRemoved(eq(10)) + verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED)) + verify(native).setPointerAcceleration( + eq(IInputConstants.DEFAULT_POINTER_ACCELERATION.toFloat())) + verifyNoMoreInteractions(native) + + // This call should not block because the virtual mouse pointer override was never removed. + localService.setVirtualMousePointerDisplayId(10) + + verify(native).setPointerDisplayId(eq(10)) + verifyNoMoreInteractions(native) + } + + @Test + fun updateAdditionalInputPropertiesForOverrideDisplay() { + setVirtualMousePointerDisplayIdAndVerify(10) + + localService.setPointerIconVisible(false, 10) + verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) + localService.setPointerAcceleration(5f, 10) + verify(native).setPointerAcceleration(eq(5f)) + + localService.setPointerIconVisible(true, 10) + verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED)) + localService.setPointerAcceleration(1f, 10) + verify(native).setPointerAcceleration(eq(1f)) + + // Verify that setting properties on a different display is not propagated until the + // pointer is moved to that display. + localService.setPointerIconVisible(false, 20) + localService.setPointerAcceleration(6f, 20) + verifyNoMoreInteractions(native) + + clearInvocations(native) + setVirtualMousePointerDisplayIdAndVerify(20) + + verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) + verify(native).setPointerAcceleration(eq(6f)) + } + + @Test + fun setAdditionalInputPropertiesBeforeOverride() { + localService.setPointerIconVisible(false, 10) + localService.setPointerAcceleration(5f, 10) + + verifyNoMoreInteractions(native) + + setVirtualMousePointerDisplayIdAndVerify(10) + + verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) + verify(native).setPointerAcceleration(eq(5f)) + } + + @Test + fun setDeviceTypeAssociation_setsDeviceTypeAssociation() { + val inputPort = "inputPort" + val type = "type" + + localService.setTypeAssociation(inputPort, type) + + assertThat(service.getDeviceTypeAssociations()).asList().containsExactly(inputPort, type) + .inOrder() + } + + @Test + fun setAndUnsetDeviceTypeAssociation_deviceTypeAssociationIsMissing() { + val inputPort = "inputPort" + val type = "type" + + localService.setTypeAssociation(inputPort, type) + localService.unsetTypeAssociation(inputPort) + + assertTrue(service.getDeviceTypeAssociations().isEmpty()) + } + + @Test + fun testAddAndRemoveVirtualmKeyboardLayoutAssociation() { + val inputPort = "input port" + val languageTag = "language" + val layoutType = "layoutType" + localService.addKeyboardLayoutAssociation(inputPort, languageTag, layoutType) + verify(native).changeKeyboardLayoutAssociation() + + localService.removeKeyboardLayoutAssociation(inputPort) + verify(native, times(2)).changeKeyboardLayoutAssociation() + } + + private fun setVirtualMousePointerDisplayIdAndVerify(overrideDisplayId: Int) { + val thread = Thread { localService.setVirtualMousePointerDisplayId(overrideDisplayId) } + thread.start() + + // Allow some time for the set override call to park while waiting for the native callback. + Thread.sleep(100 /*millis*/) + verify(native).setPointerDisplayId(overrideDisplayId) + + service.onPointerDisplayIdChanged(overrideDisplayId, 0f, 0f) + testLooper.dispatchNext() + verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, 0f, 0f) + thread.join(100 /*millis*/) + } +} + +private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) diff --git a/tests/Input/src/com/android/server/input/KeyRemapperTests.kt b/tests/Input/src/com/android/server/input/KeyRemapperTests.kt new file mode 100644 index 000000000000..f74fd723d540 --- /dev/null +++ b/tests/Input/src/com/android/server/input/KeyRemapperTests.kt @@ -0,0 +1,193 @@ +/* + * 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.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.input.IInputManager +import android.hardware.input.InputManager +import android.hardware.input.InputManagerGlobal +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.provider.Settings +import android.view.InputDevice +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream + +private fun createKeyboard(deviceId: Int): InputDevice = + InputDevice.Builder() + .setId(deviceId) + .setName("Device $deviceId") + .setDescriptor("descriptor $deviceId") + .setSources(InputDevice.SOURCE_KEYBOARD) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC) + .setExternal(true) + .build() + +/** + * Tests for {@link KeyRemapper}. + * + * Build/Install/Run: + * atest InputTests:KeyRemapperTests + */ +@Presubmit +class KeyRemapperTests { + + companion object { + const val DEVICE_ID = 1 + val REMAPPABLE_KEYS = intArrayOf( + KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_RIGHT, + KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_META_RIGHT, + KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_RIGHT, + KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_CAPS_LOCK + ) + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + @Mock + private lateinit var iInputManager: IInputManager + @Mock + private lateinit var native: NativeInputManagerService + private lateinit var mKeyRemapper: KeyRemapper + private lateinit var context: Context + private lateinit var dataStore: PersistentDataStore + private lateinit var testLooper: TestLooper + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + + @Before + fun setup() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { + override fun openRead(): InputStream? { + throw FileNotFoundException() + } + + override fun startWrite(): FileOutputStream? { + throw IOException() + } + + override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} + }) + testLooper = TestLooper() + mKeyRemapper = KeyRemapper( + context, + native, + dataStore, + testLooper.looper + ) + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + val inputManager = InputManager(context) + Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + @Test + fun testKeyRemapping_whenRemappingEnabled() { + ModifierRemappingFlag(true).use { + val keyboard = createKeyboard(DEVICE_ID) + Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard) + + for (i in REMAPPABLE_KEYS.indices) { + val fromKeyCode = REMAPPABLE_KEYS[i] + val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size] + mKeyRemapper.remapKey(fromKeyCode, toKeyCode) + testLooper.dispatchNext() + } + + val remapping = mKeyRemapper.keyRemapping + val expectedSize = REMAPPABLE_KEYS.size + assertEquals("Remapping size should be $expectedSize", expectedSize, remapping.size) + + for (i in REMAPPABLE_KEYS.indices) { + val fromKeyCode = REMAPPABLE_KEYS[i] + val toKeyCode = REMAPPABLE_KEYS[(i + 1) % REMAPPABLE_KEYS.size] + assertEquals( + "Remapping should include mapping from $fromKeyCode to $toKeyCode", + toKeyCode, + remapping.getOrDefault(fromKeyCode, -1) + ) + } + + mKeyRemapper.clearAllKeyRemappings() + testLooper.dispatchNext() + + assertEquals( + "Remapping size should be 0 after clearAllModifierKeyRemappings", + 0, + mKeyRemapper.keyRemapping.size + ) + } + } + + @Test + fun testKeyRemapping_whenRemappingDisabled() { + ModifierRemappingFlag(false).use { + val keyboard = createKeyboard(DEVICE_ID) + Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard) + + mKeyRemapper.remapKey(REMAPPABLE_KEYS[0], REMAPPABLE_KEYS[1]) + testLooper.dispatchAll() + + val remapping = mKeyRemapper.keyRemapping + assertEquals( + "Remapping should not be done if modifier key remapping is disabled", + 0, + remapping.size + ) + } + } + + private inner class ModifierRemappingFlag constructor(enabled: Boolean) : AutoCloseable { + init { + Settings.Global.putString( + context.contentResolver, + "settings_new_keyboard_modifier_key", enabled.toString() + ) + } + + override fun close() { + Settings.Global.putString( + context.contentResolver, + "settings_new_keyboard_modifier_key", + "" + ) + } + } +}
\ No newline at end of file diff --git a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt new file mode 100644 index 000000000000..59aa96c46336 --- /dev/null +++ b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -0,0 +1,849 @@ +/* + * 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.server.input + +import android.animation.ValueAnimator +import android.content.Context +import android.content.ContextWrapper +import android.graphics.Color +import android.hardware.input.IInputManager +import android.hardware.input.IKeyboardBacklightListener +import android.hardware.input.IKeyboardBacklightState +import android.hardware.input.InputManager +import android.hardware.input.InputManagerGlobal +import android.hardware.lights.Light +import android.os.UEventObserver +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.view.InputDevice +import androidx.test.annotation.UiThreadTest +import androidx.test.core.app.ApplicationProvider +import com.android.server.input.KeyboardBacklightController.DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL +import com.android.server.input.KeyboardBacklightController.MAX_BRIGHTNESS_CHANGE_STEPS +import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.spy +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream + +private fun createKeyboard(deviceId: Int): InputDevice = + InputDevice.Builder() + .setId(deviceId) + .setName("Device $deviceId") + .setDescriptor("descriptor $deviceId") + .setSources(InputDevice.SOURCE_KEYBOARD) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC) + .setExternal(true) + .build() + +private fun createLight(lightId: Int, lightType: Int): Light = + createLight( + lightId, + lightType, + null + ) + +private fun createLight(lightId: Int, lightType: Int, suggestedBrightnessLevels: IntArray?): Light = + Light( + lightId, + "Light $lightId", + 1, + lightType, + Light.LIGHT_CAPABILITY_BRIGHTNESS, + suggestedBrightnessLevels + ) +/** + * Tests for {@link KeyboardBacklightController}. + * + * Build/Install/Run: + * atest InputTests:KeyboardBacklightControllerTests + */ +@Presubmit +class KeyboardBacklightControllerTests { + companion object { + const val DEVICE_ID = 1 + const val LIGHT_ID = 2 + const val SECOND_LIGHT_ID = 3 + const val MAX_BRIGHTNESS = 255 + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + @Mock + private lateinit var iInputManager: IInputManager + @Mock + private lateinit var native: NativeInputManagerService + @Mock + private lateinit var uEventManager: UEventManager + private lateinit var keyboardBacklightController: KeyboardBacklightController + private lateinit var context: Context + private lateinit var dataStore: PersistentDataStore + private lateinit var testLooper: TestLooper + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + private var lightColorMap: HashMap<Int, Int> = HashMap() + private var lastBacklightState: KeyboardBacklightState? = null + private var sysfsNodeChanges = 0 + private var lastAnimationValues = IntArray(2) + + @Before + fun setup() { + context = spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { + override fun openRead(): InputStream? { + throw FileNotFoundException() + } + + override fun startWrite(): FileOutputStream? { + throw IOException() + } + + override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} + }) + testLooper = TestLooper() + keyboardBacklightController = KeyboardBacklightController(context, native, dataStore, + testLooper.looper, FakeAnimatorFactory(), uEventManager) + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + val inputManager = InputManager(context) + `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) + `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) + `when`(native.setLightColor(anyInt(), anyInt(), anyInt())).then { + val args = it.arguments + lightColorMap.put(args[1] as Int, args[2] as Int) + } + `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer { + val args = it.arguments + lightColorMap.getOrDefault(args[1] as Int, 0) + } + lightColorMap.clear() + `when`(native.sysfsNodeChanged(any())).then { + sysfsNodeChanges++ + } + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + @Test + fun testKeyboardBacklightIncrementDecrement() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL) + } + } + + @Test + fun testKeyboardWithoutBacklight() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { + val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) + val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) + } + } + + @Test + fun testKeyboardWithMultipleLight() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn( + listOf( + keyboardBacklight, + keyboardInputLight + ) + ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) + assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) + assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) + } + } + + @Test + fun testRestoreBacklightOnInputDeviceAdded() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + + for (level in 1 until DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size) { + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1 + ) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the " + + "data store", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) + } + } + } + + @Test + fun testRestoreBacklightOnInputDeviceChanged() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertTrue( + "Keyboard backlight should not be changed until its added", + lightColorMap.isEmpty() + ) + + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } + } + + @Test + fun testKeyboardBacklight_registerUnregisterListener() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + val maxLevel = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size - 1 + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + // Register backlight listener + val listener = KeyboardBacklightListener() + keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) + + lastBacklightState = null + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() + + assertEquals( + "Backlight state device Id should be $DEVICE_ID", + DEVICE_ID, + lastBacklightState!!.deviceId + ) + assertEquals( + "Backlight state brightnessLevel should be 1", + 1, + lastBacklightState!!.brightnessLevel + ) + assertEquals( + "Backlight state maxBrightnessLevel should be $maxLevel", + maxLevel, + lastBacklightState!!.maxBrightnessLevel + ) + assertEquals( + "Backlight state isTriggeredByKeyPress should be true", + true, + lastBacklightState!!.isTriggeredByKeyPress + ) + + // Unregister listener + keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) + + lastBacklightState = null + incrementKeyboardBacklight(DEVICE_ID) + + assertNull("Listener should not receive any updates", lastBacklightState) + } + } + + @Test + fun testKeyboardBacklight_userActivity() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + + testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000) + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be turned off after inactivity", + 0, + lightColorMap[LIGHT_ID] + ) + } + } + + @Test + fun testKeyboardBacklight_displayOnOff() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + MAX_BRIGHTNESS + ) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data " + + "store when display turned on", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + + keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be turned off after display is turned off", + 0, + lightColorMap[LIGHT_ID] + ) + } + } + + @Test + fun testKeyboardBacklightSysfsNodeAdded_AfterInputDeviceAdded() { + var counter = sysfsNodeChanges + keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( + "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::no_backlight\u0000" + )) + assertEquals( + "Should not reload sysfs node if UEvent path doesn't contain kbd_backlight", + counter, + sysfsNodeChanges + ) + + keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( + "ACTION=add\u0000SUBSYSTEM=power\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000" + )) + assertEquals( + "Should not reload sysfs node if UEvent doesn't belong to subsystem LED", + counter, + sysfsNodeChanges + ) + + keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( + "ACTION=remove\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000" + )) + assertEquals( + "Should not reload sysfs node if UEvent doesn't have ACTION(add)", + counter, + sysfsNodeChanges + ) + + keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( + "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/pqr/abc::kbd_backlight\u0000" + )) + assertEquals( + "Should not reload sysfs node if UEvent path doesn't belong to leds/ directory", + counter, + sysfsNodeChanges + ) + + keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( + "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::kbd_backlight\u0000" + )) + assertEquals( + "Should reload sysfs node if a valid Keyboard backlight LED UEvent occurs", + ++counter, + sysfsNodeChanges + ) + + keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( + "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc:kbd_backlight:red\u0000" + )) + assertEquals( + "Should reload sysfs node if a valid Keyboard backlight LED UEvent occurs", + ++counter, + sysfsNodeChanges + ) + } + + @Test + @UiThreadTest + fun testKeyboardBacklightAnimation_onChangeLevels() { + KeyboardBacklightFlags( + animationEnabled = true, + customLevelsEnabled = false, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Should start animation from level 0", + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[0], + lastAnimationValues[0] + ) + assertEquals( + "Should start animation to level 1", + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], + lastAnimationValues[1] + ) + } + } + + @Test + fun testKeyboardBacklightPreferredLevels() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = true, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val suggestedLevels = intArrayOf(0, 22, 63, 135, 196, 255) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, + suggestedLevels) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, + suggestedLevels) + } + } + + @Test + fun testKeyboardBacklightPreferredLevels_moreThanMax_shouldUseDefault() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = true, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val suggestedLevels = IntArray(MAX_BRIGHTNESS_CHANGE_STEPS + 1) { 10 * (it + 1) } + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, + suggestedLevels) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL) + } + } + + @Test + fun testKeyboardBacklightPreferredLevels_mustHaveZeroAndMaxBrightnessAsBounds() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = true, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val suggestedLevels = intArrayOf(22, 63, 135, 196) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, + suggestedLevels) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + // Framework will add the lowest and maximum levels if not provided via config + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, + intArrayOf(0, 22, 63, 135, 196, 255)) + } + } + + @Test + fun testKeyboardBacklightPreferredLevels_dropsOutOfBoundsLevels() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = true, + ambientControlEnabled = false + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val suggestedLevels = intArrayOf(22, 63, 135, 400, 196, 1000) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, + suggestedLevels) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + // Framework will drop out of bound levels in the config + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, + intArrayOf(0, 22, 63, 135, 196, 255)) + } + } + + @Test + fun testAmbientBacklightControl_doesntRestoreBacklightLevel() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1] + ) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchNext() + assertNull( + "Keyboard backlight level should not be restored to the saved level", + lightColorMap[LIGHT_ID] + ) + } + } + + @Test + fun testAmbientBacklightControl_doesntBackupBacklightLevel() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + incrementKeyboardBacklight(DEVICE_ID) + assertFalse( + "Light value should not be backed up if ambient control is enabled", + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, LIGHT_ID + ).isPresent + ) + } + } + + @Test + fun testAmbientBacklightControl_incrementLevel_afterAmbientChange() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + sendAmbientBacklightValue(1) + assertEquals( + "Light value should be changed to ambient provided value", + Color.argb(1, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + + incrementKeyboardBacklight(DEVICE_ID) + + assertEquals( + "Light value for level after increment post Ambient change is mismatched", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } + } + + @Test + fun testAmbientBacklightControl_decrementLevel_afterAmbientChange() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + sendAmbientBacklightValue(254) + assertEquals( + "Light value should be changed to ambient provided value", + Color.argb(254, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + + decrementKeyboardBacklight(DEVICE_ID) + + val numLevels = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size + assertEquals( + "Light value for level after decrement post Ambient change is mismatched", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[numLevels - 2], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } + } + + @Test + fun testAmbientBacklightControl_ambientChanges_afterManualChange() { + KeyboardBacklightFlags( + animationEnabled = false, + customLevelsEnabled = false, + ambientControlEnabled = true + ).use { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + incrementKeyboardBacklight(DEVICE_ID) + assertEquals( + "Light value should be changed to the first level", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + + sendAmbientBacklightValue(100) + assertNotEquals( + "Light value should not change based on ambient changes after manual changes", + Color.argb(100, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } + } + + private fun assertIncrementDecrementForLevels( + device: InputDevice, + light: Light, + expectedLevels: IntArray + ) { + val deviceId = device.id + val lightId = light.id + for (level in 1 until expectedLevels.size) { + incrementKeyboardBacklight(deviceId) + assertEquals( + "Light value for level $level mismatched", + Color.argb(expectedLevels[level], 0, 0, 0), + lightColorMap[lightId] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + expectedLevels[level], + dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt + ) + } + + // Increment above max level + incrementKeyboardBacklight(deviceId) + assertEquals( + "Light value for max level mismatched", + Color.argb(MAX_BRIGHTNESS, 0, 0, 0), + lightColorMap[lightId] + ) + assertEquals( + "Light value for max level must be correctly stored in the datastore", + MAX_BRIGHTNESS, + dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt + ) + + for (level in expectedLevels.size - 2 downTo 0) { + decrementKeyboardBacklight(deviceId) + assertEquals( + "Light value for level $level mismatched", + Color.argb(expectedLevels[level], 0, 0, 0), + lightColorMap[lightId] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + expectedLevels[level], + dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt + ) + } + + // Decrement below min level + decrementKeyboardBacklight(deviceId) + assertEquals( + "Light value for min level mismatched", + Color.argb(0, 0, 0, 0), + lightColorMap[lightId] + ) + assertEquals( + "Light value for min level must be correctly stored in the datastore", + 0, + dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt + ) + } + + inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() { + override fun onBrightnessChanged( + deviceId: Int, + state: IKeyboardBacklightState, + isTriggeredByKeyPress: Boolean + ) { + lastBacklightState = KeyboardBacklightState( + deviceId, + state.brightnessLevel, + state.maxBrightnessLevel, + isTriggeredByKeyPress + ) + } + } + + private fun incrementKeyboardBacklight(deviceId: Int) { + keyboardBacklightController.incrementKeyboardBacklight(deviceId) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchAll() + } + + private fun decrementKeyboardBacklight(deviceId: Int) { + keyboardBacklightController.decrementKeyboardBacklight(deviceId) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchAll() + } + + private fun sendAmbientBacklightValue(brightnessValue: Int) { + keyboardBacklightController.handleAmbientLightValueChanged(brightnessValue) + keyboardBacklightController.notifyUserActivity() + testLooper.dispatchAll() + } + + class KeyboardBacklightState( + val deviceId: Int, + val brightnessLevel: Int, + val maxBrightnessLevel: Int, + val isTriggeredByKeyPress: Boolean + ) + + private inner class KeyboardBacklightFlags constructor( + animationEnabled: Boolean, + customLevelsEnabled: Boolean, + ambientControlEnabled: Boolean + ) : AutoCloseable { + init { + InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(animationEnabled) + InputFeatureFlagProvider.setKeyboardBacklightCustomLevelsEnabled(customLevelsEnabled) + InputFeatureFlagProvider + .setAmbientKeyboardBacklightControlEnabled(ambientControlEnabled) + } + + override fun close() { + InputFeatureFlagProvider.clearOverrides() + } + } + + private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory { + override fun makeIntAnimator(from: Int, to: Int): ValueAnimator { + lastAnimationValues[0] = from + lastAnimationValues[1] = to + return ValueAnimator.ofInt(from, to) + } + } +} diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt new file mode 100644 index 000000000000..b64775103ab2 --- /dev/null +++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -0,0 +1,912 @@ +/* + * 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.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.hardware.input.IInputManager +import android.hardware.input.InputManager +import android.hardware.input.InputManagerGlobal +import android.hardware.input.KeyboardLayout +import android.icu.util.ULocale +import android.os.Bundle +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.provider.Settings +import android.view.InputDevice +import android.view.inputmethod.InputMethodInfo +import android.view.inputmethod.InputMethodSubtype +import androidx.test.core.R +import androidx.test.core.app.ApplicationProvider +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream + +private fun createKeyboard( + deviceId: Int, + vendorId: Int, + productId: Int, + languageTag: String, + layoutType: String +): InputDevice = + InputDevice.Builder() + .setId(deviceId) + .setName("Device $deviceId") + .setDescriptor("descriptor $deviceId") + .setSources(InputDevice.SOURCE_KEYBOARD) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC) + .setExternal(true) + .setVendorId(vendorId) + .setProductId(productId) + .setKeyboardLanguageTag(languageTag) + .setKeyboardLayoutType(layoutType) + .build() + +/** + * Tests for {@link Default UI} and {@link New UI}. + * + * Build/Install/Run: + * atest InputTests:KeyboardLayoutManagerTests + */ +@Presubmit +class KeyboardLayoutManagerTests { + companion object { + const val DEVICE_ID = 1 + const val VENDOR_SPECIFIC_DEVICE_ID = 2 + const val ENGLISH_DVORAK_DEVICE_ID = 3 + const val ENGLISH_QWERTY_DEVICE_ID = 4 + const val DEFAULT_VENDOR_ID = 123 + const val DEFAULT_PRODUCT_ID = 456 + const val USER_ID = 4 + const val IME_ID = "ime_id" + const val PACKAGE_NAME = "KeyboardLayoutManagerTests" + const val RECEIVER_NAME = "DummyReceiver" + private const val ENGLISH_US_LAYOUT_NAME = "keyboard_layout_english_us" + private const val ENGLISH_UK_LAYOUT_NAME = "keyboard_layout_english_uk" + private const val VENDOR_SPECIFIC_LAYOUT_NAME = "keyboard_layout_vendorId:1,productId:1" + } + + private val ENGLISH_US_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_US_LAYOUT_NAME) + private val ENGLISH_UK_LAYOUT_DESCRIPTOR = createLayoutDescriptor(ENGLISH_UK_LAYOUT_NAME) + private val VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR = + createLayoutDescriptor(VENDOR_SPECIFIC_LAYOUT_NAME) + + @get:Rule + val rule = MockitoJUnit.rule()!! + + @Mock + private lateinit var iInputManager: IInputManager + + @Mock + private lateinit var native: NativeInputManagerService + + @Mock + private lateinit var packageManager: PackageManager + private lateinit var keyboardLayoutManager: KeyboardLayoutManager + + private lateinit var imeInfo: InputMethodInfo + private var nextImeSubtypeId = 0 + private lateinit var context: Context + private lateinit var dataStore: PersistentDataStore + private lateinit var testLooper: TestLooper + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + + // Devices + private lateinit var keyboardDevice: InputDevice + private lateinit var vendorSpecificKeyboardDevice: InputDevice + private lateinit var englishDvorakKeyboardDevice: InputDevice + private lateinit var englishQwertyKeyboardDevice: InputDevice + + @Before + fun setup() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { + override fun openRead(): InputStream? { + throw FileNotFoundException() + } + + override fun startWrite(): FileOutputStream? { + throw IOException() + } + + override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} + }) + testLooper = TestLooper() + keyboardLayoutManager = KeyboardLayoutManager(context, native, dataStore, testLooper.looper) + setupInputDevices() + setupBroadcastReceiver() + setupIme() + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + private fun setupInputDevices() { + val inputManager = InputManager(context) + Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + keyboardDevice = createKeyboard(DEVICE_ID, DEFAULT_VENDOR_ID, DEFAULT_PRODUCT_ID, "", "") + vendorSpecificKeyboardDevice = createKeyboard(VENDOR_SPECIFIC_DEVICE_ID, 1, 1, "", "") + englishDvorakKeyboardDevice = createKeyboard(ENGLISH_DVORAK_DEVICE_ID, DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, "en", "dvorak") + englishQwertyKeyboardDevice = createKeyboard(ENGLISH_QWERTY_DEVICE_ID, DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, "en", "qwerty") + Mockito.`when`(iInputManager.inputDeviceIds) + .thenReturn(intArrayOf( + DEVICE_ID, + VENDOR_SPECIFIC_DEVICE_ID, + ENGLISH_DVORAK_DEVICE_ID, + ENGLISH_QWERTY_DEVICE_ID + )) + Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) + Mockito.`when`(iInputManager.getInputDevice(VENDOR_SPECIFIC_DEVICE_ID)) + .thenReturn(vendorSpecificKeyboardDevice) + Mockito.`when`(iInputManager.getInputDevice(ENGLISH_DVORAK_DEVICE_ID)) + .thenReturn(englishDvorakKeyboardDevice) + Mockito.`when`(iInputManager.getInputDevice(ENGLISH_QWERTY_DEVICE_ID)) + .thenReturn(englishQwertyKeyboardDevice) + } + + private fun setupBroadcastReceiver() { + Mockito.`when`(context.packageManager).thenReturn(packageManager) + + val info = createMockReceiver() + Mockito.`when`(packageManager.queryBroadcastReceiversAsUser(Mockito.any(), Mockito.anyInt(), + Mockito.anyInt())).thenReturn(listOf(info)) + Mockito.`when`(packageManager.getReceiverInfo(Mockito.any(), Mockito.anyInt())) + .thenReturn(info.activityInfo) + + val resources = context.resources + Mockito.`when`( + packageManager.getResourcesForApplication( + Mockito.any( + ApplicationInfo::class.java + ) + ) + ).thenReturn(resources) + } + + private fun setupIme() { + imeInfo = InputMethodInfo(PACKAGE_NAME, RECEIVER_NAME, "", "", 0) + } + + @Test + fun testDefaultUi_getKeyboardLayouts() { + NewSettingsApiFlag(false).use { + val keyboardLayouts = keyboardLayoutManager.keyboardLayouts + assertNotEquals( + "Default UI: Keyboard layout API should not return empty array", + 0, + keyboardLayouts.size + ) + assertTrue( + "Default UI: Keyboard layout API should provide English(US) layout", + hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + } + } + + @Test + fun testNewUi_getKeyboardLayouts() { + NewSettingsApiFlag(true).use { + val keyboardLayouts = keyboardLayoutManager.keyboardLayouts + assertNotEquals( + "New UI: Keyboard layout API should not return empty array", + 0, + keyboardLayouts.size + ) + assertTrue( + "New UI: Keyboard layout API should provide English(US) layout", + hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + } + } + + @Test + fun testDefaultUi_getKeyboardLayoutsForInputDevice() { + NewSettingsApiFlag(false).use { + val keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutsForInputDevice(keyboardDevice.identifier) + assertNotEquals( + "Default UI: getKeyboardLayoutsForInputDevice API should not return empty array", + 0, + keyboardLayouts.size + ) + assertTrue( + "Default UI: getKeyboardLayoutsForInputDevice API should provide English(US) " + + "layout", + hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + + val vendorSpecificKeyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutsForInputDevice( + vendorSpecificKeyboardDevice.identifier + ) + assertEquals( + "Default UI: getKeyboardLayoutsForInputDevice API should return only vendor " + + "specific layout", + 1, + vendorSpecificKeyboardLayouts.size + ) + assertEquals( + "Default UI: getKeyboardLayoutsForInputDevice API should return vendor specific " + + "layout", + VENDOR_SPECIFIC_LAYOUT_DESCRIPTOR, + vendorSpecificKeyboardLayouts[0].descriptor + ) + } + } + + @Test + fun testNewUi_getKeyboardLayoutsForInputDevice() { + NewSettingsApiFlag(true).use { + val keyboardLayouts = keyboardLayoutManager.keyboardLayouts + assertNotEquals( + "New UI: getKeyboardLayoutsForInputDevice API should not return empty array", + 0, + keyboardLayouts.size + ) + assertTrue( + "New UI: getKeyboardLayoutsForInputDevice API should provide English(US) " + + "layout", + hasLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + } + } + + @Test + fun testDefaultUi_getSetCurrentKeyboardLayoutForInputDevice() { + NewSettingsApiFlag(false).use { + assertNull( + "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null if " + + "nothing was set", + keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier + ) + ) + + keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier, + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + val keyboardLayout = + keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier + ) + assertEquals( + "Default UI: getCurrentKeyboardLayoutForInputDevice API should return the set " + + "layout", + ENGLISH_US_LAYOUT_DESCRIPTOR, + keyboardLayout + ) + } + } + + @Test + fun testNewUi_getSetCurrentKeyboardLayoutForInputDevice() { + NewSettingsApiFlag(true).use { + keyboardLayoutManager.setCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier, + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + assertNull( + "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null " + + "even after setCurrentKeyboardLayoutForInputDevice", + keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier + ) + ) + } + } + + @Test + fun testDefaultUi_getEnabledKeyboardLayoutsForInputDevice() { + NewSettingsApiFlag(false).use { + keyboardLayoutManager.addKeyboardLayoutForInputDevice( + keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR + ) + + val keyboardLayouts = + keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice( + keyboardDevice.identifier + ) + assertEquals( + "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return added " + + "layout", + 1, + keyboardLayouts.size + ) + assertEquals( + "Default UI: getEnabledKeyboardLayoutsForInputDevice API should return " + + "English(US) layout", + ENGLISH_US_LAYOUT_DESCRIPTOR, + keyboardLayouts[0] + ) + assertEquals( + "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " + + "English(US) layout (Auto select the first enabled layout)", + ENGLISH_US_LAYOUT_DESCRIPTOR, + keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier + ) + ) + + keyboardLayoutManager.removeKeyboardLayoutForInputDevice( + keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR + ) + assertEquals( + "Default UI: getKeyboardLayoutsForInputDevice API should return 0 layouts", + 0, + keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice( + keyboardDevice.identifier + ).size + ) + assertNull( + "Default UI: getCurrentKeyboardLayoutForInputDevice API should return null after " + + "the enabled layout is removed", + keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier + ) + ) + } + } + + @Test + fun testNewUi_getEnabledKeyboardLayoutsForInputDevice() { + NewSettingsApiFlag(true).use { + keyboardLayoutManager.addKeyboardLayoutForInputDevice( + keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR + ) + + assertEquals( + "New UI: getEnabledKeyboardLayoutsForInputDevice API should return always return " + + "an empty array", + 0, + keyboardLayoutManager.getEnabledKeyboardLayoutsForInputDevice( + keyboardDevice.identifier + ).size + ) + assertNull( + "New UI: getCurrentKeyboardLayoutForInputDevice API should always return null", + keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier + ) + ) + } + } + + @Test + fun testDefaultUi_switchKeyboardLayout() { + NewSettingsApiFlag(false).use { + keyboardLayoutManager.addKeyboardLayoutForInputDevice( + keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR + ) + keyboardLayoutManager.addKeyboardLayoutForInputDevice( + keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + assertEquals( + "Default UI: getCurrentKeyboardLayoutForInputDevice API should return " + + "English(US) layout", + ENGLISH_US_LAYOUT_DESCRIPTOR, + keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier + ) + ) + + keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1) + + // Throws null pointer because trying to show toast using TestLooper + assertThrows(NullPointerException::class.java) { testLooper.dispatchAll() } + assertEquals("Default UI: getCurrentKeyboardLayoutForInputDevice API should return " + + "English(UK) layout", + ENGLISH_UK_LAYOUT_DESCRIPTOR, + keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier + ) + ) + } + } + + @Test + fun testNewUi_switchKeyboardLayout() { + NewSettingsApiFlag(true).use { + keyboardLayoutManager.addKeyboardLayoutForInputDevice( + keyboardDevice.identifier, ENGLISH_US_LAYOUT_DESCRIPTOR + ) + keyboardLayoutManager.addKeyboardLayoutForInputDevice( + keyboardDevice.identifier, ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + + keyboardLayoutManager.switchKeyboardLayout(DEVICE_ID, 1) + testLooper.dispatchAll() + + assertNull("New UI: getCurrentKeyboardLayoutForInputDevice API should always return " + + "null", + keyboardLayoutManager.getCurrentKeyboardLayoutForInputDevice( + keyboardDevice.identifier + ) + ) + } + } + + @Test + fun testDefaultUi_getKeyboardLayout() { + NewSettingsApiFlag(false).use { + val keyboardLayout = + keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR) + assertEquals("Default UI: getKeyboardLayout API should return correct Layout from " + + "available layouts", + ENGLISH_US_LAYOUT_DESCRIPTOR, + keyboardLayout!!.descriptor + ) + } + } + + @Test + fun testNewUi_getKeyboardLayout() { + NewSettingsApiFlag(true).use { + val keyboardLayout = + keyboardLayoutManager.getKeyboardLayout(ENGLISH_US_LAYOUT_DESCRIPTOR) + assertEquals("New UI: getKeyboardLayout API should return correct Layout from " + + "available layouts", + ENGLISH_US_LAYOUT_DESCRIPTOR, + keyboardLayout!!.descriptor + ) + } + } + + @Test + fun testDefaultUi_getSetKeyboardLayoutForInputDevice_WithImeInfo() { + NewSettingsApiFlag(false).use { + val imeSubtype = createImeSubtype() + keyboardLayoutManager.setKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, + ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + val keyboardLayout = + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype + ) + assertNull( + "Default UI: getKeyboardLayoutForInputDevice API should always return null", + keyboardLayout + ) + } + } + + @Test + fun testNewUi_getSetKeyboardLayoutForInputDevice_withImeInfo() { + NewSettingsApiFlag(true).use { + val imeSubtype = createImeSubtype() + + keyboardLayoutManager.setKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, + ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + assertEquals( + "New UI: getKeyboardLayoutForInputDevice API should return the set layout", + ENGLISH_UK_LAYOUT_DESCRIPTOR, + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype + ) + ) + + // This should replace previously set layout + keyboardLayoutManager.setKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + assertEquals( + "New UI: getKeyboardLayoutForInputDevice API should return the last set layout", + ENGLISH_US_LAYOUT_DESCRIPTOR, + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype + ) + ) + } + } + + @Test + fun testDefaultUi_getKeyboardLayoutListForInputDevice() { + NewSettingsApiFlag(false).use { + val keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtype() + ) + assertEquals("Default UI: getKeyboardLayoutListForInputDevice API should always " + + "return empty array", + 0, + keyboardLayouts.size + ) + } + } + + @Test + fun testNewUi_getKeyboardLayoutListForInputDevice() { + NewSettingsApiFlag(true).use { + // Check Layouts for "hi-Latn". It should return all 'Latn' keyboard layouts + var keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("hi-Latn") + ) + assertNotEquals( + "New UI: getKeyboardLayoutListForInputDevice API should return the list of " + + "supported layouts with matching script code", + 0, + keyboardLayouts.size + ) + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(US) layout for hi-Latn", + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(No script code) layout for hi-Latn", + containsLayout( + keyboardLayouts, + createLayoutDescriptor("keyboard_layout_english_without_script_code") + ) + ) + + // Check Layouts for "hi" which by default uses 'Deva' script. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("hi") + ) + assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return empty " + + "list if no supported layouts available", + 0, + keyboardLayouts.size + ) + + // If user manually selected some layout, always provide it in the layout list + val imeSubtype = createImeSubtypeForLanguageTag("hi") + keyboardLayoutManager.setKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + imeSubtype + ) + assertEquals("New UI: getKeyboardLayoutListForInputDevice API should return user " + + "selected layout even if the script is incompatible with IME", + 1, + keyboardLayouts.size + ) + + // Special case Japanese: UScript ignores provided script code for certain language tags + // Should manually match provided script codes and then rely on Uscript to derive + // script from language tags and match those. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("ja-Latn-JP") + ) + assertNotEquals( + "New UI: getKeyboardLayoutListForInputDevice API should return the list of " + + "supported layouts with matching script code for ja-Latn-JP", + 0, + keyboardLayouts.size + ) + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(US) layout for ja-Latn-JP", + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should return a list " + + "containing English(No script code) layout for ja-Latn-JP", + containsLayout( + keyboardLayouts, + createLayoutDescriptor("keyboard_layout_english_without_script_code") + ) + ) + + // If script code not explicitly provided for Japanese should rely on Uscript to find + // derived script code and hence no suitable layout will be found. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("ja-JP") + ) + assertEquals( + "New UI: getKeyboardLayoutListForInputDevice API should return empty list of " + + "supported layouts with matching script code for ja-JP", + 0, + keyboardLayouts.size + ) + + // If IME doesn't have a corresponding language tag, then should show all available + // layouts no matter the script code. + keyboardLayouts = + keyboardLayoutManager.getKeyboardLayoutListForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, null + ) + assertNotEquals( + "New UI: getKeyboardLayoutListForInputDevice API should return all layouts if" + + "language tag or subtype not provided", + 0, + keyboardLayouts.size + ) + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should contain Latin " + + "layouts if language tag or subtype not provided", + containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR) + ) + assertTrue("New UI: getKeyboardLayoutListForInputDevice API should contain Cyrillic " + + "layouts if language tag or subtype not provided", + containsLayout( + keyboardLayouts, + createLayoutDescriptor("keyboard_layout_russian") + ) + ) + } + } + + @Test + fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTag() { + NewSettingsApiFlag(true).use { + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("en-US"), + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("en-GB"), + ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("de"), + createLayoutDescriptor("keyboard_layout_german") + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("fr-FR"), + createLayoutDescriptor("keyboard_layout_french") + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTag("ru"), + createLayoutDescriptor("keyboard_layout_russian") + ) + assertNull( + "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " + + "layout available", + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("it") + ) + ) + assertNull( + "New UI: getDefaultKeyboardLayoutForInputDevice should return null when no " + + "layout for script code is available", + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTag("en-Deva") + ) + ) + } + } + + @Test + fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withImeLanguageTagAndLayoutType() { + NewSettingsApiFlag(true).use { + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-US", "qwerty"), + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-US", "dvorak"), + createLayoutDescriptor("keyboard_layout_english_us_dvorak") + ) + // Try to match layout type even if country doesn't match + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-GB", "dvorak"), + createLayoutDescriptor("keyboard_layout_english_us_dvorak") + ) + // Choose layout based on layout type priority, if layout type is not provided by IME + // (Qwerty > Dvorak > Extended) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-US", ""), + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("en-GB", "qwerty"), + ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("de", "qwertz"), + createLayoutDescriptor("keyboard_layout_german") + ) + // Wrong layout type should match with language if provided layout type not available + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("de", "qwerty"), + createLayoutDescriptor("keyboard_layout_german") + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("fr-FR", "azerty"), + createLayoutDescriptor("keyboard_layout_french") + ) + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("ru", "qwerty"), + createLayoutDescriptor("keyboard_layout_russian_qwerty") + ) + // If layout type is empty then prioritize KCM with empty layout type + assertCorrectLayout( + keyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("ru", ""), + createLayoutDescriptor("keyboard_layout_russian") + ) + assertNull("New UI: getDefaultKeyboardLayoutForInputDevice should return null when " + + "no layout for script code is available", + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, + createImeSubtypeForLanguageTagAndLayoutType("en-Deva-US", "") + ) + ) + } + } + + @Test + fun testNewUi_getDefaultKeyboardLayoutForInputDevice_withHwLanguageTagAndLayoutType() { + NewSettingsApiFlag(true).use { + val frenchSubtype = createImeSubtypeForLanguageTagAndLayoutType("fr", "azerty") + // Should return English dvorak even if IME current layout is French, since HW says the + // keyboard is a Dvorak keyboard + assertCorrectLayout( + englishDvorakKeyboardDevice, + frenchSubtype, + createLayoutDescriptor("keyboard_layout_english_us_dvorak") + ) + + // Back to back changing HW keyboards with same product and vendor ID but different + // language and layout type should configure the layouts correctly. + assertCorrectLayout( + englishQwertyKeyboardDevice, + frenchSubtype, + createLayoutDescriptor("keyboard_layout_english_us") + ) + + // Fallback to IME information if the HW provided layout script is incompatible with the + // provided IME subtype + assertCorrectLayout( + englishDvorakKeyboardDevice, + createImeSubtypeForLanguageTagAndLayoutType("ru", ""), + createLayoutDescriptor("keyboard_layout_russian") + ) + } + } + + private fun assertCorrectLayout( + device: InputDevice, + imeSubtype: InputMethodSubtype, + expectedLayout: String + ) { + assertEquals( + "New UI: getDefaultKeyboardLayoutForInputDevice should return $expectedLayout", + expectedLayout, + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + device.identifier, USER_ID, imeInfo, imeSubtype + ) + ) + } + + private fun createImeSubtype(): InputMethodSubtype = + InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++).build() + + private fun createImeSubtypeForLanguageTag(languageTag: String): InputMethodSubtype = + InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++) + .setLanguageTag(languageTag).build() + + private fun createImeSubtypeForLanguageTagAndLayoutType( + languageTag: String, + layoutType: String + ): InputMethodSubtype = + InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(nextImeSubtypeId++) + .setPhysicalKeyboardHint(ULocale.forLanguageTag(languageTag), layoutType).build() + + private fun hasLayout(layoutList: Array<KeyboardLayout>, layoutDesc: String): Boolean { + for (kl in layoutList) { + if (kl.descriptor == layoutDesc) { + return true + } + } + return false + } + + private fun createLayoutDescriptor(keyboardName: String): String = + "$PACKAGE_NAME/$RECEIVER_NAME/$keyboardName" + + private fun containsLayout(layoutList: Array<KeyboardLayout>, layoutDesc: String): Boolean { + for (kl in layoutList) { + if (kl.descriptor.equals(layoutDesc)) { + return true + } + } + return false + } + + private fun createMockReceiver(): ResolveInfo { + val info = ResolveInfo() + info.activityInfo = ActivityInfo() + info.activityInfo.packageName = PACKAGE_NAME + info.activityInfo.name = RECEIVER_NAME + info.activityInfo.applicationInfo = ApplicationInfo() + info.activityInfo.metaData = Bundle() + info.activityInfo.metaData.putInt( + InputManager.META_DATA_KEYBOARD_LAYOUTS, + R.xml.keyboard_layouts + ) + info.serviceInfo = ServiceInfo() + info.serviceInfo.packageName = PACKAGE_NAME + info.serviceInfo.name = RECEIVER_NAME + return info + } + + private inner class NewSettingsApiFlag constructor(enabled: Boolean) : AutoCloseable { + init { + Settings.Global.putString( + context.contentResolver, + "settings_new_keyboard_ui", enabled.toString() + ) + } + + override fun close() { + Settings.Global.putString( + context.contentResolver, + "settings_new_keyboard_ui", + "" + ) + } + } +} diff --git a/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt b/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt new file mode 100644 index 000000000000..b39c93244ad6 --- /dev/null +++ b/tests/Input/src/com/android/server/input/KeyboardMetricsCollectorTests.kt @@ -0,0 +1,218 @@ +/* + * Copyright 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.server.input + +import android.hardware.input.KeyboardLayout +import android.icu.util.ULocale +import android.platform.test.annotations.Presubmit +import android.view.InputDevice +import android.view.inputmethod.InputMethodSubtype +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +private fun createKeyboard( + deviceId: Int, + vendorId: Int, + productId: Int, + languageTag: String?, + layoutType: String? +): InputDevice = + InputDevice.Builder() + .setId(deviceId) + .setName("Device $deviceId") + .setDescriptor("descriptor $deviceId") + .setSources(InputDevice.SOURCE_KEYBOARD) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC) + .setExternal(true) + .setVendorId(vendorId) + .setProductId(productId) + .setKeyboardLanguageTag(languageTag) + .setKeyboardLayoutType(layoutType) + .build() + +private fun createImeSubtype( + imeSubtypeId: Int, + languageTag: ULocale?, + layoutType: String +): InputMethodSubtype = + InputMethodSubtype.InputMethodSubtypeBuilder().setSubtypeId(imeSubtypeId) + .setPhysicalKeyboardHint(languageTag, layoutType).build() + +/** + * Tests for {@link KeyboardMetricsCollector}. + * + * Build/Install/Run: + * atest InputTests:KeyboardMetricsCollectorTests + */ +@Presubmit +class KeyboardMetricsCollectorTests { + + companion object { + const val DEVICE_ID = 1 + const val DEFAULT_VENDOR_ID = 123 + const val DEFAULT_PRODUCT_ID = 456 + } + + @Test + fun testCreateKeyboardConfigurationEvent_throwsExceptionWithoutAnyLayoutConfiguration() { + assertThrows(IllegalStateException::class.java) { + KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + null, + null + ) + ).build() + } + } + + @Test + fun testCreateKeyboardConfigurationEvent_throwsExceptionWithInvalidLayoutSelectionCriteria() { + assertThrows(IllegalStateException::class.java) { + KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + null, + null + ) + ).addLayoutSelection(createImeSubtype(1, ULocale.forLanguageTag("en-US"), "qwerty"), + null, 123).build() + } + } + + @Test + fun testCreateKeyboardConfigurationEvent_withMultipleConfigurations() { + val builder = KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + "de-CH", + "qwertz" + ) + ) + val event = builder.addLayoutSelection( + createImeSubtype(1, ULocale.forLanguageTag("en-US"), "qwerty"), + KeyboardLayout(null, "English(US)(Qwerty)", null, 0, null, 0, 0, 0), + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD + ).addLayoutSelection( + createImeSubtype(2, ULocale.forLanguageTag("en-US"), "azerty"), + null, // Default layout type + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER + ).addLayoutSelection( + createImeSubtype(3, ULocale.forLanguageTag("en-US"), "qwerty"), + KeyboardLayout(null, "German", null, 0, null, 0, 0, 0), + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE + ).setIsFirstTimeConfiguration(true).build() + + assertEquals( + "KeyboardConfigurationEvent should pick vendor ID from provided InputDevice", + DEFAULT_VENDOR_ID, + event.vendorId + ) + assertEquals( + "KeyboardConfigurationEvent should pick product ID from provided InputDevice", + DEFAULT_PRODUCT_ID, + event.productId + ) + assertTrue(event.isFirstConfiguration) + + assertEquals( + "KeyboardConfigurationEvent should contain 3 configurations provided", + 3, + event.layoutConfigurations.size + ) + assertExpectedLayoutConfiguration( + event.layoutConfigurations[0], + "de-CH", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"), + "English(US)(Qwerty)", + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD, + "en-US", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"), + ) + assertExpectedLayoutConfiguration( + event.layoutConfigurations[1], + "de-CH", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"), + KeyboardMetricsCollector.DEFAULT_LAYOUT_NAME, + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_USER, + "en-US", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("azerty"), + ) + assertExpectedLayoutConfiguration( + event.layoutConfigurations[2], + "de-CH", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwertz"), + "German", + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE, + "en-US", + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"), + ) + } + + @Test + fun testCreateKeyboardConfigurationEvent_withDefaultLanguageTag() { + val builder = KeyboardMetricsCollector.KeyboardConfigurationEvent.Builder( + createKeyboard( + DEVICE_ID, + DEFAULT_VENDOR_ID, + DEFAULT_PRODUCT_ID, + "und", // Undefined language tag + "azerty" + ) + ) + val event = builder.addLayoutSelection( + createImeSubtype(4, null, "qwerty"), // Default language tag + KeyboardLayout(null, "German", null, 0, null, 0, 0, 0), + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE + ).build() + + assertExpectedLayoutConfiguration( + event.layoutConfigurations[0], + KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("azerty"), + "German", + KeyboardMetricsCollector.LAYOUT_SELECTION_CRITERIA_DEVICE, + KeyboardMetricsCollector.DEFAULT_LANGUAGE_TAG, + KeyboardLayout.LayoutType.getLayoutTypeEnumValue("qwerty"), + ) + } + + private fun assertExpectedLayoutConfiguration( + configuration: KeyboardMetricsCollector.LayoutConfiguration, + expectedKeyboardLanguageTag: String, + expectedKeyboardLayoutType: Int, + expectedSelectedLayout: String, + expectedLayoutSelectionCriteria: Int, + expectedImeLanguageTag: String, + expectedImeLayoutType: Int + ) { + assertEquals(expectedKeyboardLanguageTag, configuration.keyboardLanguageTag) + assertEquals(expectedKeyboardLayoutType, configuration.keyboardLayoutType) + assertEquals(expectedSelectedLayout, configuration.keyboardLayoutName) + assertEquals(expectedLayoutSelectionCriteria, configuration.layoutSelectionCriteria) + assertEquals(expectedImeLanguageTag, configuration.imeLanguageTag) + assertEquals(expectedImeLayoutType, configuration.imeLayoutType) + } +} diff --git a/tests/Input/src/com/android/test/input/AnrTest.kt b/tests/Input/src/com/android/test/input/AnrTest.kt index 7bc5df52e316..4893d14ad79b 100644 --- a/tests/Input/src/com/android/test/input/AnrTest.kt +++ b/tests/Input/src/com/android/test/input/AnrTest.kt @@ -100,6 +100,7 @@ class AnrTest { private fun clickCloseAppOnAnrDialog() { // Find anr dialog and kill app + val timestamp = System.currentTimeMillis() val uiDevice: UiDevice = UiDevice.getInstance(instrumentation) val closeAppButton: UiObject2? = uiDevice.wait(Until.findObject(By.res("android:id/aerr_close")), 20000) @@ -107,7 +108,6 @@ class AnrTest { fail("Could not find anr dialog") return } - val initialReasons = getExitReasons() closeAppButton.click() /** * We must wait for the app to be fully closed before exiting this test. This is because @@ -116,7 +116,7 @@ class AnrTest { * the killing logic will apply to the newly launched 'am start' instance, and the second * test will fail because the unresponsive activity will never be launched. */ - waitForNewExitReason(initialReasons[0].timestamp) + waitForNewExitReasonAfter(timestamp) } private fun clickWaitOnAnrDialog() { @@ -140,20 +140,20 @@ class AnrTest { return infos } - private fun waitForNewExitReason(previousExitTimestamp: Long) { + private fun waitForNewExitReasonAfter(timestamp: Long) { PollingCheck.waitFor { - getExitReasons()[0].timestamp > previousExitTimestamp + val reasons = getExitReasons() + !reasons.isEmpty() && reasons[0].timestamp >= timestamp } val reasons = getExitReasons() - assertTrue(reasons[0].timestamp > previousExitTimestamp) + assertTrue(reasons[0].timestamp > timestamp) assertEquals(ApplicationExitInfo.REASON_ANR, reasons[0].reason) } private fun triggerAnr() { startUnresponsiveActivity() val uiDevice: UiDevice = UiDevice.getInstance(instrumentation) - val obj: UiObject2? = uiDevice.wait(Until.findObject( - By.text("Unresponsive gesture monitor")), 10000) + val obj: UiObject2? = uiDevice.wait(Until.findObject(By.pkg(PACKAGE_NAME)), 10000) if (obj == null) { fail("Could not find unresponsive activity") diff --git a/tests/Input/src/com/android/test/input/InputEventSenderAndReceiverTest.kt b/tests/Input/src/com/android/test/input/InputEventSenderAndReceiverTest.kt index 37b67f4c183c..075cf0cc5a45 100644 --- a/tests/Input/src/com/android/test/input/InputEventSenderAndReceiverTest.kt +++ b/tests/Input/src/com/android/test/input/InputEventSenderAndReceiverTest.kt @@ -113,13 +113,11 @@ class InputEventSenderAndReceiverTest { val sent = SpyInputEventSender.Timeline( inputEventId = 1, gpuCompletedTime = 3, presentTime = 2) mReceiver.reportTimeline(sent.inputEventId, sent.gpuCompletedTime, sent.presentTime) - val received = mSender.getTimeline() - assertEquals(null, received) + mSender.assertNoEvents() // Sender will no longer receive callbacks for this fd, even if receiver sends a valid // timeline later mReceiver.reportTimeline(2 /*inputEventId*/, 3 /*gpuCompletedTime*/, 4 /*presentTime*/) - val receivedSecondTimeline = mSender.getTimeline() - assertEquals(null, receivedSecondTimeline) + mSender.assertNoEvents() } /** diff --git a/tests/Input/src/com/android/test/input/PointerEventDispatcherTest.kt b/tests/Input/src/com/android/test/input/PointerEventDispatcherTest.kt index 1099878a1954..f311bc222d22 100644 --- a/tests/Input/src/com/android/test/input/PointerEventDispatcherTest.kt +++ b/tests/Input/src/com/android/test/input/PointerEventDispatcherTest.kt @@ -25,7 +25,6 @@ import android.view.WindowManagerPolicyConstants.PointerEventListener import com.android.server.UiThread import com.android.server.wm.PointerEventDispatcher import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull import org.junit.After import org.junit.Before import org.junit.Test @@ -86,8 +85,7 @@ class PointerEventDispatcherTest { // Since the listener raises an exception during the event handling, the event should be // marked as 'not handled'. assertEquals(SpyInputEventSender.FinishedSignal(seq, handled = false), finishedSignal) - // Ensure that there aren't double finish calls. This would crash if there's a call - // to finish twice. - assertNull(mSender.getFinishedSignal()) + // Ensure that there aren't double finish calls. + mSender.assertNoEvents() } } diff --git a/tests/Input/src/com/android/test/input/SpyInputEventSenderAndReceiver.kt b/tests/Input/src/com/android/test/input/SpyInputEventSenderAndReceiver.kt index 2d9af9a65d33..5cbfce534b15 100644 --- a/tests/Input/src/com/android/test/input/SpyInputEventSenderAndReceiver.kt +++ b/tests/Input/src/com/android/test/input/SpyInputEventSenderAndReceiver.kt @@ -27,10 +27,17 @@ import android.view.MotionEvent import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit +import org.junit.Assert.assertNull + private fun <T> getEvent(queue: LinkedBlockingQueue<T>): T? { return queue.poll(DEFAULT_DISPATCHING_TIMEOUT_MILLIS.toLong(), TimeUnit.MILLISECONDS) } +private fun <T> assertNoEvents(queue: LinkedBlockingQueue<T>) { + // Poll the queue with a shorter timeout, to make the check faster. + assertNull(queue.poll(100L, TimeUnit.MILLISECONDS)) +} + class SpyInputEventReceiver(channel: InputChannel, looper: Looper) : InputEventReceiver(channel, looper) { private val mInputEvents = LinkedBlockingQueue<InputEvent>() @@ -72,4 +79,9 @@ class SpyInputEventSender(channel: InputChannel, looper: Looper) : fun getTimeline(): Timeline? { return getEvent(mTimelines) } + + fun assertNoEvents() { + assertNoEvents(mFinishedSignals) + assertNoEvents(mTimelines) + } } diff --git a/tests/InputMethodStressTest/TEST_MAPPING b/tests/InputMethodStressTest/TEST_MAPPING index 06e2ce84eb0f..d982dd817eaa 100644 --- a/tests/InputMethodStressTest/TEST_MAPPING +++ b/tests/InputMethodStressTest/TEST_MAPPING @@ -1,5 +1,5 @@ { - "presubmit-large": [ + "postsubmit": [ { "name": "InputMethodStressTest" } diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/NotificationTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/NotificationTest.java index f4a04a163ebb..b6b99242c414 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/NotificationTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/NotificationTest.java @@ -97,6 +97,8 @@ public final class NotificationTest { assumeFalse(pm.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)); // Do not run on TV. Direct Reply isn't supported on TV. assumeFalse(pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK_ONLY)); + // Do not run on Wear. Direct Reply isn't supported on Wear. + assumeFalse(pm.hasSystemFeature(PackageManager.FEATURE_WATCH)); } @After diff --git a/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java b/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java index 7419ee1230d3..00085f845cd0 100644 --- a/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java +++ b/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java @@ -104,11 +104,11 @@ public class TimeoutRecordTest { @Test public void forServiceExec_returnsCorrectTimeoutRecord() { - TimeoutRecord record = TimeoutRecord.forServiceExec("Test ANR reason"); + TimeoutRecord record = TimeoutRecord.forServiceExec("com.app.MyService", 1000L); assertNotNull(record); assertEquals(record.mKind, TimeoutRecord.TimeoutKind.SERVICE_EXEC); - assertEquals(record.mReason, "Test ANR reason"); + assertEquals(record.mReason, "executing service com.app.MyService, waited 1000ms"); assertTrue(record.mEndTakenBeforeLocks); } diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt index 7cf69b7780d9..c92d768439fd 100644 --- a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt +++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt @@ -42,7 +42,7 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context private var selectedImage = -1 private var outputMode = R.id.output_hdr private var bitmap: Bitmap? = null - private var gainmap: Gainmap? = null + private var originalGainmap: Gainmap? = null private var gainmapVisualizer: Bitmap? = null private lateinit var imageView: SubsamplingScaleImageView private lateinit var gainmapMetadataEditor: GainmapMetadataEditor @@ -65,15 +65,8 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context findViewById<RadioGroup>(R.id.output_mode)!!.also { it.check(outputMode) it.setOnCheckedChangeListener { _, checkedId -> - val previousMode = outputMode outputMode = checkedId - if (previousMode == R.id.output_sdr && checkedId == R.id.output_hdr) { - animateToHdr() - } else if (previousMode == R.id.output_hdr && checkedId == R.id.output_sdr) { - animateToSdr() - } else { - updateDisplay() - } + updateDisplay() } } @@ -120,7 +113,7 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context } private fun doDecode(source: ImageDecoder.Source) { - gainmap = null + originalGainmap = null bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, source -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE } @@ -138,9 +131,10 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context findViewById<TextView>(R.id.error_msg)!!.visibility = View.GONE findViewById<RadioGroup>(R.id.output_mode)!!.visibility = View.VISIBLE - gainmap = bitmap!!.gainmap - gainmapMetadataEditor.setGainmap(gainmap) - val map = gainmap!!.gainmapContents + val gainmap = bitmap!!.gainmap!! + originalGainmap = gainmap + gainmapMetadataEditor.setGainmap(Gainmap(gainmap, gainmap.gainmapContents)) + val map = gainmap.gainmapContents if (map.config != Bitmap.Config.ALPHA_8) { gainmapVisualizer = map } else { @@ -164,33 +158,17 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context updateDisplay() } - private fun animateToHdr() { - if (bitmap == null || gainmap == null) return - - // TODO: Trigger an animation - updateDisplay() - } - - private fun animateToSdr() { - if (bitmap == null) return - - // TODO: Trigger an animation - updateDisplay() - } - private fun updateDisplay() { if (bitmap == null) return imageView.setImage(ImageSource.cachedBitmap(when (outputMode) { R.id.output_hdr -> { - gainmapMetadataEditor.useOriginalMetadata() - bitmap!!.gainmap = gainmap + bitmap!!.gainmap = originalGainmap bitmap!! } R.id.output_hdr_test -> { - gainmapMetadataEditor.useEditMetadata() - bitmap!!.gainmap = gainmap + bitmap!!.gainmap = gainmapMetadataEditor.editedGainmap() bitmap!! } diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt index 1a79a11a3718..43debb11013a 100644 --- a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt +++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt @@ -28,23 +28,27 @@ import android.widget.TextView import com.android.test.silkfx.R data class GainmapMetadata( - var ratioMin: Float, - var ratioMax: Float, - var capacityMin: Float, - var capacityMax: Float, - var gamma: Float, - var offsetSdr: Float, - var offsetHdr: Float + var ratioMin: Float, + var ratioMax: Float, + var capacityMin: Float, + var capacityMax: Float, + var gamma: Float, + var offsetSdr: Float, + var offsetHdr: Float ) +/** + * Note: This can only handle single-channel gainmaps nicely. It will force all 3-channel + * metadata to have the same value single value and is not intended to be a robust demonstration + * of gainmap metadata editing + */ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { - private var gainmap: Gainmap? = null - private var showingEdits = false + private lateinit var gainmap: Gainmap private var metadataPopup: PopupWindow? = null private var originalMetadata: GainmapMetadata = GainmapMetadata( - 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f) + 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f) private var currentMetadata: GainmapMetadata = originalMetadata.copy() private val maxProgress = 100.0f @@ -61,23 +65,18 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { private val maxGamma = 3.0f // Min and max offsets are 0.0 and 1.0 respectively - fun setGainmap(newGainmap: Gainmap?) { + fun setGainmap(newGainmap: Gainmap) { gainmap = newGainmap - originalMetadata = GainmapMetadata(gainmap!!.getRatioMin()[0], - gainmap!!.getRatioMax()[0], gainmap!!.getMinDisplayRatioForHdrTransition(), - gainmap!!.getDisplayRatioForFullHdr(), gainmap!!.getGamma()[0], - gainmap!!.getEpsilonSdr()[0], gainmap!!.getEpsilonHdr()[0]) + originalMetadata = GainmapMetadata(gainmap.getRatioMin()[0], + gainmap.getRatioMax()[0], gainmap.getMinDisplayRatioForHdrTransition(), + gainmap.getDisplayRatioForFullHdr(), gainmap.getGamma()[0], + gainmap.getEpsilonSdr()[0], gainmap.getEpsilonHdr()[0]) currentMetadata = originalMetadata.copy() } - fun useOriginalMetadata() { - showingEdits = false - applyMetadata(originalMetadata) - } - - fun useEditMetadata() { - showingEdits = true + fun editedGainmap(): Gainmap { applyMetadata(currentMetadata) + return gainmap } fun closeEditor() { @@ -93,7 +92,7 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { val view = LayoutInflater.from(parent.getContext()).inflate(R.layout.gainmap_metadata, null) metadataPopup = PopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT) + ViewGroup.LayoutParams.WRAP_CONTENT) metadataPopup!!.showAtLocation(view, Gravity.CENTER, 0, 0) (view.getParent() as ViewGroup).removeView(view) @@ -117,7 +116,7 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { val offsetSdrSeek = view.requireViewById<SeekBar>(R.id.gainmap_metadata_offsetsdr) val offsetHdrSeek = view.requireViewById<SeekBar>(R.id.gainmap_metadata_offsethdr) arrayOf(gainmapMinSeek, gainmapMaxSeek, capacityMinSeek, capacityMaxSeek, gammaSeek, - offsetSdrSeek, offsetHdrSeek).forEach { + offsetSdrSeek, offsetHdrSeek).forEach { it.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{ override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { if (!fromUser) return @@ -149,37 +148,37 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { val offsetHdrSeek = parent.requireViewById<SeekBar>(R.id.gainmap_metadata_offsethdr) gainmapMinSeek.setProgress( - ((currentMetadata.ratioMin - minRatioMin) / maxRatioMin * maxProgress).toInt()) + ((currentMetadata.ratioMin - minRatioMin) / maxRatioMin * maxProgress).toInt()) gainmapMaxSeek.setProgress( - ((currentMetadata.ratioMax - minRatioMax) / maxRatioMax * maxProgress).toInt()) + ((currentMetadata.ratioMax - minRatioMax) / maxRatioMax * maxProgress).toInt()) capacityMinSeek.setProgress( - ((currentMetadata.capacityMin - minCapacityMin) / maxCapacityMin * maxProgress).toInt()) + ((currentMetadata.capacityMin - minCapacityMin) / maxCapacityMin * maxProgress).toInt()) capacityMaxSeek.setProgress( - ((currentMetadata.capacityMax - minCapacityMax) / maxCapacityMax * maxProgress).toInt()) + ((currentMetadata.capacityMax - minCapacityMax) / maxCapacityMax * maxProgress).toInt()) gammaSeek.setProgress( - ((currentMetadata.gamma - minGamma) / maxGamma * maxProgress).toInt()) + ((currentMetadata.gamma - minGamma) / maxGamma * maxProgress).toInt()) // Log base 3 via: log_b(x) = log_y(x) / log_y(b) offsetSdrSeek.setProgress( - ((1.0 - Math.log(currentMetadata.offsetSdr.toDouble() / Math.log(3.0)) / -11.0) - .toFloat() * maxProgress).toInt()) + ((1.0 - Math.log(currentMetadata.offsetSdr.toDouble() / Math.log(3.0)) / -11.0) + .toFloat() * maxProgress).toInt()) offsetHdrSeek.setProgress( - ((1.0 - Math.log(currentMetadata.offsetHdr.toDouble() / Math.log(3.0)) / -11.0) - .toFloat() * maxProgress).toInt()) + ((1.0 - Math.log(currentMetadata.offsetHdr.toDouble() / Math.log(3.0)) / -11.0) + .toFloat() * maxProgress).toInt()) parent.requireViewById<TextView>(R.id.gainmap_metadata_gainmapmin_val).setText( - "%.3f".format(currentMetadata.ratioMin)) + "%.3f".format(currentMetadata.ratioMin)) parent.requireViewById<TextView>(R.id.gainmap_metadata_gainmapmax_val).setText( - "%.3f".format(currentMetadata.ratioMax)) + "%.3f".format(currentMetadata.ratioMax)) parent.requireViewById<TextView>(R.id.gainmap_metadata_capacitymin_val).setText( - "%.3f".format(currentMetadata.capacityMin)) + "%.3f".format(currentMetadata.capacityMin)) parent.requireViewById<TextView>(R.id.gainmap_metadata_capacitymax_val).setText( - "%.3f".format(currentMetadata.capacityMax)) + "%.3f".format(currentMetadata.capacityMax)) parent.requireViewById<TextView>(R.id.gainmap_metadata_gamma_val).setText( - "%.3f".format(currentMetadata.gamma)) + "%.3f".format(currentMetadata.gamma)) parent.requireViewById<TextView>(R.id.gainmap_metadata_offsetsdr_val).setText( - "%.5f".format(currentMetadata.offsetSdr)) + "%.5f".format(currentMetadata.offsetSdr)) parent.requireViewById<TextView>(R.id.gainmap_metadata_offsethdr_val).setText( - "%.5f".format(currentMetadata.offsetHdr)) + "%.5f".format(currentMetadata.offsetHdr)) } private fun resetGainmapMetadata() { @@ -189,69 +188,59 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { } private fun applyMetadata(newMetadata: GainmapMetadata) { - gainmap!!.setRatioMin(newMetadata.ratioMin, newMetadata.ratioMin, newMetadata.ratioMin) - gainmap!!.setRatioMax(newMetadata.ratioMax, newMetadata.ratioMax, newMetadata.ratioMax) - gainmap!!.setMinDisplayRatioForHdrTransition(newMetadata.capacityMin) - gainmap!!.setDisplayRatioForFullHdr(newMetadata.capacityMax) - gainmap!!.setGamma(newMetadata.gamma, newMetadata.gamma, newMetadata.gamma) - gainmap!!.setEpsilonSdr(newMetadata.offsetSdr, newMetadata.offsetSdr, newMetadata.offsetSdr) - gainmap!!.setEpsilonHdr(newMetadata.offsetHdr, newMetadata.offsetHdr, newMetadata.offsetHdr) + gainmap.setRatioMin(newMetadata.ratioMin, newMetadata.ratioMin, newMetadata.ratioMin) + gainmap.setRatioMax(newMetadata.ratioMax, newMetadata.ratioMax, newMetadata.ratioMax) + gainmap.setMinDisplayRatioForHdrTransition(newMetadata.capacityMin) + gainmap.setDisplayRatioForFullHdr(newMetadata.capacityMax) + gainmap.setGamma(newMetadata.gamma, newMetadata.gamma, newMetadata.gamma) + gainmap.setEpsilonSdr(newMetadata.offsetSdr, newMetadata.offsetSdr, newMetadata.offsetSdr) + gainmap.setEpsilonHdr(newMetadata.offsetHdr, newMetadata.offsetHdr, newMetadata.offsetHdr) renderView.invalidate() } private fun updateGainmapMin(normalized: Float) { val newValue = minRatioMin + normalized * (maxRatioMin - minRatioMin) parent.requireViewById<TextView>(R.id.gainmap_metadata_gainmapmin_val).setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.ratioMin = newValue - if (showingEdits) { - gainmap!!.setRatioMin(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setRatioMin(newValue, newValue, newValue) + renderView.invalidate() } private fun updateGainmapMax(normalized: Float) { val newValue = minRatioMax + normalized * (maxRatioMax - minRatioMax) parent.requireViewById<TextView>(R.id.gainmap_metadata_gainmapmax_val).setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.ratioMax = newValue - if (showingEdits) { - gainmap!!.setRatioMax(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setRatioMax(newValue, newValue, newValue) + renderView.invalidate() } private fun updateCapacityMin(normalized: Float) { val newValue = minCapacityMin + normalized * (maxCapacityMin - minCapacityMin) parent.requireViewById<TextView>(R.id.gainmap_metadata_capacitymin_val).setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.capacityMin = newValue - if (showingEdits) { - gainmap!!.setMinDisplayRatioForHdrTransition(newValue) - renderView.invalidate() - } + gainmap.setMinDisplayRatioForHdrTransition(newValue) + renderView.invalidate() } private fun updateCapacityMax(normalized: Float) { val newValue = minCapacityMax + normalized * (maxCapacityMax - minCapacityMax) parent.requireViewById<TextView>(R.id.gainmap_metadata_capacitymax_val).setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.capacityMax = newValue - if (showingEdits) { - gainmap!!.setDisplayRatioForFullHdr(newValue) - renderView.invalidate() - } + gainmap.setDisplayRatioForFullHdr(newValue) + renderView.invalidate() } private fun updateGamma(normalized: Float) { val newValue = minGamma + normalized * (maxGamma - minGamma) parent.requireViewById<TextView>(R.id.gainmap_metadata_gamma_val).setText( - "%.3f".format(newValue)) + "%.3f".format(newValue)) currentMetadata.gamma = newValue - if (showingEdits) { - gainmap!!.setGamma(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setGamma(newValue, newValue, newValue) + renderView.invalidate() } private fun updateOffsetSdr(normalized: Float) { @@ -260,12 +249,10 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { newValue = Math.pow(3.0, (1.0 - normalized.toDouble()) * -11.0).toFloat() } parent.requireViewById<TextView>(R.id.gainmap_metadata_offsetsdr_val).setText( - "%.5f".format(newValue)) + "%.5f".format(newValue)) currentMetadata.offsetSdr = newValue - if (showingEdits) { - gainmap!!.setEpsilonSdr(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setEpsilonSdr(newValue, newValue, newValue) + renderView.invalidate() } private fun updateOffsetHdr(normalized: Float) { @@ -274,11 +261,9 @@ class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) { newValue = Math.pow(3.0, (1.0 - normalized.toDouble()) * -11.0).toFloat() } parent.requireViewById<TextView>(R.id.gainmap_metadata_offsethdr_val).setText( - "%.5f".format(newValue)) + "%.5f".format(newValue)) currentMetadata.offsetHdr = newValue - if (showingEdits) { - gainmap!!.setEpsilonHdr(newValue, newValue, newValue) - renderView.invalidate() - } + gainmap.setEpsilonHdr(newValue, newValue, newValue) + renderView.invalidate() } } diff --git a/tests/SurfaceViewBufferTests/Android.bp b/tests/SurfaceViewBufferTests/Android.bp index dc75f00e7cdc..38313f85c31d 100644 --- a/tests/SurfaceViewBufferTests/Android.bp +++ b/tests/SurfaceViewBufferTests/Android.bp @@ -23,7 +23,10 @@ package { android_test { name: "SurfaceViewBufferTests", - srcs: ["**/*.java","**/*.kt"], + srcs: [ + "**/*.java", + "**/*.kt", + ], manifest: "AndroidManifest.xml", test_config: "AndroidTest.xml", platform_apis: true, @@ -41,6 +44,7 @@ android_test { "kotlin-stdlib", "kotlinx-coroutines-android", "flickerlib", + "flickerlib-trace_processor_shell", "truth-prebuilt", "cts-wm-util", "CtsSurfaceValidatorLib", @@ -60,7 +64,7 @@ cc_library_shared { "libandroid", ], include_dirs: [ - "system/core/include" + "system/core/include", ], stl: "libc++_static", cflags: [ diff --git a/tests/SurfaceViewBufferTests/AndroidManifest.xml b/tests/SurfaceViewBufferTests/AndroidManifest.xml index c910ecdac1b3..798c67a320ce 100644 --- a/tests/SurfaceViewBufferTests/AndroidManifest.xml +++ b/tests/SurfaceViewBufferTests/AndroidManifest.xml @@ -29,9 +29,12 @@ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <!-- Save failed test bitmap images !--> <uses-permission android:name="android.Manifest.permission.WRITE_EXTERNAL_STORAGE"/> + <!-- Allow the test to connect to perfetto trace processor --> + <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="false" - android:supportsRtl="true"> + android:supportsRtl="true" + android:networkSecurityConfig="@xml/network_security_config"> <activity android:name=".MainActivity" android:taskAffinity="com.android.test.MainActivity" android:theme="@style/AppTheme" @@ -43,7 +46,7 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> - <service android:name="android.view.cts.surfacevalidator.LocalMediaProjectionService" + <service android:name="com.android.test.LocalMediaProjectionService" android:foregroundServiceType="mediaProjection" android:enabled="true"> </service> diff --git a/tests/SurfaceViewBufferTests/res/xml/network_security_config.xml b/tests/SurfaceViewBufferTests/res/xml/network_security_config.xml new file mode 100644 index 000000000000..4bd9ca049f55 --- /dev/null +++ b/tests/SurfaceViewBufferTests/res/xml/network_security_config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<network-security-config> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">localhost</domain> + </domain-config> +</network-security-config> diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/InverseDisplayTransformTests.kt b/tests/SurfaceViewBufferTests/src/com/android/test/InverseDisplayTransformTests.kt index e722ba537a8e..1de965ebcb36 100644 --- a/tests/SurfaceViewBufferTests/src/com/android/test/InverseDisplayTransformTests.kt +++ b/tests/SurfaceViewBufferTests/src/com/android/test/InverseDisplayTransformTests.kt @@ -76,4 +76,4 @@ class InverseDisplayTransformTests(useBlastAdapter: Boolean) : } LayersTraceSubject(trace).layer("SurfaceView", 3).hasBufferSize(rotatedBufferSize) } -} +}
\ No newline at end of file diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/LocalMediaProjectionService.java b/tests/SurfaceViewBufferTests/src/com/android/test/LocalMediaProjectionService.java new file mode 100644 index 000000000000..7339a6b8c9a4 --- /dev/null +++ b/tests/SurfaceViewBufferTests/src/com/android/test/LocalMediaProjectionService.java @@ -0,0 +1,100 @@ +/* + * 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.test; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.drawable.Icon; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; + +public class LocalMediaProjectionService extends Service { + + private Bitmap mTestBitmap; + + private static final String NOTIFICATION_CHANNEL_ID = "Surfacevalidator"; + private static final String CHANNEL_NAME = "ProjectionService"; + + static final int MSG_START_FOREGROUND_DONE = 1; + static final String EXTRA_MESSENGER = "messenger"; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + startForeground(intent); + return super.onStartCommand(intent, flags, startId); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + if (mTestBitmap != null) { + mTestBitmap.recycle(); + mTestBitmap = null; + } + super.onDestroy(); + } + + private Icon createNotificationIcon() { + mTestBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(mTestBitmap); + canvas.drawColor(Color.BLUE); + return Icon.createWithBitmap(mTestBitmap); + } + + private void startForeground(Intent intent) { + final NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, + CHANNEL_NAME, NotificationManager.IMPORTANCE_NONE); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + + final NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.createNotificationChannel(channel); + + final Notification.Builder notificationBuilder = + new Notification.Builder(this, NOTIFICATION_CHANNEL_ID); + + final Notification notification = notificationBuilder.setOngoing(true) + .setContentTitle("App is running") + .setSmallIcon(createNotificationIcon()) + .setCategory(Notification.CATEGORY_SERVICE) + .setContentText("Context") + .build(); + + startForeground(2, notification); + + final Messenger messenger = intent.getParcelableExtra(EXTRA_MESSENGER); + final Message msg = Message.obtain(); + msg.what = MSG_START_FOREGROUND_DONE; + try { + messenger.send(msg); + } catch (RemoteException e) { + } + } + +} diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/ScreenRecordTestBase.kt b/tests/SurfaceViewBufferTests/src/com/android/test/ScreenRecordTestBase.kt index df3d30e13908..e80dd8ed0690 100644 --- a/tests/SurfaceViewBufferTests/src/com/android/test/ScreenRecordTestBase.kt +++ b/tests/SurfaceViewBufferTests/src/com/android/test/ScreenRecordTestBase.kt @@ -19,7 +19,6 @@ import android.annotation.ColorInt import android.content.Context import android.content.Intent import android.graphics.Rect -import android.server.wm.WindowManagerState.getLogicalDisplaySize import android.view.cts.surfacevalidator.CapturedActivity import android.view.cts.surfacevalidator.ISurfaceValidatorTestCase import android.view.cts.surfacevalidator.PixelChecker @@ -44,8 +43,6 @@ open class ScreenRecordTestBase(useBlastAdapter: Boolean) : mActivity = mActivityRule.launchActivity(Intent()) lateinit var surfaceReadyLatch: CountDownLatch runOnUiThread { - it.dismissPermissionDialog() - it.setLogicalDisplaySize(getLogicalDisplaySize()) surfaceReadyLatch = it.addSurfaceView(defaultBufferSize) } surfaceReadyLatch.await() diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeTests.kt b/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeTests.kt index be3ed715d4e2..4c5224a8b151 100644 --- a/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeTests.kt +++ b/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeTests.kt @@ -87,4 +87,4 @@ class SharedBufferModeTests(useBlastAdapter: Boolean) : SurfaceTracingTestBase(u checkPixels(svBounds, Color.BLUE) } } -} +}
\ No newline at end of file diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/SurfaceTracingTestBase.kt b/tests/SurfaceViewBufferTests/src/com/android/test/SurfaceTracingTestBase.kt index cf4cb8c97ea1..b03b7335b08b 100644 --- a/tests/SurfaceViewBufferTests/src/com/android/test/SurfaceTracingTestBase.kt +++ b/tests/SurfaceViewBufferTests/src/com/android/test/SurfaceTracingTestBase.kt @@ -21,11 +21,9 @@ import android.graphics.Color import android.graphics.Rect import android.util.Log import androidx.test.ext.junit.rules.ActivityScenarioRule -import android.tools.common.flicker.subject.layers.LayerSubject import android.tools.common.traces.surfaceflinger.LayersTrace -import android.tools.device.traces.io.ResultWriter -import android.tools.device.traces.monitors.surfaceflinger.LayersTraceMonitor import android.tools.device.traces.monitors.withSFTracing +import android.tools.device.traces.monitors.PerfettoTraceMonitor import junit.framework.Assert import org.junit.After import org.junit.Before @@ -33,6 +31,7 @@ import org.junit.Rule import java.io.FileOutputStream import java.io.IOException import java.util.concurrent.CountDownLatch +import perfetto.protos.PerfettoConfig.SurfaceFlingerLayersConfig open class SurfaceTracingTestBase(useBlastAdapter: Boolean) : SurfaceViewBufferTestBase(useBlastAdapter) { @@ -43,7 +42,7 @@ open class SurfaceTracingTestBase(useBlastAdapter: Boolean) : @Before override fun setup() { super.setup() - stopLayerTrace() + PerfettoTraceMonitor.stopAllSessions() addSurfaceView() } @@ -83,10 +82,6 @@ open class SurfaceTracingTestBase(useBlastAdapter: Boolean) : instrumentation.waitForIdleSync() } - private fun stopLayerTrace() { - LayersTraceMonitor().stop(ResultWriter()) - } - fun checkPixels(bounds: Rect, @ColorInt color: Int) { val screenshot = instrumentation.getUiAutomation().takeScreenshot() val pixels = IntArray(screenshot.width * screenshot.height) @@ -106,14 +101,19 @@ open class SurfaceTracingTestBase(useBlastAdapter: Boolean) : Log.e("SurfaceViewBufferTests", "Error writing bitmap to file", e) } } - Assert.assertEquals("Checking $bounds found mismatch $i,$j", - Color.valueOf(color), Color.valueOf(actualColor)) + Assert.assertEquals( + "Checking $bounds found mismatch $i,$j", + Color.valueOf(color), + Color.valueOf(actualColor) + ) } } } private companion object { - private const val TRACE_FLAGS = - (1 shl 0) or (1 shl 5) or (1 shl 6) // TRACE_CRITICAL | TRACE_BUFFERS | TRACE_SYNC + private val TRACE_FLAGS = listOf( + SurfaceFlingerLayersConfig.TraceFlag.TRACE_FLAG_BUFFERS, + SurfaceFlingerLayersConfig.TraceFlag.TRACE_FLAG_VIRTUAL_DISPLAYS, + ) } -} +}
\ No newline at end of file diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/SurfaceViewBufferTestBase.kt b/tests/SurfaceViewBufferTests/src/com/android/test/SurfaceViewBufferTestBase.kt index bba967815ba5..1770e32a5145 100644 --- a/tests/SurfaceViewBufferTests/src/com/android/test/SurfaceViewBufferTestBase.kt +++ b/tests/SurfaceViewBufferTests/src/com/android/test/SurfaceViewBufferTestBase.kt @@ -100,4 +100,4 @@ open class SurfaceViewBufferTestBase(val useBlastAdapter: Boolean) { INVERSE_DISPLAY(0x08) } } -} +}
\ No newline at end of file diff --git a/tests/TaskOrganizerTest/Android.bp b/tests/TaskOrganizerTest/Android.bp index 9b72d359aae6..bf12f423f145 100644 --- a/tests/TaskOrganizerTest/Android.bp +++ b/tests/TaskOrganizerTest/Android.bp @@ -25,7 +25,10 @@ package { android_test { name: "TaskOrganizerTest", - srcs: ["**/*.java","**/*.kt"], + srcs: [ + "**/*.java", + "**/*.kt", + ], manifest: "AndroidManifest.xml", test_config: "AndroidTest.xml", platform_apis: true, @@ -39,6 +42,7 @@ android_test { "kotlin-stdlib", "kotlinx-coroutines-android", "flickerlib", + "flickerlib-trace_processor_shell", "truth-prebuilt", ], -}
\ No newline at end of file +} diff --git a/tests/TaskOrganizerTest/AndroidManifest.xml b/tests/TaskOrganizerTest/AndroidManifest.xml index 1f1bd3ef7d81..cbeb246eb86c 100644 --- a/tests/TaskOrganizerTest/AndroidManifest.xml +++ b/tests/TaskOrganizerTest/AndroidManifest.xml @@ -16,9 +16,11 @@ <uses-permission android:name="android.permission.CHANGE_CONFIGURATION"/> <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/> + <!-- Allow the test to connect to perfetto trace processor --> + <uses-permission android:name="android.permission.INTERNET"/> <!-- Enable / Disable tracing !--> <uses-permission android:name="android.permission.DUMP" /> - <application> + <application android:networkSecurityConfig="@xml/network_security_config"> <activity android:name="TaskOrganizerMultiWindowTest" android:label="TaskOrganizer MW Test" android:exported="true" diff --git a/tests/TaskOrganizerTest/res/xml/network_security_config.xml b/tests/TaskOrganizerTest/res/xml/network_security_config.xml new file mode 100644 index 000000000000..e450a993da28 --- /dev/null +++ b/tests/TaskOrganizerTest/res/xml/network_security_config.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +~ 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. +--> +<network-security-config> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">localhost</domain> + </domain-config> +</network-security-config>
\ No newline at end of file diff --git a/tests/TaskOrganizerTest/src/com/android/test/taskembed/ResizeTasksSyncTest.kt b/tests/TaskOrganizerTest/src/com/android/test/taskembed/ResizeTasksSyncTest.kt index 6f4f7b13af66..2c7905d552f1 100644 --- a/tests/TaskOrganizerTest/src/com/android/test/taskembed/ResizeTasksSyncTest.kt +++ b/tests/TaskOrganizerTest/src/com/android/test/taskembed/ResizeTasksSyncTest.kt @@ -22,8 +22,6 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.runner.AndroidJUnit4 import android.tools.common.datatypes.Size import android.tools.common.flicker.subject.layers.LayersTraceSubject -import android.tools.device.traces.io.ResultWriter -import android.tools.device.traces.monitors.surfaceflinger.LayersTraceMonitor import android.tools.device.traces.monitors.withSFTracing import org.junit.After import org.junit.Before @@ -41,16 +39,13 @@ class ResizeTasksSyncTest { @get:Rule var scenarioRule: ActivityScenarioRule<TaskOrganizerMultiWindowTest> = ActivityScenarioRule<TaskOrganizerMultiWindowTest>( - TaskOrganizerMultiWindowTest::class.java) + TaskOrganizerMultiWindowTest::class.java + ) protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() @Before fun setup() { - val monitor = LayersTraceMonitor() - if (monitor.isEnabled) { - monitor.stop(ResultWriter()) - } val firstTaskBounds = Rect(0, 0, 1080, 1000) val secondTaskBounds = Rect(0, 1000, 1080, 2000) @@ -71,7 +66,7 @@ class ResizeTasksSyncTest { val firstBounds = Rect(0, 0, 1080, 800) val secondBounds = Rect(0, 1000, 1080, 1800) - val trace = withSFTracing(TRACE_FLAGS) { + val trace = withSFTracing() { lateinit var resizeReadyLatch: CountDownLatch scenarioRule.getScenario().onActivity { resizeReadyLatch = it.resizeTaskView(firstBounds, secondBounds) @@ -106,7 +101,6 @@ class ResizeTasksSyncTest { } companion object { - private const val TRACE_FLAGS = 0x1 // TRACE_CRITICAL private const val FIRST_ACTIVITY = "Activity1" private const val SECOND_ACTIVITY = "Activity2" } diff --git a/tests/UiBench/Android.bp b/tests/UiBench/Android.bp index 0d2f2ef46cab..af5676df49bc 100644 --- a/tests/UiBench/Android.bp +++ b/tests/UiBench/Android.bp @@ -10,7 +10,10 @@ package { android_test { name: "UiBench", sdk_version: "current", - min_sdk_version: "21", + // As Perfetto trace recording is supported on non-rooted devices + // since Android 12. Set min/target sdk version to 31. + target_sdk_version: "31", + min_sdk_version: "31", // omit gradle 'build' dir srcs: ["src/**/*.java"], // use appcompat/support lib from the tree, so improvements/ diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java index fa5b7c15a6fe..ba9e4a831789 100644 --- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java +++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java @@ -24,7 +24,6 @@ import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; -import static org.junit.Assume.assumeTrue; import static java.util.concurrent.TimeUnit.SECONDS; @@ -36,7 +35,6 @@ import android.graphics.fonts.FontManager; import android.graphics.fonts.FontStyle; import android.os.ParcelFileDescriptor; import android.platform.test.annotations.RootPermissionTest; -import android.security.FileIntegrityManager; import android.text.FontConfig; import android.util.Log; import android.util.Pair; @@ -139,10 +137,6 @@ public class UpdatableSystemFontTest { @Before public void setUp() throws Exception { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - // Run tests only if updatable system font is enabled. - FileIntegrityManager fim = context.getSystemService(FileIntegrityManager.class); - assumeTrue(fim != null); - assumeTrue(fim.isApkVeritySupported()); mKeyId = insertCert(CERT_PATH); mFontManager = context.getSystemService(FontManager.class); expectCommandToSucceed("cmd font clear"); diff --git a/tests/UsbTests/Android.bp b/tests/UsbTests/Android.bp index 9328b67795cb..c60c519ec4d4 100644 --- a/tests/UsbTests/Android.bp +++ b/tests/UsbTests/Android.bp @@ -29,7 +29,7 @@ android_test { static_libs: [ "frameworks-base-testutils", "androidx.test.rules", - "mockito-target-inline-minus-junit4", + "mockito-target-extended-minus-junit4", "platform-test-annotations", "services.core", "services.net", @@ -37,7 +37,12 @@ android_test { "truth-prebuilt", "UsbManagerTestLib", ], - jni_libs: ["libdexmakerjvmtiagent"], + jni_libs: [ + // Required for ExtendedMockito + "libdexmakerjvmtiagent", + "libmultiplejvmtiagentsinterferenceagent", + "libstaticjvmtiagent", + ], certificate: "platform", platform_apis: true, test_suites: ["device-tests"], diff --git a/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java b/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java index 210e3ea2a9b2..c2d0f7c05b91 100644 --- a/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java +++ b/tests/UsbTests/src/com/android/server/usb/UsbHandlerTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -27,23 +28,29 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.debug.AdbManagerInternal; +import android.debug.AdbTransportType; import android.hardware.usb.UsbManager; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.provider.Settings; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.server.FgThread; +import com.android.server.LocalServices; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; import java.util.HashMap; import java.util.Locale; @@ -68,6 +75,8 @@ public class UsbHandlerTest { private SharedPreferences mSharedPreferences; @Mock private SharedPreferences.Editor mEditor; + @Mock + private AdbManagerInternal mAdbManagerInternal; private MockUsbHandler mUsbHandler; @@ -83,6 +92,7 @@ public class UsbHandlerTest { private Map<String, String> mMockProperties; private Map<String, Integer> mMockGlobalSettings; + private MockitoSession mStaticMockSession; private class MockUsbHandler extends UsbDeviceManager.UsbHandler { boolean mIsUsbTransferAllowed; @@ -157,6 +167,10 @@ public class UsbHandlerTest { @Before public void before() { MockitoAnnotations.initMocks(this); + mStaticMockSession = ExtendedMockito.mockitoSession() + .mockStatic(LocalServices.class) + .strictness(Strictness.WARN) + .startMocking(); mMockProperties = new HashMap<>(); mMockGlobalSettings = new HashMap<>(); when(mSharedPreferences.edit()).thenReturn(mEditor); @@ -164,6 +178,16 @@ public class UsbHandlerTest { mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, mUsbSettingsManager, mUsbPermissionManager); + + when(LocalServices.getService(eq(AdbManagerInternal.class))) + .thenReturn(mAdbManagerInternal); + } + + @After + public void tearDown() throws Exception { + if (mStaticMockSession != null) { + mStaticMockSession.finishMocking(); + } } @SmallTest @@ -234,8 +258,8 @@ public class UsbHandlerTest { assertEquals(mUsbHandler.getEnabledFunctions(), UsbManager.FUNCTION_NONE); assertEquals(mMockProperties.get(UsbDeviceManager.UsbHandler .USB_PERSISTENT_CONFIG_PROPERTY), UsbManager.USB_FUNCTION_ADB); - assertTrue(mUsbHandler.isAdbEnabled()); + when(mAdbManagerInternal.isAdbEnabled(eq(AdbTransportType.USB))).thenReturn(true); mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_UPDATE_STATE, 1, 1)); assertTrue(mUsbHandler.mBroadcastedIntent.getBooleanExtra(UsbManager.USB_CONNECTED, false)); @@ -271,20 +295,6 @@ public class UsbHandlerTest { @SmallTest @Test - public void bootCompletedAdbEnabled() { - mMockProperties.put(UsbDeviceManager.UsbHandler.USB_PERSISTENT_CONFIG_PROPERTY, "adb"); - mUsbHandler = new MockUsbHandler(FgThread.get().getLooper(), - InstrumentationRegistry.getContext(), mUsbDeviceManager, mUsbAlsaManager, - mUsbSettingsManager, mUsbPermissionManager); - - sendBootCompleteMessages(mUsbHandler); - assertEquals(mUsbHandler.getEnabledFunctions(), UsbManager.FUNCTION_NONE); - assertEquals(mMockGlobalSettings.get(Settings.Global.ADB_ENABLED).intValue(), 1); - assertTrue(mUsbHandler.isAdbEnabled()); - } - - @SmallTest - @Test public void userSwitchedDisablesMtp() { mUsbHandler.handleMessage(mUsbHandler.obtainMessage(MSG_SET_CURRENT_FUNCTIONS, UsbManager.FUNCTION_MTP)); diff --git a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java index ba12acb2c877..2b605c594ce8 100644 --- a/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java +++ b/tests/WindowInsetsTests/src/com/google/android/test/windowinsetstests/ChatActivity.java @@ -18,6 +18,7 @@ package com.google.android.test.windowinsetstests; import static android.view.WindowInsets.Type.ime; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; + import static java.lang.Math.max; import static java.lang.Math.min; @@ -41,11 +42,11 @@ import android.view.WindowInsetsAnimationController; import android.view.animation.LinearInterpolator; import android.widget.LinearLayout; +import androidx.appcompat.app.AppCompatActivity; + import java.util.ArrayList; import java.util.List; -import androidx.appcompat.app.AppCompatActivity; - public class ChatActivity extends AppCompatActivity { private View mRoot; @@ -148,7 +149,7 @@ public class ChatActivity extends AppCompatActivity { inset = min(inset, shown); mAnimationController.setInsetsAndAlpha( Insets.of(0, 0, 0, inset), - 1f, (inset - start) / (float)(end - start)); + 1f, start == end ? 1f : (inset - start) / (float) (end - start)); } }); diff --git a/tests/componentalias/Android.bp b/tests/componentalias/Android.bp index 7af76e1144f8..01d34e4ff645 100644 --- a/tests/componentalias/Android.bp +++ b/tests/componentalias/Android.bp @@ -26,7 +26,6 @@ java_defaults { "compatibility-device-util-axt", "mockito-target-extended-minus-junit4", "truth-prebuilt", - "ub-uiautomator", ], libs: ["android.test.base"], srcs: [ diff --git a/tests/permission/OWNERS b/tests/permission/OWNERS index 999ea0e62a0a..2fe78c5a7092 100644 --- a/tests/permission/OWNERS +++ b/tests/permission/OWNERS @@ -1 +1,2 @@ +#Bug component: 137825 include /core/java/android/permission/OWNERS diff --git a/tests/utils/testutils/TEST_MAPPING b/tests/utils/testutils/TEST_MAPPING new file mode 100644 index 000000000000..d9eb44fc4222 --- /dev/null +++ b/tests/utils/testutils/TEST_MAPPING @@ -0,0 +1,32 @@ +{ + "presubmit": [ + { + "name": "frameworks-base-testutils-tests", + "options": [ + { + "exclude-annotation": "android.platform.test.annotations.LargeTest" + }, + { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation": "org.junit.Ignore" + } + ] + } + ], + "postsubmit": [ + { + "name": "frameworks-base-testutils-tests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + } + ] + } + ] +} + diff --git a/tests/utils/testutils/java/com/android/internal/util/test/LocalServiceKeeperRule.java b/tests/utils/testutils/java/com/android/internal/util/test/LocalServiceKeeperRule.java new file mode 100644 index 000000000000..7012daf586d8 --- /dev/null +++ b/tests/utils/testutils/java/com/android/internal/util/test/LocalServiceKeeperRule.java @@ -0,0 +1,81 @@ +/* + * 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.internal.util.test; + +import com.android.server.LocalServices; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * JUnit Rule helps override /restore {@link LocalServices} state. + */ +public class LocalServiceKeeperRule implements TestRule { + + private final Map<Class<?>, Object> mOverriddenServices = new HashMap<>(); + private final List<Class<?>> mAddedServices = new ArrayList<>(); + + private volatile boolean mRuleApplied = false; + + /** + * Overrides service in LocalServices. Service will be restored to original after test run. + */ + public <T> void overrideLocalService(Class<T> type, T service) { + if (!mRuleApplied) { + throw new IllegalStateException("Can't override service without applying rule"); + } + if (mOverriddenServices.containsKey(type) || mAddedServices.contains(type)) { + throw new IllegalArgumentException("Type already overridden: " + type); + } + + T currentService = LocalServices.getService(type); + if (currentService != null) { + mOverriddenServices.put(type, currentService); + LocalServices.removeServiceForTest(type); + } else { + mAddedServices.add(type); + } + LocalServices.addService(type, service); + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + public void evaluate() throws Throwable { + try { + mRuleApplied = true; + base.evaluate(); + } finally { + mRuleApplied = false; + mAddedServices.forEach(LocalServices::removeServiceForTest); + mOverriddenServices.forEach((clazz, service) -> { + LocalServices.removeServiceForTest(clazz); + LocalServices.addService((Class) clazz, service); + }); + mAddedServices.clear(); + mOverriddenServices.clear(); + } + } + }; + } +} diff --git a/tests/utils/testutils/java/com/android/server/accessibility/TEST_MAPPING b/tests/utils/testutils/java/com/android/server/accessibility/TEST_MAPPING new file mode 100644 index 000000000000..1c673996b550 --- /dev/null +++ b/tests/utils/testutils/java/com/android/server/accessibility/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "imports": [ + { + "path": "frameworks/base/services/accessibility/TEST_MAPPING" + } + ] +} diff --git a/tests/utils/testutils/tests/Android.bp b/tests/utils/testutils/tests/Android.bp new file mode 100644 index 000000000000..b901b1802383 --- /dev/null +++ b/tests/utils/testutils/tests/Android.bp @@ -0,0 +1,47 @@ +// +// 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 { + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "frameworks-base-testutils-tests", + + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "junit", + "hamcrest-library", + "androidx.test.runner", + "mockito-target-minus-junit4", + "frameworks-base-testutils", + ], + + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + ], + + certificate: "platform", + test_suites: [ + "device-tests", + "automotive-tests", + ], +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTestCfArm.kt b/tests/utils/testutils/tests/AndroidManifest.xml index 3b5bfa986119..9e7e2752f1d5 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTestCfArm.kt +++ b/tests/utils/testutils/tests/AndroidManifest.xml @@ -1,4 +1,5 @@ -/* +<?xml version="1.0" encoding="utf-8"?> +<!-- * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,18 +13,17 @@ * 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.server.wm.flicker.ime +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.frameworks.tests.utils.testutils.tests"> -import android.tools.device.flicker.junit.FlickerParametersRunnerFactory -import android.tools.device.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized + <application> + <uses-library android:name="android.test.runner"/> + </application> -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class CloseImeOnGoHomeTestCfArm(flicker: LegacyFlickerTest) : CloseImeOnGoHomeTest(flicker) + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.frameworks.tests.utils.testutils.tests" + android:label="Test Utils Tests"/> +</manifest> diff --git a/tests/utils/testutils/java/android/os/test/TestLooperTest.java b/tests/utils/testutils/tests/src/android/os/test/TestLooperTest.java index c72e20ccc5ba..c72e20ccc5ba 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooperTest.java +++ b/tests/utils/testutils/tests/src/android/os/test/TestLooperTest.java diff --git a/tests/utils/testutils/tests/src/com/android/internal/util/test/LocalServiceKeeperRuleTest.java b/tests/utils/testutils/tests/src/com/android/internal/util/test/LocalServiceKeeperRuleTest.java new file mode 100644 index 000000000000..12f2fae0a581 --- /dev/null +++ b/tests/utils/testutils/tests/src/com/android/internal/util/test/LocalServiceKeeperRuleTest.java @@ -0,0 +1,119 @@ +/* + * 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.internal.util.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import androidx.test.filters.SmallTest; + +import com.android.server.LocalServices; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +@SmallTest +public class LocalServiceKeeperRuleTest { + + private final Description mDescription = Description.createSuiteDescription("Description"); + + LocalServiceKeeperRule mRule = new LocalServiceKeeperRule(); + + @After + public void tearDown() { + LocalServices.removeServiceForTest(TestService.class); + } + + @Test + public void testFailedIfCalledOutsideOfTheRule() { + TestService service = new TestService() {}; + + assertThrows(IllegalStateException.class, + () -> mRule.overrideLocalService(TestService.class, service)); + } + + @Test + public void testSetsLocalServiceIfNotPresent() throws Throwable { + LocalServices.removeServiceForTest(TestService.class); + TestService service = new TestService() {}; + + runInRuleApply(() -> { + mRule.overrideLocalService(TestService.class, service); + assertEquals(service, LocalServices.getService(TestService.class)); + }); + } + + @Test + public void testOverridesLocalServiceIfPresent() throws Throwable { + TestService service = new TestService() {}; + LocalServices.addService(TestService.class, service); + TestService overriddenService = new TestService() {}; + + runInRuleApply(() -> { + mRule.overrideLocalService(TestService.class, overriddenService); + assertEquals(overriddenService, LocalServices.getService(TestService.class)); + }); + } + + @Test + public void testDoesNotAllowToOverrideSameServiceTwice() throws Throwable { + TestService service = new TestService() {}; + + runInRuleApply(() -> { + mRule.overrideLocalService(TestService.class, service); + assertThrows(IllegalArgumentException.class, + () -> mRule.overrideLocalService(TestService.class, service)); + }); + } + + @Test + public void testRestroresLocalServiceAfterTestIfPresent() throws Throwable { + TestService expectedService = new TestService() {}; + LocalServices.addService(TestService.class, expectedService); + TestService overriddenService = new TestService() {}; + + runInRuleApply(() -> mRule.overrideLocalService(TestService.class, overriddenService)); + + assertEquals(expectedService, LocalServices.getService(TestService.class)); + } + + @Test + public void testRemovesLocalServiceAfterTestIfNotPresent() throws Throwable { + LocalServices.removeServiceForTest(TestService.class); + TestService service = new TestService() {}; + + runInRuleApply(() -> mRule.overrideLocalService(TestService.class, service)); + + assertNull(LocalServices.getService(TestService.class)); + } + + private void runInRuleApply(Runnable runnable) throws Throwable { + Statement testStatement = new Statement() { + @Override + public void evaluate() { + runnable.run(); + } + }; + mRule.apply(testStatement, mDescription).evaluate(); + } + + interface TestService { + } +} diff --git a/tests/utils/testutils/java/com/android/test/filters/SelectTestTests.java b/tests/utils/testutils/tests/src/com/android/test/filters/SelectTestTests.java index df18985f77bf..df18985f77bf 100644 --- a/tests/utils/testutils/java/com/android/test/filters/SelectTestTests.java +++ b/tests/utils/testutils/tests/src/com/android/test/filters/SelectTestTests.java |