diff options
Diffstat (limited to 'tests')
212 files changed, 12033 insertions, 1010 deletions
diff --git a/tests/AppJankTest/Android.bp b/tests/AppJankTest/Android.bp new file mode 100644 index 000000000000..acf8dc9aca47 --- /dev/null +++ b/tests/AppJankTest/Android.bp @@ -0,0 +1,37 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package { + // 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: "CoreAppJankTestCases", + team: "trendy_team_system_performance", + srcs: ["src/**/*.java"], + static_libs: [ + "androidx.test.rules", + "androidx.test.core", + "platform-test-annotations", + "flag-junit", + ], + platform_apis: true, + test_suites: ["device-tests"], + certificate: "platform", +} diff --git a/tests/AppJankTest/AndroidManifest.xml b/tests/AppJankTest/AndroidManifest.xml new file mode 100644 index 000000000000..ae973393b90e --- /dev/null +++ b/tests/AppJankTest/AndroidManifest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.app.jank.tests"> + + <application> + <uses-library android:name="android.test.runner" /> + <activity android:name=".EmptyActivity" + android:label="EmptyActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/> + </intent-filter> + </activity> + </application> + + <!-- self-instrumenting test package. --> + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="android.app.jank.tests" + android:label="Core tests of App Jank Tracking"> + </instrumentation> + +</manifest>
\ No newline at end of file diff --git a/tests/AppJankTest/AndroidTest.xml b/tests/AppJankTest/AndroidTest.xml new file mode 100644 index 000000000000..c01c75c9695c --- /dev/null +++ b/tests/AppJankTest/AndroidTest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<configuration description="Config for Core App Jank Tests"> + <option name="test-suite-tag" value="apct"/> + + <option name="config-descriptor:metadata" key="component" value="systems"/> + <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" /> + <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states" /> + + <option name="not-shardable" value="true" /> + <option name="install-arg" value="-t" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="CoreAppJankTestCases.apk"/> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="android.app.jank.tests"/> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/> + </test> +</configuration>
\ No newline at end of file diff --git a/tests/AppJankTest/OWNERS b/tests/AppJankTest/OWNERS new file mode 100644 index 000000000000..806de574b071 --- /dev/null +++ b/tests/AppJankTest/OWNERS @@ -0,0 +1,4 @@ +steventerrell@google.com +carmenjackson@google.com +jjaggi@google.com +pmuetschard@google.com
\ No newline at end of file diff --git a/tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java b/tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java new file mode 100644 index 000000000000..b326765ab097 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.jank.tests; + +import android.app.Activity; + +public class EmptyActivity extends Activity { +} diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java new file mode 100644 index 000000000000..2cd625eec032 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.jank.tests; + +import static org.junit.Assert.assertEquals; + +import android.app.jank.Flags; +import android.app.jank.JankDataProcessor; +import android.app.jank.StateTracker; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.Choreographer; +import android.view.SurfaceControl; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.core.app.ActivityScenario; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class JankDataProcessorTest { + + private Choreographer mChoreographer; + private StateTracker mStateTracker; + private JankDataProcessor mJankDataProcessor; + private static final int NANOS_PER_MS = 1_000_000; + private static String sActivityName; + private static ActivityScenario<EmptyActivity> sEmptyActivityActivityScenario; + private static final int APP_ID = 25; + + @BeforeClass + public static void classSetup() { + sEmptyActivityActivityScenario = ActivityScenario.launch(EmptyActivity.class); + sActivityName = sEmptyActivityActivityScenario.toString(); + } + + @AfterClass + public static void classTearDown() { + sEmptyActivityActivityScenario.close(); + } + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Before + @UiThreadTest + public void setup() { + mChoreographer = Choreographer.getInstance(); + mStateTracker = new StateTracker(mChoreographer); + mJankDataProcessor = new JankDataProcessor(mStateTracker); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_multipleFramesAndStates_attributesTotalFramesCorrectly() { + List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); + mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); + + mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); + + long totalFramesAttributed = getTotalFramesCounted(); + + // Each state is active for each frame that is passed in, there are two states being tested + // which is why jankData.size is multiplied by 2. + assertEquals(jankData.size() * 2, totalFramesAttributed); + } + + /** + * Each JankData frame has an associated vsyncid, only frames that have vsyncids between the + * StatData start and end vsyncids should be counted. This test confirms that if JankData + * does not share any frames with the states then no jank stats are added. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_outOfRangeVsyncId_skipOutOfRangeVsyncIds() { + List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); + mStateTracker.addPendingStateData(getMockStateData_vsyncId_outOfRange()); + + mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); + + assertEquals(0, mJankDataProcessor.getPendingJankStats().size()); + } + + /** + * It's expected to see many duplicate widget states, if a user is scrolling then + * pauses and resumes scrolling again, we may get three widget states two of which are the same. + * State 1: {Scroll,WidgetId,Scrolling} State 2: {Scroll,WidgetId,None} + * State 3: {Scroll,WidgetId,Scrolling} + * These duplicate states should coalesce into only one Jank stat. This test confirms that + * behavior. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_duplicateStates_confirmDuplicatesCoalesce() { + // getMockStateData will return 10 states 5 of which are set to none and 5 of which are + // scrolling. + mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); + + mJankDataProcessor.processJankData(getMockJankData_vsyncId_inRange(), sActivityName, + APP_ID); + + // Confirm the duplicate states are coalesced down to 2 stats 1 for the scrolling state + // another for the none state. + assertEquals(2, mJankDataProcessor.getPendingJankStats().size()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_inRangeVsyncIds_confirmOnlyInRangeFramesCounted() { + List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); + int inRangeFrameCount = jankData.size(); + + mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); + mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); + + // Two states are active for each frame which is why inRangeFrameCount is multiplied by 2. + assertEquals(inRangeFrameCount * 2, getTotalFramesCounted()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_inRangeVsyncIds_confirmHistogramCountMatchesFrameCount() { + List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); + mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); + mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); + + long totalFrames = getTotalFramesCounted(); + long histogramFrames = getHistogramFrameCount(); + + assertEquals(totalFrames, histogramFrames); + } + + // TODO b/375005277 add tests that cover logging and releasing resources back to pool. + + private long getTotalFramesCounted() { + return mJankDataProcessor.getPendingJankStats().values() + .stream().mapToLong(stat -> stat.getTotalFrames()).sum(); + } + + private long getHistogramFrameCount() { + long totalHistogramFrames = 0; + + for (JankDataProcessor.PendingJankStat stats : + mJankDataProcessor.getPendingJankStats().values()) { + int[] overrunHistogram = stats.getFrameOverrunBuckets(); + + for (int i = 0; i < overrunHistogram.length; i++) { + totalHistogramFrames += overrunHistogram[i]; + } + } + + return totalHistogramFrames; + } + + /** + * Out of range data will have a mVsyncIdStart and mVsyncIdEnd values set to below 25. + */ + private List<StateTracker.StateData> getMockStateData_vsyncId_outOfRange() { + ArrayList<StateTracker.StateData> stateData = new ArrayList<StateTracker.StateData>(); + StateTracker.StateData newStateData = new StateTracker.StateData(); + newStateData.mVsyncIdEnd = 20; + newStateData.mStateDataKey = "Test1_OutBand"; + newStateData.mVsyncIdStart = 1; + newStateData.mWidgetState = "scrolling"; + newStateData.mWidgetId = "widgetId"; + newStateData.mWidgetCategory = "Scroll"; + stateData.add(newStateData); + + newStateData = new StateTracker.StateData(); + newStateData.mVsyncIdEnd = 24; + newStateData.mStateDataKey = "Test1_InBand"; + newStateData.mVsyncIdStart = 20; + newStateData.mWidgetState = "Idle"; + newStateData.mWidgetId = "widgetId"; + newStateData.mWidgetCategory = "Scroll"; + stateData.add(newStateData); + + newStateData = new StateTracker.StateData(); + newStateData.mVsyncIdEnd = 20; + newStateData.mStateDataKey = "Test1_OutBand"; + newStateData.mVsyncIdStart = 12; + newStateData.mWidgetState = "Idle"; + newStateData.mWidgetId = "widgetId"; + newStateData.mWidgetCategory = "Scroll"; + stateData.add(newStateData); + + return stateData; + } + + /** + * This method returns two unique states, one state is set to scrolling the other is set + * to none. Both states will have the same startvsyncid to ensure each state is counted the same + * number of times. This keeps logic in asserts easier to reason about. Both states will have + * a startVsyncId between 25 and 35. + */ + private List<StateTracker.StateData> getMockStateData_vsyncId_inRange() { + ArrayList<StateTracker.StateData> stateData = new ArrayList<StateTracker.StateData>(); + + for (int i = 0; i < 10; i++) { + StateTracker.StateData newStateData = new StateTracker.StateData(); + newStateData.mVsyncIdEnd = Long.MAX_VALUE; + newStateData.mStateDataKey = "Test1_" + (i % 2 == 0 ? "scrolling" : "none"); + // Divide i by two to ensure both the scrolling and none states get the same vsyncid + // This makes asserts in tests easier to reason about as each state should be counted + // the same number of times. + newStateData.mVsyncIdStart = 25 + (i / 2); + newStateData.mWidgetState = i % 2 == 0 ? "scrolling" : "none"; + newStateData.mWidgetId = "widgetId"; + newStateData.mWidgetCategory = "Scroll"; + + stateData.add(newStateData); + } + + return stateData; + } + + /** + * In range data will have a frameVsyncId value between 25 and 35. + */ + private List<SurfaceControl.JankData> getMockJankData_vsyncId_inRange() { + ArrayList<SurfaceControl.JankData> mockData = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + mockData.add(new SurfaceControl.JankData( + /*frameVsyncId*/25 + i, + SurfaceControl.JankData.JANK_NONE, + NANOS_PER_MS * ((long) i), + NANOS_PER_MS * ((long) i), + NANOS_PER_MS * ((long) i))); + + } + + return mockData; + } + + /** + * Out of range data will have frameVsyncId values below 25. + */ + private List<SurfaceControl.JankData> getMockJankData_vsyncId_outOfRange() { + ArrayList<SurfaceControl.JankData> mockData = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + mockData.add(new SurfaceControl.JankData( + /*frameVsyncId*/i, + SurfaceControl.JankData.JANK_NONE, + NANOS_PER_MS * ((long) i), + NANOS_PER_MS * ((long) i), + NANOS_PER_MS * ((long) i))); + + } + + return mockData; + } + +} diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java new file mode 100644 index 000000000000..a3e5533599bc --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.jank.tests; + +import static org.junit.Assert.assertEquals; + +import android.app.jank.Flags; +import android.app.jank.JankTracker; +import android.app.jank.StateTracker; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.Choreographer; +import android.view.View; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.core.app.ActivityScenario; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; + +@RunWith(AndroidJUnit4.class) +public class JankTrackerTest { + private Choreographer mChoreographer; + private JankTracker mJankTracker; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + /** + * Start an empty activity so decore view is not null when creating the JankTracker instance. + */ + private static ActivityScenario<EmptyActivity> sEmptyActivityRule; + + private static String sActivityName; + + private static View sActivityDecorView; + + @BeforeClass + public static void classSetup() { + sEmptyActivityRule = ActivityScenario.launch(EmptyActivity.class); + sEmptyActivityRule.onActivity(activity -> { + sActivityDecorView = activity.getWindow().getDecorView(); + sActivityName = activity.toString(); + }); + } + + @AfterClass + public static void classTearDown() { + sEmptyActivityRule.close(); + } + + @Before + @UiThreadTest + public void setup() { + mChoreographer = Choreographer.getInstance(); + mJankTracker = new JankTracker(mChoreographer, sActivityDecorView); + mJankTracker.setActivityName(sActivityName); + } + + /** + * When jank tracking is enabled the activity name should be added as a state to associate + * frames to it. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void jankTracking_WhenEnabled_ActivityAdded() { + mJankTracker.enableAppJankTracking(); + + ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); + mJankTracker.getAllUiStates(stateData); + + assertEquals(1, stateData.size()); + + StateTracker.StateData firstState = stateData.getFirst(); + + assertEquals(sActivityName, firstState.mWidgetId); + } + + /** + * No states should be added when tracking is disabled. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void jankTrackingDisabled_StatesShouldNot_BeAddedToTracker() { + mJankTracker.disableAppJankTracking(); + + mJankTracker.addUiState("FAKE_CATEGORY", "FAKE_ID", + "FAKE_STATE"); + + ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); + mJankTracker.getAllUiStates(stateData); + + assertEquals(0, stateData.size()); + } + + /** + * The activity name as well as the test state should be added for frame association. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void jankTrackingEnabled_StatesShould_BeAddedToTracker() { + mJankTracker.forceListenerRegistration(); + + mJankTracker.enableAppJankTracking(); + mJankTracker.addUiState("FAKE_CATEGORY", "FAKE_ID", + "FAKE_STATE"); + + ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); + mJankTracker.getAllUiStates(stateData); + + assertEquals(2, stateData.size()); + } + + /** + * Activity state should only be added once even if jank tracking is enabled multiple times. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void jankTrackingEnabled_EnabledCalledTwice_ActivityStateOnlyAddedOnce() { + mJankTracker.enableAppJankTracking(); + + ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); + mJankTracker.getAllUiStates(stateData); + + assertEquals(1, stateData.size()); + + stateData.clear(); + + mJankTracker.enableAppJankTracking(); + mJankTracker.getAllUiStates(stateData); + + assertEquals(1, stateData.size()); + } +} diff --git a/tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java b/tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java new file mode 100644 index 000000000000..541009e05e55 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.jank.tests; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.app.jank.Flags; +import android.app.jank.StateTracker; +import android.app.jank.StateTracker.StateData; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.Choreographer; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.core.app.ActivityScenario; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; + + +@RunWith(AndroidJUnit4.class) +public class StateTrackerTest { + + private static final String WIDGET_CATEGORY_NONE = "None"; + private static final String WIDGET_CATEGORY_SCROLL = "Scroll"; + private static final String WIDGET_STATE_IDLE = "Idle"; + private static final String WIDGET_STATE_SCROLLING = "Scrolling"; + private StateTracker mStateTracker; + private Choreographer mChoreographer; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + /** + * Start an empty activity so choreographer won't return -1 for vsyncid. + */ + private static ActivityScenario<EmptyActivity> sEmptyActivityRule; + + @BeforeClass + public static void classSetup() { + sEmptyActivityRule = ActivityScenario.launch(EmptyActivity.class); + } + + @AfterClass + public static void classTearDown() { + sEmptyActivityRule.close(); + } + + @Before + @UiThreadTest + public void setup() { + mChoreographer = Choreographer.getInstance(); + mStateTracker = new StateTracker(mChoreographer); + } + + /** + * Check that the start vsyncid is added when the state is first added and end vsyncid is + * set to the default value, indicating it has not been updated. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void addWidgetState_VerifyStateHasStartVsyncId() { + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addWidgetState_VerifyStateHasStartVsyncId"); + + ArrayList<StateData> stateList = new ArrayList<StateData>(); + mStateTracker.retrieveAllStates(stateList); + StateData stateData = stateList.get(0); + + assertTrue(stateData.mVsyncIdStart > 0); + assertTrue(stateData.mVsyncIdEnd == Long.MAX_VALUE); + } + + /** + * Check that the end vsyncid is added when the state is removed. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void removeWidgetState_VerifyStateHasEndVsyncId() { + + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "removeWidgetState_VerifyStateHasEndVsyncId"); + mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "removeWidgetState_VerifyStateHasEndVsyncId"); + + ArrayList<StateData> stateList = new ArrayList<StateData>(); + mStateTracker.retrieveAllStates(stateList); + StateData stateData = stateList.get(0); + + assertTrue(stateData.mVsyncIdStart > 0); + assertTrue(stateData.mVsyncIdEnd != Long.MAX_VALUE); + } + + /** + * Check that duplicate states are aggregated into only one active instance. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void addDuplicateStates_ConfirmStateCountOnlyOne() { + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addDuplicateStates_ConfirmStateCountOnlyOne"); + + ArrayList<StateData> stateList = new ArrayList<>(); + mStateTracker.retrieveAllStates(stateList); + + assertEquals(stateList.size(), 1); + + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addDuplicateStates_ConfirmStateCountOnlyOne"); + + stateList.clear(); + + mStateTracker.retrieveAllStates(stateList); + + assertEquals(stateList.size(), 1); + } + + /** + * Check that correct distinct states are returned when all states are retrieved. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void addThreeStateChanges_ConfirmThreeStatesReturned() { + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addThreeStateChanges_ConfirmThreeStatesReturned"); + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addThreeStateChanges_ConfirmThreeStatesReturned_01"); + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addThreeStateChanges_ConfirmThreeStatesReturned_02"); + + ArrayList<StateData> stateList = new ArrayList<>(); + mStateTracker.retrieveAllStates(stateList); + + assertEquals(stateList.size(), 3); + } + + /** + * Confirm when states are added and removed the removed states are moved to the previousStates + * list and returned when retrieveAllStates is called. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void simulateAddingSeveralStates() { + for (int i = 0; i < 20; i++) { + mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i - 1)); + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i)); + } + + ArrayList<StateData> stateList = new ArrayList<>(); + mStateTracker.retrieveAllStates(stateList); + + int countStatesWithEndVsync = 0; + for (int i = 0; i < stateList.size(); i++) { + if (stateList.get(i).mVsyncIdEnd != Long.MAX_VALUE) { + countStatesWithEndVsync++; + } + } + + // The last state that was added would be an active state and should not have an associated + // end vsyncid. + assertEquals(19, countStatesWithEndVsync); + } + + /** + * Confirm once a state has been attributed to a frame it has been removed from the previous + * state list. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void confirmProcessedStates_RemovedFromPreviousStateList() { + for (int i = 0; i < 20; i++) { + mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i - 1)); + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i)); + + if (i == 19) { + mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i)); + } + } + + ArrayList<StateData> stateList = new ArrayList<>(); + mStateTracker.retrieveAllStates(stateList); + + assertEquals(20, stateList.size()); + + // Simulate processing all the states. + for (int i = 0; i < stateList.size(); i++) { + stateList.get(i).mProcessed = true; + } + // Clear out all processed states. + mStateTracker.stateProcessingComplete(); + + stateList.clear(); + + mStateTracker.retrieveAllStates(stateList); + + assertEquals(0, stateList.size()); + } +} diff --git a/tests/AttestationVerificationTest/src/android/security/attestationverification/PeerDeviceSystemAttestationVerificationTest.kt b/tests/AttestationVerificationTest/src/android/security/attestationverification/PeerDeviceSystemAttestationVerificationTest.kt index ad95fbc36867..88ebf3edc7ed 100644 --- a/tests/AttestationVerificationTest/src/android/security/attestationverification/PeerDeviceSystemAttestationVerificationTest.kt +++ b/tests/AttestationVerificationTest/src/android/security/attestationverification/PeerDeviceSystemAttestationVerificationTest.kt @@ -2,10 +2,11 @@ package android.security.attestationverification import android.app.Activity import android.os.Bundle +import android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_CERTS +import android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS import android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE import android.security.attestationverification.AttestationVerificationManager.PARAM_PUBLIC_KEY import android.security.attestationverification.AttestationVerificationManager.PROFILE_PEER_DEVICE -import android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE import android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE import android.security.attestationverification.AttestationVerificationManager.TYPE_PUBLIC_KEY import android.security.attestationverification.AttestationVerificationManager.TYPE_UNKNOWN @@ -54,7 +55,7 @@ class PeerDeviceSystemAttestationVerificationTest { future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } @Test @@ -66,7 +67,7 @@ class PeerDeviceSystemAttestationVerificationTest { future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } @Test @@ -80,7 +81,7 @@ class PeerDeviceSystemAttestationVerificationTest { future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) val future2 = CompletableFuture<Int>() val challengeRequirements = Bundle() @@ -90,7 +91,7 @@ class PeerDeviceSystemAttestationVerificationTest { future2.complete(result) } - assertThat(future2.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future2.getSoon()).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } @Test @@ -104,7 +105,7 @@ class PeerDeviceSystemAttestationVerificationTest { future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } @Test @@ -118,7 +119,7 @@ class PeerDeviceSystemAttestationVerificationTest { future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_CERTS) } @Test @@ -131,7 +132,7 @@ class PeerDeviceSystemAttestationVerificationTest { invalidAttestationByteArray, activity.mainExecutor) { result, _ -> future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_CERTS) } private fun <T> CompletableFuture<T>.getSoon(): T { diff --git a/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt b/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt index 8f06b4a2ea0a..e77364de8747 100644 --- a/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt +++ b/tests/AttestationVerificationTest/src/android/security/attestationverification/SystemAttestationVerificationTest.kt @@ -2,6 +2,9 @@ package android.security.attestationverification import android.os.Bundle import android.app.Activity +import android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_CERTS +import android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS +import android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_UNSUPPORTED_PROFILE import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -14,9 +17,6 @@ import com.google.common.truth.Truth.assertThat import android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE import android.security.attestationverification.AttestationVerificationManager.PROFILE_SELF_TRUSTED import android.security.attestationverification.AttestationVerificationManager.PROFILE_UNKNOWN -import android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE -import android.security.attestationverification.AttestationVerificationManager.RESULT_SUCCESS -import android.security.attestationverification.AttestationVerificationManager.RESULT_UNKNOWN import android.security.attestationverification.AttestationVerificationManager.TYPE_PUBLIC_KEY import android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE import android.security.keystore.KeyGenParameterSpec @@ -58,19 +58,19 @@ class SystemAttestationVerificationTest { future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_UNKNOWN) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_UNSUPPORTED_PROFILE) } @Test fun verifyAttestation_returnsFailureWithEmptyAttestation() { val future = CompletableFuture<Int>() - val profile = AttestationProfile(PROFILE_SELF_TRUSTED) - avm.verifyAttestation(profile, TYPE_CHALLENGE, Bundle(), ByteArray(0), - activity.mainExecutor) { result, _ -> + val selfTrusted = TestSelfTrustedAttestation("test", "challengeStr") + avm.verifyAttestation(selfTrusted.profile, selfTrusted.localBindingType, + selfTrusted.requirements, ByteArray(0), activity.mainExecutor) { result, _ -> future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_CERTS) } @Test @@ -81,7 +81,7 @@ class SystemAttestationVerificationTest { Bundle(), selfTrusted.attestation, activity.mainExecutor) { result, _ -> future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } @Test @@ -92,7 +92,7 @@ class SystemAttestationVerificationTest { selfTrusted.requirements, selfTrusted.attestation, activity.mainExecutor) { result, _ -> future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } @Test @@ -106,7 +106,7 @@ class SystemAttestationVerificationTest { wrongKeyRequirements, selfTrusted.attestation, activity.mainExecutor) { result, _ -> future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } @Test @@ -119,7 +119,7 @@ class SystemAttestationVerificationTest { wrongChallengeRequirements, selfTrusted.attestation, activity.mainExecutor) { result, _ -> future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_FAILURE) + assertThat(future.getSoon()).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } // TODO(b/216144791): Add more failure tests for PROFILE_SELF_TRUSTED. @@ -131,20 +131,7 @@ class SystemAttestationVerificationTest { selfTrusted.requirements, selfTrusted.attestation, activity.mainExecutor) { result, _ -> future.complete(result) } - assertThat(future.getSoon()).isEqualTo(RESULT_SUCCESS) - } - - @Test - fun verifyToken_returnsUnknown() { - val future = CompletableFuture<Int>() - val profile = AttestationProfile(PROFILE_SELF_TRUSTED) - avm.verifyAttestation(profile, TYPE_PUBLIC_KEY, Bundle(), ByteArray(0), - activity.mainExecutor) { _, token -> - val result = avm.verifyToken(profile, TYPE_PUBLIC_KEY, Bundle(), token, null) - future.complete(result) - } - - assertThat(future.getSoon()).isEqualTo(RESULT_UNKNOWN) + assertThat(future.getSoon()).isEqualTo(0) } @Test diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/AttestationVerificationPeerDeviceVerifierTest.kt b/tests/AttestationVerificationTest/src/com/android/server/security/AttestationVerificationPeerDeviceVerifierTest.kt index afb3593e3e98..4d1a1a55af74 100644 --- a/tests/AttestationVerificationTest/src/com/android/server/security/AttestationVerificationPeerDeviceVerifierTest.kt +++ b/tests/AttestationVerificationTest/src/com/android/server/security/AttestationVerificationPeerDeviceVerifierTest.kt @@ -3,10 +3,12 @@ package com.android.server.security import android.app.Activity import android.content.Context import android.os.Bundle +import android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_CERTS +import android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS +import android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_PATCH_LEVEL_DIFF import android.security.attestationverification.AttestationVerificationManager.PARAM_CHALLENGE +import android.security.attestationverification.AttestationVerificationManager.PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS import android.security.attestationverification.AttestationVerificationManager.PARAM_PUBLIC_KEY -import android.security.attestationverification.AttestationVerificationManager.RESULT_FAILURE -import android.security.attestationverification.AttestationVerificationManager.RESULT_SUCCESS import android.security.attestationverification.AttestationVerificationManager.TYPE_CHALLENGE import android.security.attestationverification.AttestationVerificationManager.TYPE_PUBLIC_KEY import android.util.IndentingPrintWriter @@ -71,7 +73,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TYPE_CHALLENGE, challengeRequirements, TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_SUCCESS) + assertThat(result).isEqualTo(0) } @Test @@ -87,7 +89,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TYPE_CHALLENGE, challengeRequirements, TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_SUCCESS) + assertThat(result).isEqualTo(0) } @Test @@ -107,7 +109,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TYPE_PUBLIC_KEY, pkRequirements, TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_SUCCESS) + assertThat(result).isEqualTo(0) } @Test @@ -125,7 +127,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TEST_OWNED_BY_SYSTEM_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_SUCCESS) + assertThat(result).isEqualTo(0) } @Test @@ -142,7 +144,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TYPE_CHALLENGE, challengeRequirements, TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_FAILURE) + assertThat(result).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } @Test @@ -158,7 +160,60 @@ class AttestationVerificationPeerDeviceVerifierTest { TYPE_CHALLENGE, challengeRequirements, TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_FAILURE) + assertThat(result).isEqualTo(FLAG_FAILURE_PATCH_LEVEL_DIFF) + } + + @Test + fun verifyAttestation_returnsSuccessPatchDataWithinMaxPatchDiff() { + val verifier = AttestationVerificationPeerDeviceVerifier( + context, dumpLogger, trustAnchors, false, LocalDate.of(2023, 3, 1), + LocalDate.of(2023, 2, 1) + ) + val challengeRequirements = Bundle() + challengeRequirements.putByteArray(PARAM_CHALLENGE, "player456".encodeToByteArray()) + challengeRequirements.putInt(PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS, 24) + + val result = verifier.verifyAttestation( + TYPE_CHALLENGE, challengeRequirements, + TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() + ) + assertThat(result).isEqualTo(0) + } + + @Test + fun verifyAttestation_returnsFailurePatchDataNotWithinMaxPatchDiff() { + val verifier = AttestationVerificationPeerDeviceVerifier( + context, dumpLogger, trustAnchors, false, LocalDate.of(2024, 10, 1), + LocalDate.of(2024, 9, 1) + ) + val challengeRequirements = Bundle() + challengeRequirements.putByteArray(PARAM_CHALLENGE, "player456".encodeToByteArray()) + challengeRequirements.putInt(PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS, 24) + + val result = verifier.verifyAttestation( + TYPE_CHALLENGE, challengeRequirements, + TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() + ) + assertThat(result).isEqualTo(FLAG_FAILURE_PATCH_LEVEL_DIFF) + } + + @Test + fun verifyAttestation_returnsFailureOwnedBySystemAndPatchDataNotWithinMaxPatchDiff() { + val verifier = AttestationVerificationPeerDeviceVerifier( + context, dumpLogger, trustAnchors, false, LocalDate.of(2024, 10, 1), + LocalDate.of(2024, 9, 1) + ) + val challengeRequirements = Bundle() + challengeRequirements.putByteArray(PARAM_CHALLENGE, "player456".encodeToByteArray()) + challengeRequirements.putBoolean("android.key_owned_by_system", true) + challengeRequirements.putInt(PARAM_MAX_PATCH_LEVEL_DIFF_MONTHS, 24) + + val result = verifier.verifyAttestation( + TYPE_CHALLENGE, challengeRequirements, + TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() + ) + // Both "owned by system" and "patch level diff" checks should fail. + assertThat(result).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS or FLAG_FAILURE_PATCH_LEVEL_DIFF) } @Test @@ -174,7 +229,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TYPE_CHALLENGE, challengeRequirements, TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_FAILURE) + assertThat(result).isEqualTo(FLAG_FAILURE_CERTS) } @Test @@ -196,7 +251,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TYPE_CHALLENGE, challengeRequirements, TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_FAILURE) + assertThat(result).isEqualTo(FLAG_FAILURE_CERTS) } fun verifyAttestation_returnsFailureChallenge() { @@ -211,7 +266,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TYPE_CHALLENGE, challengeRequirements, TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_FAILURE) + assertThat(result).isEqualTo(FLAG_FAILURE_LOCAL_BINDING_REQUIREMENTS) } private fun String.fromPEMFileToCerts(): Collection<Certificate> { @@ -245,6 +300,7 @@ class AttestationVerificationPeerDeviceVerifierTest { companion object { private const val TAG = "AVFTest" private const val TEST_ROOT_CERT_FILENAME = "test_root_certs.pem" + // Local patch date is 20220105 private const val TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME = "test_attestation_with_root_certs.pem" private const val TEST_ATTESTATION_CERT_FILENAME = "test_attestation_wrong_root_certs.pem" diff --git a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java index 08430f2f2744..30cc002b4144 100644 --- a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java +++ b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java @@ -159,7 +159,7 @@ public class BatteryUsageStatsPerfTest { private static BatteryUsageStats buildBatteryUsageStats() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, 0) + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, false, false, 0) .setBatteryCapacity(4000) .setDischargePercentage(20) .setDischargedPowerRange(1000, 2000) @@ -171,11 +171,11 @@ public class BatteryUsageStatsPerfTest { .setConsumedPower(123) .setConsumedPower( BatteryConsumer.POWER_COMPONENT_CPU, 10100) - .setConsumedPowerForCustomComponent( + .setConsumedPower( BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10200) .setUsageDurationMillis( BatteryConsumer.POWER_COMPONENT_CPU, 10300) - .setUsageDurationForCustomComponentMillis( + .setUsageDurationMillis( BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10400); for (int i = 0; i < 1000; i++) { @@ -191,10 +191,9 @@ public class BatteryUsageStatsPerfTest { consumerBuilder.setUsageDurationMillis(componentId, componentId * 1000); } - consumerBuilder.setConsumedPowerForCustomComponent( - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 1234) - .setUsageDurationForCustomComponentMillis( - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 4321); + consumerBuilder + .setConsumedPower(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 1234) + .setUsageDurationMillis(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 4321); } return builder.build(); } diff --git a/tests/BootImageProfileTest/Android.bp b/tests/BootImageProfileTest/Android.bp index 9fb5aa21f985..dbdc4b4407b7 100644 --- a/tests/BootImageProfileTest/Android.bp +++ b/tests/BootImageProfileTest/Android.bp @@ -19,6 +19,7 @@ package { // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_art_mainline", } java_test_host { diff --git a/tests/FlickerTests/ActivityEmbedding/Android.bp b/tests/FlickerTests/ActivityEmbedding/Android.bp index e09fbf6adc02..529f84ac4e90 100644 --- a/tests/FlickerTests/ActivityEmbedding/Android.bp +++ b/tests/FlickerTests/ActivityEmbedding/Android.bp @@ -24,61 +24,116 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -filegroup { - name: "FlickerTestsOtherCommon-src", - srcs: ["src/**/ActivityEmbeddingTestBase.kt"], +android_test { + name: "FlickerTestsActivityEmbedding", + defaults: ["FlickerTestsDefault"], + manifest: "AndroidManifest.xml", + package_name: "com.android.server.wm.flicker", + instrumentation_target_package: "com.android.server.wm.flicker", + test_config_template: "AndroidTestTemplate.xml", + srcs: ["src/**/*"], + static_libs: ["FlickerTestsBase"], + data: ["trace_config/*"], } -filegroup { - name: "FlickerTestsOtherOpen-src", - srcs: ["src/**/open/*"], +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for FlickerTestsActivityEmbedding module + +test_module_config { + name: "FlickerTestsActivityEmbedding-CatchAll", + base: "FlickerTestsActivityEmbedding", + exclude_filters: [ + "com.android.server.wm.flicker.activityembedding.close.CloseSecondaryActivityInSplitTest", + "com.android.server.wm.flicker.activityembedding.layoutchange.HorizontalSplitChangeRatioTest", + "com.android.server.wm.flicker.activityembedding.open.MainActivityStartsSecondaryWithAlwaysExpandTest", + "com.android.server.wm.flicker.activityembedding.open.OpenActivityEmbeddingPlaceholderSplitTest", + "com.android.server.wm.flicker.activityembedding.open.OpenActivityEmbeddingSecondaryToSplitTest", + "com.android.server.wm.flicker.activityembedding.open.OpenThirdActivityOverSplitTest", + "com.android.server.wm.flicker.activityembedding.open.OpenTrampolineActivityTest", + "com.android.server.wm.flicker.activityembedding.pip.SecondaryActivityEnterPipTest", + "com.android.server.wm.flicker.activityembedding.rotation.RotateSplitNoChangeTest", + "com.android.server.wm.flicker.activityembedding.rtl.RTLStartSecondaryWithPlaceholderTest", + "com.android.server.wm.flicker.activityembedding.splitscreen.EnterSystemSplitTest", + ], + test_suites: ["device-tests"], } -filegroup { - name: "FlickerTestsOtherRotation-src", - srcs: ["src/**/rotation/*"], +test_module_config { + name: "FlickerTestsActivityEmbedding-Close-CloseSecondaryActivityInSplitTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.close.CloseSecondaryActivityInSplitTest"], + test_suites: ["device-tests"], } -java_library { - name: "FlickerTestsOtherCommon", - defaults: ["FlickerTestsDefault"], - srcs: [":FlickerTestsOtherCommon-src"], - static_libs: ["FlickerTestsBase"], +test_module_config { + name: "FlickerTestsActivityEmbedding-LayoutChange-HorizontalSplitChangeRatioTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.layoutchange.HorizontalSplitChangeRatioTest"], + test_suites: ["device-tests"], } -java_defaults { - name: "FlickerTestsOtherDefaults", - defaults: ["FlickerTestsDefault"], - manifest: "AndroidManifest.xml", - package_name: "com.android.server.wm.flicker", - instrumentation_target_package: "com.android.server.wm.flicker", - test_config_template: "AndroidTestTemplate.xml", - static_libs: [ - "FlickerTestsBase", - "FlickerTestsOtherCommon", - ], - data: ["trace_config/*"], +test_module_config { + name: "FlickerTestsActivityEmbedding-Open-MainActivityStartsSecondaryWithAlwaysExpandTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.open.MainActivityStartsSecondaryWithAlwaysExpandTest"], + test_suites: ["device-tests"], } -android_test { - name: "FlickerTestsOtherOpen", - defaults: ["FlickerTestsOtherDefaults"], - srcs: [":FlickerTestsOtherOpen-src"], +test_module_config { + name: "FlickerTestsActivityEmbedding-Open-OpenActivityEmbeddingPlaceholderSplitTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.open.OpenActivityEmbeddingPlaceholderSplitTest"], + test_suites: ["device-tests"], } -android_test { - name: "FlickerTestsOtherRotation", - defaults: ["FlickerTestsOtherDefaults"], - srcs: [":FlickerTestsOtherRotation-src"], +test_module_config { + name: "FlickerTestsActivityEmbedding-Open-OpenActivityEmbeddingSecondaryToSplitTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.open.OpenActivityEmbeddingSecondaryToSplitTest"], + test_suites: ["device-tests"], } -android_test { - name: "FlickerTestsOther", - defaults: ["FlickerTestsOtherDefaults"], - srcs: ["src/**/*"], - exclude_srcs: [ - ":FlickerTestsOtherOpen-src", - ":FlickerTestsOtherRotation-src", - ":FlickerTestsOtherCommon-src", - ], +test_module_config { + name: "FlickerTestsActivityEmbedding-Open-OpenThirdActivityOverSplitTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.open.OpenThirdActivityOverSplitTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsActivityEmbedding-Open-OpenTrampolineActivityTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.open.OpenTrampolineActivityTest"], + test_suites: ["device-tests"], } + +test_module_config { + name: "FlickerTestsActivityEmbedding-Pip-SecondaryActivityEnterPipTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.pip.SecondaryActivityEnterPipTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsActivityEmbedding-Rotation-RotateSplitNoChangeTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.rotation.RotateSplitNoChangeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsActivityEmbedding-Rtl-RTLStartSecondaryWithPlaceholderTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.rtl.RTLStartSecondaryWithPlaceholderTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsActivityEmbedding-SplitScreen-EnterSystemSplitTest", + base: "FlickerTestsActivityEmbedding", + include_filters: ["com.android.server.wm.flicker.activityembedding.splitscreen.EnterSystemSplitTest"], + test_suites: ["device-tests"], +} + +// End breakdowns for FlickerTestsActivityEmbedding module +//////////////////////////////////////////////////////////////////////////////// diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml index 82de070921f0..8b65efdfb5f9 100644 --- a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml +++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt index 519b4296d93a..f44e282e8116 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/close/CloseSecondaryActivityInSplitTest.kt @@ -38,7 +38,7 @@ import org.junit.runners.Parameterized * Setup: Launch A|B in split with B being the secondary activity. Transitions: Finish B and expect * A to become fullscreen. * - * To run this test: `atest FlickerTestsOther:CloseSecondaryActivityInSplitTest` + * To run this test: `atest FlickerTestsActivityEmbedding:CloseSecondaryActivityInSplitTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt index 4cd6d15b2983..7a76dd9d1ebb 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt @@ -39,7 +39,7 @@ import org.junit.runners.Parameterized * 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` + * To run this test: `atest FlickerTestsActivityEmbedding:HorizontalSplitChangeRatioTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt index 5df8b57294f0..08b5f38a4655 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/MainActivityStartsSecondaryWithAlwaysExpandTest.kt @@ -39,7 +39,7 @@ import org.junit.runners.Parameterized * Setup: Launch A|B in split with B being the secondary activity. Transitions: A start C with * alwaysExpand=true, expect C to launch in fullscreen and cover split A|B. * - * To run this test: `atest FlickerTestsOther:MainActivityStartsSecondaryWithAlwaysExpandTest` + * To run this test: `atest FlickerTestsActivityEmbedding:MainActivityStartsSecondaryWithAlwaysExpandTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt index 5009c7ce4e70..1f002a089486 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingPlaceholderSplitTest.kt @@ -34,7 +34,7 @@ import org.junit.runners.Parameterized * Test opening an activity that will launch another activity as ActivityEmbedding placeholder in * split. * - * To run this test: `atest FlickerTestsOther:OpenActivityEmbeddingPlaceholderSplitTest` + * To run this test: `atest FlickerTestsActivityEmbedding:OpenActivityEmbeddingPlaceholderSplitTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt index 6327d92ed570..b78c3ec65e32 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenActivityEmbeddingSecondaryToSplitTest.kt @@ -34,7 +34,7 @@ import org.junit.runners.Parameterized /** * Test opening a secondary activity that will split with the main activity. * - * To run this test: `atest FlickerTestsOther:OpenActivityEmbeddingSecondaryToSplitTest` + * To run this test: `atest FlickerTestsActivityEmbedding:OpenActivityEmbeddingSecondaryToSplitTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt index 78004ccc3f97..10167d71c255 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt @@ -39,7 +39,7 @@ import org.junit.runners.Parameterized * * Transitions: Let B start C, expect C to cover B and end up in split A|C. * - * To run this test: `atest FlickerTestsOther:OpenThirdActivityOverSplitTest` + * To run this test: `atest FlickerTestsActivityEmbedding:OpenThirdActivityOverSplitTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt index 67825d2df361..3753b23966d2 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt @@ -23,7 +23,6 @@ import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.flicker.subject.region.RegionSubject -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 @@ -41,9 +40,8 @@ import org.junit.runners.Parameterized * 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` + * To run this test: `atest FlickerTestsActivityEmbedding:OpenTrampolineActivityTest` */ -@FlakyTest(bugId = 341209752) @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt index eed9225d3da0..a0b910bb9ac3 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/pip/SecondaryActivityEnterPipTest.kt @@ -41,7 +41,7 @@ import org.junit.runners.Parameterized * Setup: Start from a split A|B. Transition: B enters PIP, observe the window first goes fullscreen * then shrink to the bottom right corner on screen. * - * To run this test: `atest FlickerTestsOther:SecondaryActivityEnterPipTest` + * To run this test: `atest FlickerTestsActivityEmbedding:SecondaryActivityEnterPipTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt index f5e6c7854eba..ea13f5f748e1 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotateSplitNoChangeTest.kt @@ -36,7 +36,7 @@ import org.junit.runners.Parameterized * Setup: Launch A|B in split with B being the secondary activity. Transitions: Rotate display, and * expect A and B to split evenly in new rotation. * - * To run this test: `atest FlickerTestsOther:RotateSplitNoChangeTest` + * To run this test: `atest FlickerTestsActivityEmbedding:RotateSplitNoChangeTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt index ee2c05e82d51..06326f8cc8d2 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rotation/RotationTransition.kt @@ -36,7 +36,7 @@ abstract class RotationTransition(flicker: LegacyFlickerTest) : ActivityEmbeddin teardown { testApp.exit(wmHelper) } transitions { this.setRotation(flicker.scenario.endRotation) - if (!flicker.scenario.isTablet) { + if (!usesTaskbar) { wmHelper.StateSyncBuilder() .add(navBarInPosition(flicker.scenario.isGesturalNavigation)) .waitForAndVerify() diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rtl/RTLStartSecondaryWithPlaceholderTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rtl/RTLStartSecondaryWithPlaceholderTest.kt index 65a23e854e0b..2a177d53b037 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rtl/RTLStartSecondaryWithPlaceholderTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/rtl/RTLStartSecondaryWithPlaceholderTest.kt @@ -37,7 +37,7 @@ import org.junit.runners.Parameterized * PlaceholderPrimary, which is configured to launch with PlaceholderSecondary in RTL. Expect split * PlaceholderSecondary|PlaceholderPrimary covering split B|A. * - * To run this test: `atest FlickerTestsOther:RTLStartSecondaryWithPlaceholderTest` + * To run this test: `atest FlickerTestsActivityEmbedding:RTLStartSecondaryWithPlaceholderTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt index 379b45cdf08e..0ca8f37b239b 100644 --- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt +++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt @@ -24,6 +24,7 @@ import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.traces.parsers.toFlickerComponent +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions @@ -46,7 +47,7 @@ import org.junit.runners.Parameterized * Setup: Launch A|B in split and secondaryApp, return to home. Transitions: Let AE Split A|B enter * splitscreen with secondaryApp. Resulting in A|B|secondaryApp. * - * To run this test: `atest FlickerTestsOther:EnterSystemSplitTest` + * To run this test: `atest FlickerTestsActivityEmbedding:EnterSystemSplitTest` */ @RequiresDevice @RunWith(Parameterized::class) @@ -177,6 +178,13 @@ class EnterSystemSplitTest(flicker: LegacyFlickerTest) : ActivityEmbeddingTestBa @Ignore("Not applicable to this CUJ.") override fun visibleLayersShownMoreThanOneConsecutiveEntry() {} + @FlakyTest(bugId = 342596801) + override fun entireScreenCovered() = super.entireScreenCovered() + + @FlakyTest(bugId = 342596801) + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { /** {@inheritDoc} */ private var startDisplayBounds = Rect() diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp index 27e9ffa4cea5..1e997b386faa 100644 --- a/tests/FlickerTests/Android.bp +++ b/tests/FlickerTests/Android.bp @@ -47,7 +47,6 @@ java_defaults { java_library { name: "wm-flicker-common-assertions", - platform_apis: true, optimize: { enabled: false, }, diff --git a/tests/FlickerTests/AppClose/Android.bp b/tests/FlickerTests/AppClose/Android.bp index d14a178fe316..8b45740aad7b 100644 --- a/tests/FlickerTests/AppClose/Android.bp +++ b/tests/FlickerTests/AppClose/Android.bp @@ -33,3 +33,34 @@ android_test { static_libs: ["FlickerTestsBase"], data: ["trace_config/*"], } + +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for FlickerTestsAppClose module + +test_module_config { + name: "FlickerTestsAppClose-CatchAll", + base: "FlickerTestsAppClose", + exclude_filters: [ + "com.android.server.wm.flicker.close.CloseAppBackButtonTest", + "com.android.server.wm.flicker.close.CloseAppHomeButtonTest", + "com.android.server.wm.flicker.close.", + ], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppClose-CloseAppBackButtonTest", + base: "FlickerTestsAppClose", + include_filters: ["com.android.server.wm.flicker.close.CloseAppBackButtonTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppClose-CloseAppHomeButtonTest", + base: "FlickerTestsAppClose", + include_filters: ["com.android.server.wm.flicker.close.CloseAppHomeButtonTest"], + test_suites: ["device-tests"], +} + +// End breakdowns for FlickerTestsAppClose module +//////////////////////////////////////////////////////////////////////////////// diff --git a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml index 4ffb11ab92ae..3382c1e227b3 100644 --- a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt index e19e1ce35cd9..56b718a642db 100644 --- a/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt +++ b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppBackButtonTest.kt @@ -31,7 +31,7 @@ import org.junit.runners.Parameterized /** * Test app closes by pressing back button * - * To run this test: `atest FlickerTests:CloseAppBackButtonTest` + * To run this test: `atest FlickerTestsAppClose:CloseAppBackButtonTest` * * Actions: * ``` diff --git a/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt index 47ed642cd5f5..5deacaf31802 100644 --- a/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt +++ b/tests/FlickerTests/AppClose/src/com/android/server/wm/flicker/close/CloseAppHomeButtonTest.kt @@ -31,7 +31,7 @@ import org.junit.runners.Parameterized /** * Test app closes by pressing home button * - * To run this test: `atest FlickerTests:CloseAppHomeButtonTest` + * To run this test: `atest FlickerTestsAppClose:CloseAppHomeButtonTest` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/Android.bp b/tests/FlickerTests/AppLaunch/Android.bp index 72a90650927f..17d0f967b1bd 100644 --- a/tests/FlickerTests/AppLaunch/Android.bp +++ b/tests/FlickerTests/AppLaunch/Android.bp @@ -15,6 +15,7 @@ // package { + default_team: "trendy_team_windowing_animations_transitions", // 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" @@ -23,49 +24,115 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -filegroup { - name: "FlickerTestsAppLaunchCommon-src", - srcs: ["src/**/common/*"], -} - -filegroup { - name: "FlickerTestsAppLaunch1-src", - srcs: ["src/**/OpenAppFrom*"], -} - -java_library { - name: "FlickerTestsAppLaunchCommon", - defaults: ["FlickerTestsDefault"], - srcs: [":FlickerTestsAppLaunchCommon-src"], - static_libs: ["FlickerTestsBase"], -} - android_test { - name: "FlickerTestsAppLaunch1", + name: "FlickerTestsAppLaunch", defaults: ["FlickerTestsDefault"], manifest: "AndroidManifest.xml", test_config_template: "AndroidTestTemplate.xml", - srcs: [":FlickerTestsAppLaunch1-src"], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsAppLaunchCommon", - ], + srcs: ["src/**/*"], + static_libs: ["FlickerTestsBase"], data: ["trace_config/*"], } -android_test { - name: "FlickerTestsAppLaunch2", - defaults: ["FlickerTestsDefault"], - manifest: "AndroidManifest.xml", - test_config_template: "AndroidTestTemplate.xml", - srcs: ["src/**/*"], - exclude_srcs: [ - ":FlickerTestsAppLaunchCommon-src", - ":FlickerTestsAppLaunch1-src", - ], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsAppLaunchCommon", +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for FlickerTestsAppLaunch module + +test_module_config { + name: "FlickerTestsAppLaunch-CatchAll", + base: "FlickerTestsAppLaunch", + exclude_filters: [ + "com.android.server.wm.flicker.launch.TaskTransitionTest", + "com.android.server.wm.flicker.launch.ActivityTransitionTest", + "com.android.server.wm.flicker.launch.OpenAppFromIconColdTest", + "com.android.server.wm.flicker.launch.OpenAppFromIntentColdAfterCameraTest", + "com.android.server.wm.flicker.launch.OpenAppFromIntentColdTest", + "com.android.server.wm.flicker.launch.OpenAppFromIntentWarmTest", + "com.android.server.wm.flicker.launch.OpenAppFromLockscreenViaIntentTest", + "com.android.server.wm.flicker.launch.OpenAppFromOverviewTest", + "com.android.server.wm.flicker.launch.OpenCameraFromHomeOnDoubleClickPowerButtonTest", + "com.android.server.wm.flicker.launch.OpenTransferSplashscreenAppFromLauncherTransition", + "com.android.server.wm.flicker.launch.OverrideTaskTransitionTest", + "com.android.server.wm.flicker.launch.TaskTransitionTest", ], - data: ["trace_config/*"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-ActivityTransitionTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.ActivityTransitionTest"], + test_suites: ["device-tests"], } + +test_module_config { + name: "FlickerTestsAppLaunch-OpenAppFromIconColdTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.OpenAppFromIconColdTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-OpenAppFromIntentColdAfterCameraTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.OpenAppFromIntentColdAfterCameraTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-OpenAppFromIntentColdTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.OpenAppFromIntentColdTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-OpenAppFromIntentWarmTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.OpenAppFromIntentWarmTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-OpenAppFromLockscreenViaIntentTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.OpenAppFromLockscreenViaIntentTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-OpenAppFromOverviewTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.OpenAppFromOverviewTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-OpenCameraFromHomeOnDoubleClickPowerButtonTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.OpenCameraFromHomeOnDoubleClickPowerButtonTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-OpenTransferSplashscreenAppFromLauncherTransition", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.OpenTransferSplashscreenAppFromLauncherTransition"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-OverrideTaskTransitionTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.OverrideTaskTransitionTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppLaunch-TaskTransitionTest", + base: "FlickerTestsAppLaunch", + include_filters: ["com.android.server.wm.flicker.launch.TaskTransitionTest"], + test_suites: ["device-tests"], +} + +// End breakdowns for FlickerTestsAppLaunch module +//////////////////////////////////////////////////////////////////////////////// diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml index 0fa4d07b2eca..e941e79faea3 100644 --- a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/AppLaunch/OWNERS b/tests/FlickerTests/AppLaunch/OWNERS index 2c414a27cacb..d16b57dcb84c 100644 --- a/tests/FlickerTests/AppLaunch/OWNERS +++ b/tests/FlickerTests/AppLaunch/OWNERS @@ -1,4 +1,2 @@ -# System UI > ... > Overview (recent apps) > UI -# Bug template url: https://b.corp.google.com/issues/new?component=807991&template=1390280 = per-file *Overview* # window manager > animations/transitions # Bug template url: https://b.corp.google.com/issues/new?component=316275&template=1018192 diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt index ffa90a33e7b3..01cdbb810379 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/ActivityTransitionTest.kt @@ -35,7 +35,7 @@ import org.junit.runners.Parameterized /** * Test the back and forward transition between 2 activities. * - * To run this test: `atest FlickerTests:ActivitiesTransitionTest` + * To run this test: `atest FlickerTestsAppLaunch:ActivitiesTransitionTest` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt index 8c285bda6616..3d9321c0b830 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIconColdTest.kt @@ -30,7 +30,7 @@ import org.junit.runners.Parameterized /** * Test cold launching an app from launcher * - * To run this test: `atest FlickerTests:OpenAppColdFromIcon` + * To run this test: `atest FlickerTestsAppLaunch:OpenAppColdFromIcon` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt index 57da05f13bbb..92075303028c 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdAfterCameraTest.kt @@ -30,7 +30,7 @@ import org.junit.runners.Parameterized /** * Test launching an app after cold opening camera * - * To run this test: `atest FlickerTests:OpenAppAfterCameraTest` + * To run this test: `atest FlickerTestsAppLaunch:OpenAppAfterCameraTest` * * Notes: Some default assertions are inherited [OpenAppTransition] */ diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt index 267f282db41c..cbe7c3241df3 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentColdTest.kt @@ -35,7 +35,7 @@ import org.junit.runners.Parameterized /** * Test cold launching an app from launcher * - * To run this test: `atest FlickerTests:OpenAppColdTest` + * To run this test: `atest FlickerTestsAppLaunch:OpenAppColdTest` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt index 83065de8b592..b2941e70a2ed 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromIntentWarmTest.kt @@ -34,7 +34,7 @@ import org.junit.runners.Parameterized /** * Test warm launching an app from launcher * - * To run this test: `atest FlickerTests:OpenAppWarmTest` + * To run this test: `atest FlickerTestsAppLaunch:OpenAppWarmTest` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt index 44ae27c2ee4b..4048e0c89619 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt @@ -41,7 +41,7 @@ import org.junit.runners.Parameterized * * This test assumes the device doesn't have AOD enabled * - * To run this test: `atest FlickerTests:OpenAppNonResizeableTest` + * To run this test: `atest FlickerTestsAppLaunch:OpenAppNonResizeableTest` * * Actions: * ``` @@ -75,7 +75,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : @FlakyTest(bugId = 288341660) @Test fun navBarLayerVisibilityChanges() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.assertLayers { this.isInvisible(ComponentNameMatcher.NAV_BAR) .then() @@ -97,7 +97,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : @FlakyTest(bugId = 293581770) @Test fun navBarWindowsVisibilityChanges() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.assertWm { this.isNonAppWindowInvisible(ComponentNameMatcher.NAV_BAR) .then() @@ -112,7 +112,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : @Presubmit @Test fun taskBarLayerIsVisibleAtEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) } } @@ -170,7 +170,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : @Presubmit @Test fun navBarLayerIsVisibleAtEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.NAV_BAR) } } @@ -184,7 +184,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : @Presubmit @Test override fun appLayerBecomesVisible() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) super.appLayerBecomesVisible() } @@ -192,11 +192,11 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : @FlakyTest(bugId = 227143265) @Test fun appLayerBecomesVisibleTablet() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) super.appLayerBecomesVisible() } - @Presubmit + @FlakyTest(bugId = 338296297) @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt index 6d3eaeb9c1b3..064c76f1b92f 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt @@ -35,7 +35,7 @@ import org.junit.runners.Parameterized /** * Test launching an app from the recents app view (the overview) * - * To run this test: `atest FlickerTests:OpenAppFromOverviewTest` + * To run this test: `atest FlickerTestsAppLaunch:OpenAppFromOverviewTest` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt index bec02d0e59c6..41423fd9eefe 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenCameraFromHomeOnDoubleClickPowerButtonTest.kt @@ -41,7 +41,7 @@ import org.junit.runners.Parameterized /** * Test cold launching camera from launcher by double pressing power button * - * To run this test: `atest FlickerTests:OpenCameraOnDoubleClickPowerButton` + * To run this test: `atest FlickerTestsAppLaunch:OpenCameraOnDoubleClickPowerButton` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt index e0aef8d1addd..9d7a9c6789f8 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OpenTransferSplashscreenAppFromLauncherTransition.kt @@ -34,7 +34,7 @@ import org.junit.runners.Parameterized /** * Test cold launching an app from launcher * - * To run this test: `atest FlickerTests:OpenTransferSplashscreenAppFromLauncherTransition` + * To run this test: `atest FlickerTestsAppLaunch:OpenTransferSplashscreenAppFromLauncherTransition` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt index f1144991c438..7e2d472f4c4d 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt @@ -42,7 +42,7 @@ import org.junit.runners.Parameterized /** * Test the [android.app.ActivityOptions.makeCustomTaskAnimation]. * - * To run this test: `atest FlickerTests:OverrideTaskTransitionTest` + * To run this test: `atest FlickerTestsAppLaunch:OverrideTaskTransitionTest` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt index a71599d25632..95e8126964e7 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt @@ -49,7 +49,7 @@ import org.junit.runners.Parameterized /** * Test the back and forward transition between 2 activities. * - * To run this test: `atest FlickerTests:TaskTransitionTest` + * To run this test: `atest FlickerTestsAppLaunch:TaskTransitionTest` * * Actions: * ``` diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt index 8a3304b0343d..b497e3048759 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromIconTransition.kt @@ -28,6 +28,7 @@ abstract class OpenAppFromIconTransition(flicker: LegacyFlickerTest) : get() = { super.transition(this) setup { + // By default, launcher doesn't rotate on phones, but rotates on tablets if (flicker.scenario.isTablet) { tapl.setExpectedRotation(flicker.scenario.startRotation.value) } else { diff --git a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromLockscreenTransition.kt b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromLockscreenTransition.kt index f8fd35860f6f..a6e31d49a0e8 100644 --- a/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromLockscreenTransition.kt +++ b/tests/FlickerTests/AppLaunch/src/com/android/server/wm/flicker/launch/common/OpenAppFromLockscreenTransition.kt @@ -103,7 +103,7 @@ abstract class OpenAppFromLockscreenTransition(flicker: LegacyFlickerTest) : @Presubmit @Test open fun navBarLayerPositionAtEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerPositionAtEnd() } diff --git a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml index 4d9fefbc7d88..4e06dca17fe2 100644 --- a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml +++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt index 8040610c485b..cfc818b6c0e9 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonLandscape.kt @@ -31,8 +31,7 @@ import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class CloseAppBackButton3ButtonLandscape : CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_90) { - // TODO: Missing CUJ (b/300078127) - @ExpectedScenarios(["ENTIRE_TRACE"]) + @ExpectedScenarios(["APP_CLOSE_TO_HOME"]) @Test override fun closeAppBackButtonTest() = super.closeAppBackButtonTest() diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt index aacccf4e680c..6bf32a8e2083 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButton3ButtonPortrait.kt @@ -31,8 +31,7 @@ import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class CloseAppBackButton3ButtonPortrait : CloseAppBackButton(NavBar.MODE_3BUTTON, Rotation.ROTATION_0) { - // TODO: Missing CUJ (b/300078127) - @ExpectedScenarios(["ENTIRE_TRACE"]) + @ExpectedScenarios(["APP_CLOSE_TO_HOME"]) @Test override fun closeAppBackButtonTest() = super.closeAppBackButtonTest() diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt index 74ee46093f6e..4b6ab773f15e 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavLandscape.kt @@ -31,8 +31,7 @@ import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class CloseAppBackButtonGesturalNavLandscape : CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_90) { - // TODO: Missing CUJ (b/300078127) - @ExpectedScenarios(["ENTIRE_TRACE"]) + @ExpectedScenarios(["APP_CLOSE_TO_HOME"]) @Test override fun closeAppBackButtonTest() = super.closeAppBackButtonTest() diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt index 57463c33c1fa..7cc9db027e1f 100644 --- a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/close/flicker/CloseAppBackButtonGesturalNavPortrait.kt @@ -31,8 +31,7 @@ import org.junit.runner.RunWith @RunWith(FlickerServiceJUnit4ClassRunner::class) class CloseAppBackButtonGesturalNavPortrait : CloseAppBackButton(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) { - // TODO: Missing CUJ (b/300078127) - @ExpectedScenarios(["ENTIRE_TRACE"]) + @ExpectedScenarios(["APP_CLOSE_TO_HOME"]) @Test override fun closeAppBackButtonTest() = super.closeAppBackButtonTest() diff --git a/tests/FlickerTests/IME/Android.bp b/tests/FlickerTests/IME/Android.bp index ccc3683f0b93..cba3d09ebefd 100644 --- a/tests/FlickerTests/IME/Android.bp +++ b/tests/FlickerTests/IME/Android.bp @@ -24,16 +24,6 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -filegroup { - name: "FlickerTestsImeCommon-src", - srcs: ["src/**/common/*"], -} - -filegroup { - name: "FlickerTestsIme1-src", - srcs: ["src/**/Close*"], -} - android_test { name: "FlickerTestsIme", defaults: ["FlickerTestsDefault"], @@ -48,43 +38,136 @@ android_test { data: ["trace_config/*"], } -java_library { - name: "FlickerTestsImeCommon", - defaults: ["FlickerTestsDefault"], - srcs: [":FlickerTestsImeCommon-src"], - static_libs: ["FlickerTestsBase"], -} +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for FlickerTestsIme module -android_test { - name: "FlickerTestsIme1", - defaults: ["FlickerTestsDefault"], - manifest: "AndroidManifest.xml", - test_config_template: "AndroidTestTemplate.xml", - test_suites: [ - "device-tests", - "device-platinum-tests", +test_module_config { + name: "FlickerTestsIme-CatchAll", + base: "FlickerTestsIme", + exclude_filters: [ + "com.android.server.wm.flicker.ime.CloseImeOnDismissPopupDialogTest", + "com.android.server.wm.flicker.ime.CloseImeOnGoHomeTest", + "com.android.server.wm.flicker.ime.CloseImeShownOnAppStartOnGoHomeTest", + "com.android.server.wm.flicker.ime.CloseImeShownOnAppStartToAppOnPressBackTest", + "com.android.server.wm.flicker.ime.CloseImeToAppOnPressBackTest", + "com.android.server.wm.flicker.ime.CloseImeToHomeOnFinishActivityTest", + "com.android.server.wm.flicker.ime.OpenImeWindowToFixedPortraitAppTest", + "com.android.server.wm.flicker.ime.ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest", + "com.android.server.wm.flicker.ime.ShowImeOnAppStartWhenLaunchingAppFromOverviewTest", + "com.android.server.wm.flicker.ime.ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest", + "com.android.server.wm.flicker.ime.ShowImeOnAppStartWhenLaunchingAppTest", + "com.android.server.wm.flicker.ime.ShowImeOnUnlockScreenTest", + "com.android.server.wm.flicker.ime.ShowImeWhenFocusingOnInputFieldTest", + "com.android.server.wm.flicker.ime.ShowImeWhileDismissingThemedPopupDialogTest", + "com.android.server.wm.flicker.ime.ShowImeWhileEnteringOverviewTest", ], - srcs: [":FlickerTestsIme1-src"], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsImeCommon", - ], - data: ["trace_config/*"], + test_suites: ["device-tests"], } -android_test { - name: "FlickerTestsIme2", - defaults: ["FlickerTestsDefault"], - manifest: "AndroidManifest.xml", - test_config_template: "AndroidTestTemplate.xml", - srcs: ["src/**/*"], - exclude_srcs: [ - ":FlickerTestsIme1-src", - ":FlickerTestsImeCommon-src", - ], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsImeCommon", - ], - data: ["trace_config/*"], +test_module_config { + name: "FlickerTestsIme-CloseImeOnDismissPopupDialogTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.CloseImeOnDismissPopupDialogTest"], + test_suites: ["device-tests"], } + +test_module_config { + name: "FlickerTestsIme-CloseImeOnGoHomeTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.CloseImeOnGoHomeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-CloseImeShownOnAppStartOnGoHomeTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.CloseImeShownOnAppStartOnGoHomeTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-CloseImeShownOnAppStartToAppOnPressBackTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.CloseImeShownOnAppStartToAppOnPressBackTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-CloseImeToAppOnPressBackTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.CloseImeToAppOnPressBackTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-CloseImeToHomeOnFinishActivityTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.CloseImeToHomeOnFinishActivityTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-OpenImeWindowToFixedPortraitAppTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.OpenImeWindowToFixedPortraitAppTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-ShowImeOnAppStartWhenLaunchingAppFromOverviewTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.ShowImeOnAppStartWhenLaunchingAppFromOverviewTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-ShowImeOnAppStartWhenLaunchingAppTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.ShowImeOnAppStartWhenLaunchingAppTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-ShowImeOnUnlockScreenTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.ShowImeOnUnlockScreenTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-ShowImeWhenFocusingOnInputFieldTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.ShowImeWhenFocusingOnInputFieldTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-ShowImeWhileDismissingThemedPopupDialogTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.ShowImeWhileDismissingThemedPopupDialogTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsIme-ShowImeWhileEnteringOverviewTest", + base: "FlickerTestsIme", + include_filters: ["com.android.server.wm.flicker.ime.ShowImeWhileEnteringOverviewTest"], + test_suites: ["device-tests"], +} + +// End breakdowns for FlickerTestsIme module +//////////////////////////////////////////////////////////////////////////////// diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml index b879c54dcab3..0cadd68597b6 100644 --- a/tests/FlickerTests/IME/AndroidTestTemplate.xml +++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- enable AOD --> <option name="set-secure-setting" key="doze_always_on" value="1" /> <!-- prevents the phone from restarting --> diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt index 2b6ddcb43f18..48ca36ff8012 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt @@ -33,7 +33,7 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * To run this test: `atest FlickerTestsIme1:CloseImeOnDismissPopupDialogTest` + * To run this test: `atest FlickerTestsIme:CloseImeOnDismissPopupDialogTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt index 0344197c1425..e3f3aca135d1 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt @@ -34,7 +34,7 @@ import org.junit.runners.Parameterized /** * Test IME window closing to home transitions. - * To run this test: `atest FlickerTestsIme1:CloseImeOnGoHomeTest` + * To run this test: `atest FlickerTestsIme:CloseImeOnGoHomeTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt index fde1373b032b..3509e5bfecaf 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartOnGoHomeTest.kt @@ -42,7 +42,7 @@ import org.junit.runners.Parameterized * * More details on b/190352379 * - * To run this test: `atest FlickerTestsIme1:CloseImeShownOnAppStartOnGoHomeTest` + * To run this test: `atest FlickerTestsIme:CloseImeShownOnAppStartOnGoHomeTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt index ed6e8df3e293..53d7a3ff8f21 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeShownOnAppStartToAppOnPressBackTest.kt @@ -43,7 +43,7 @@ import org.junit.runners.Parameterized * * More details on b/190352379 * - * To run this test: `atest FlickerTestsIme1:CloseImeShownOnAppStartToAppOnPressBackTest` + * To run this test: `atest FlickerTestsIme:CloseImeShownOnAppStartToAppOnPressBackTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt index dc2bd1bc9996..4bc2705aa17d 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt @@ -35,7 +35,7 @@ import org.junit.runners.Parameterized /** * Test IME window closing back to app window transitions. - * To run this test: `atest FlickerTestsIme1:CloseImeToAppOnPressBackTest` + * To run this test: `atest FlickerTestsIme:CloseImeToAppOnPressBackTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @@ -72,7 +72,7 @@ class CloseImeToAppOnPressBackTest(flicker: LegacyFlickerTest) : BaseTest(flicke @Presubmit @Test override fun navBarLayerPositionAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) Assume.assumeFalse(flicker.scenario.isLandscapeOrSeascapeAtStart) flicker.navBarLayerPositionAtStartAndEnd() } @@ -80,7 +80,7 @@ class CloseImeToAppOnPressBackTest(flicker: LegacyFlickerTest) : BaseTest(flicke @Presubmit @Test fun navBarLayerPositionAtStartAndEndLandscapeOrSeascapeAtStart() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) Assume.assumeTrue(flicker.scenario.isLandscapeOrSeascapeAtStart) flicker.navBarLayerPositionAtStartAndEnd() } diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt index 05771e88fc83..6117bb0971d0 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt @@ -40,7 +40,7 @@ import org.junit.runners.Parameterized * Unlike {@link OpenImeWindowTest} testing IME window opening transitions, this test also verify * there is no flickering when back to the simple activity without requesting IME to show. * - * To run this test: `atest FlickerTestsIme1:CloseImeToHomeOnFinishActivityTest` + * To run this test: `atest FlickerTestsIme:CloseImeToHomeOnFinishActivityTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt index 336fe6f991ca..9b8d86d82007 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt @@ -37,7 +37,7 @@ import org.junit.runners.Parameterized /** * Test IME window shown on the app with fixing portrait orientation. - * To run this test: `atest FlickerTestsIme2:OpenImeWindowToFixedPortraitAppTest` + * To run this test: `atest FlickerTestsIme:OpenImeWindowToFixedPortraitAppTest` */ @RequiresDevice @RunWith(Parameterized::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt index 34a708578396..f806fae01eb4 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt @@ -34,7 +34,7 @@ import org.junit.runners.Parameterized /** * Test IME window opening transitions. - * To run this test: `atest FlickerTestsIme2:ShowImeOnAppStartWhenLaunchingAppFromOverviewTest` + * To run this test: `atest FlickerTestsIme:ShowImeOnAppStartWhenLaunchingAppFromOverviewTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt index 7c72c3187a7f..cc19f62a7cb3 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt @@ -36,7 +36,7 @@ import org.junit.runners.Parameterized /** * Test IME windows switching with 2-Buttons or gestural navigation. - * To run this test: `atest FlickerTestsIme2:ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest` + * To run this test: `atest FlickerTestsIme:ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt index fe5320cd1a46..4a4d3725d82c 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt @@ -36,7 +36,7 @@ import org.junit.runners.Parameterized /** * Launch an app that automatically displays the IME * - * To run this test: `atest FlickerTestsIme2:ShowImeOnAppStartWhenLaunchingAppTest` + * To run this test: `atest FlickerTestsIme:ShowImeOnAppStartWhenLaunchingAppTest` * * Actions: * ``` diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt index 92b6b934874f..d47e7ad8d904 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnUnlockScreenTest.kt @@ -36,7 +36,7 @@ import org.junit.runners.Parameterized /** * Test IME window closing on lock and opening on screen unlock. - * To run this test: `atest FlickerTestsIme2:ShowImeOnUnlockScreenTest` + * To run this test: `atest FlickerTestsIme:ShowImeOnUnlockScreenTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @@ -54,7 +54,7 @@ class ShowImeOnUnlockScreenTest(flicker: LegacyFlickerTest) : BaseTest(flicker) } transitions { device.sleep() - wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify() + wmHelper.StateSyncBuilder().withKeyguardShowing().waitForAndVerify() UnlockScreenRule.unlockScreen(device) wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify() } diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt index 9eaf998ed63f..47bf32483d69 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt @@ -33,7 +33,7 @@ import org.junit.runners.Parameterized /** * Test IME window opening transitions. - * To run this test: `atest FlickerTestsIme2:ShowImeWhenFocusingOnInputFieldTest` + * To run this test: `atest FlickerTestsIme:ShowImeWhenFocusingOnInputFieldTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt index 7186a2c48c4c..e3118b4cae0c 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt @@ -41,7 +41,7 @@ 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 FlickerTestsIme2:ShowImeWhileDismissingThemedPopupDialogTest` + * To run this test: `atest FlickerTestsIme:ShowImeWhileDismissingThemedPopupDialogTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt index c96c760e2d7b..064c07ea0dc0 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileEnteringOverviewTest.kt @@ -28,6 +28,7 @@ import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd import com.android.server.wm.flicker.statusBarLayerIsVisibleAtStartAndEnd +import com.android.server.wm.flicker.taskBarLayerIsVisibleAtStartAndEnd import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Ignore @@ -38,7 +39,7 @@ 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 FlickerTestsIme2:ShowImeWhileEnteringOverviewTest` + * To run this test: `atest FlickerTestsIme:ShowImeWhileEnteringOverviewTest` */ @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @@ -93,7 +94,7 @@ class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(fl @Presubmit @Test fun navBarLayerIsVisibleAtStartAndEnd3Button() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) Assume.assumeFalse(flicker.scenario.isGesturalNavigation) flicker.navBarLayerIsVisibleAtStartAndEnd() } @@ -105,7 +106,7 @@ class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(fl @Presubmit @Test fun navBarLayerIsInvisibleInLandscapeGestural() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) Assume.assumeTrue(flicker.scenario.isLandscapeOrSeascapeAtStart) Assume.assumeTrue(flicker.scenario.isGesturalNavigation) flicker.assertLayersStart { this.isVisible(ComponentNameMatcher.NAV_BAR) } @@ -114,7 +115,7 @@ class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(fl /** * In the legacy transitions, the nav bar is not marked as invisible. In the new transitions - * this is fixed and the nav bar shows as invisible + * this is fixed and the status bar shows as invisible */ @Presubmit @Test @@ -128,7 +129,7 @@ class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(fl /** * In the legacy transitions, the nav bar is not marked as invisible. In the new transitions - * this is fixed and the nav bar shows as invisible + * this is fixed and the status bar shows as invisible */ @Presubmit @Test @@ -149,6 +150,10 @@ class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(fl @Ignore("Visibility changes depending on orientation and navigation mode") override fun navBarLayerPositionAtStartAndEnd() {} + @Test + @Ignore("Visibility changes depending on orientation and navigation mode") + override fun taskBarLayerIsVisibleAtStartAndEnd() {} + /** {@inheritDoc} */ @Test @Ignore("Visibility changes depending on orientation and navigation mode") @@ -161,7 +166,10 @@ class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(fl @Presubmit @Test - override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() + fun taskBarLayerIsVisibleAtStartAndEndForTablets() { + Assume.assumeTrue(flicker.scenario.isTablet) + flicker.taskBarLayerIsVisibleAtStartAndEnd() + } @Presubmit @Test @@ -174,7 +182,7 @@ class ShowImeWhileEnteringOverviewTest(flicker: LegacyFlickerTest) : BaseTest(fl @Test fun statusBarLayerIsInvisibleInLandscape() { Assume.assumeTrue(flicker.scenario.isLandscapeOrSeascapeAtStart) - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.assertLayersStart { this.isVisible(ComponentNameMatcher.STATUS_BAR) } flicker.assertLayersEnd { this.isInvisible(ComponentNameMatcher.STATUS_BAR) } } diff --git a/tests/FlickerTests/Notification/Android.bp b/tests/FlickerTests/Notification/Android.bp index 4648383b2771..06daaafacbd8 100644 --- a/tests/FlickerTests/Notification/Android.bp +++ b/tests/FlickerTests/Notification/Android.bp @@ -32,3 +32,57 @@ android_test { static_libs: ["FlickerTestsBase"], data: ["trace_config/*"], } + +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for FlickerTestsNotification module + +test_module_config { + name: "FlickerTestsNotification-CatchAll", + base: "FlickerTestsNotification", + exclude_filters: [ + "com.android.server.wm.flicker.notification.OpenAppFromLockscreenNotificationColdTest", + "com.android.server.wm.flicker.notification.OpenAppFromLockscreenNotificationWarmTest", + "com.android.server.wm.flicker.notification.OpenAppFromLockscreenNotificationWithOverlayAppTest", + "com.android.server.wm.flicker.notification.OpenAppFromNotificationColdTest", + "com.android.server.wm.flicker.notification.OpenAppFromNotificationWarmTest", + ], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsNotification-OpenAppFromLockscreenNotificationColdTest", + base: "FlickerTestsNotification", + include_filters: ["com.android.server.wm.flicker.notification.OpenAppFromLockscreenNotificationColdTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsNotification-OpenAppFromLockscreenNotificationWarmTest", + base: "FlickerTestsNotification", + include_filters: ["com.android.server.wm.flicker.notification.OpenAppFromLockscreenNotificationWarmTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsNotification-OpenAppFromLockscreenNotificationWithOverlayAppTest", + base: "FlickerTestsNotification", + include_filters: ["com.android.server.wm.flicker.notification.OpenAppFromLockscreenNotificationWithOverlayAppTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsNotification-OpenAppFromNotificationColdTest", + base: "FlickerTestsNotification", + include_filters: ["com.android.server.wm.flicker.notification.OpenAppFromNotificationColdTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsNotification-OpenAppFromNotificationWarmTest", + base: "FlickerTestsNotification", + include_filters: ["com.android.server.wm.flicker.notification.OpenAppFromNotificationWarmTest"], + test_suites: ["device-tests"], +} + +// End breakdowns for FlickerTestsNotification module +//////////////////////////////////////////////////////////////////////////////// diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml index 04b312a896b9..f32e8bed85ef 100644 --- a/tests/FlickerTests/Notification/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt index 07fc2300286a..ad70757a9a4d 100644 --- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt +++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt @@ -151,7 +151,7 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : @Presubmit @Test open fun taskBarWindowIsVisibleAtEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarWindowIsVisibleAtEnd() } @@ -163,7 +163,7 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : @Presubmit @Test open fun taskBarLayerIsVisibleAtEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarLayerIsVisibleAtEnd() } @@ -171,7 +171,7 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : @Presubmit @Test open fun navBarLayerPositionAtEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerPositionAtEnd() } @@ -179,14 +179,14 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : @Presubmit @Test open fun navBarLayerIsVisibleAtEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerIsVisibleAtEnd() } @Presubmit @Test open fun navBarWindowIsVisibleAtEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarWindowIsVisibleAtEnd() } diff --git a/tests/FlickerTests/QuickSwitch/Android.bp b/tests/FlickerTests/QuickSwitch/Android.bp index 8755d0e3b304..4d5dba3d9221 100644 --- a/tests/FlickerTests/QuickSwitch/Android.bp +++ b/tests/FlickerTests/QuickSwitch/Android.bp @@ -32,3 +32,41 @@ android_test { static_libs: ["FlickerTestsBase"], data: ["trace_config/*"], } + +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for FlickerTestsQuickswitch module + +test_module_config { + name: "FlickerTestsQuickswitch-CatchAll", + base: "FlickerTestsQuickswitch", + exclude_filters: [ + "com.android.server.wm.flicker.quickswitch.QuickSwitchBetweenTwoAppsBackTest", + "com.android.server.wm.flicker.quickswitch.QuickSwitchBetweenTwoAppsForwardTest", + "com.android.server.wm.flicker.quickswitch.QuickSwitchFromLauncherTest", + ], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsQuickswitch-QuickSwitchBetweenTwoAppsBackTest", + base: "FlickerTestsQuickswitch", + include_filters: ["com.android.server.wm.flicker.quickswitch.QuickSwitchBetweenTwoAppsBackTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsQuickswitch-QuickSwitchBetweenTwoAppsForwardTest", + base: "FlickerTestsQuickswitch", + include_filters: ["com.android.server.wm.flicker.quickswitch.QuickSwitchBetweenTwoAppsForwardTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsQuickswitch-QuickSwitchFromLauncherTest", + base: "FlickerTestsQuickswitch", + include_filters: ["com.android.server.wm.flicker.quickswitch.QuickSwitchFromLauncherTest"], + test_suites: ["device-tests"], +} + +// End breakdowns for FlickerTestsQuickswitch module +//////////////////////////////////////////////////////////////////////////////// diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml index 8acdabc2337d..68ae4f1f7f4f 100644 --- a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml +++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt index 9bb62e1e1794..1a32f2045546 100644 --- a/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt +++ b/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt @@ -38,7 +38,7 @@ import org.junit.runners.Parameterized /** * Test quick switching back to previous app from last opened app * - * To run this test: `atest FlickerTests:QuickSwitchBetweenTwoAppsBackTest` + * To run this test: `atest FlickerTestsQuickswitch:QuickSwitchBetweenTwoAppsBackTest` * * Actions: * ``` diff --git a/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt index 491b9945d12d..d82dddd9eeeb 100644 --- a/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt +++ b/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt @@ -37,7 +37,7 @@ import org.junit.runners.Parameterized /** * Test quick switching back to previous app from last opened app * - * To run this test: `atest FlickerTests:QuickSwitchBetweenTwoAppsForwardTest` + * To run this test: `atest FlickerTestsQuickswitch:QuickSwitchBetweenTwoAppsForwardTest` * * Actions: * ``` diff --git a/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt index de54c95da361..ab366286b6d8 100644 --- a/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt +++ b/tests/FlickerTests/QuickSwitch/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt @@ -38,7 +38,7 @@ import org.junit.runners.Parameterized /** * Test quick switching to last opened app from launcher * - * To run this test: `atest FlickerTests:QuickSwitchFromLauncherTest` + * To run this test: `atest FlickerTestsQuickswitch:QuickSwitchFromLauncherTest` * * Actions: * ``` diff --git a/tests/FlickerTests/Rotation/Android.bp b/tests/FlickerTests/Rotation/Android.bp index aceb8bad256f..0884ef9734b0 100644 --- a/tests/FlickerTests/Rotation/Android.bp +++ b/tests/FlickerTests/Rotation/Android.bp @@ -37,3 +37,41 @@ android_test { static_libs: ["FlickerTestsBase"], data: ["trace_config/*"], } + +//////////////////////////////////////////////////////////////////////////////// +// Begin breakdowns for FlickerTestsRotation module + +test_module_config { + name: "FlickerTestsAppRotation-CatchAll", + base: "FlickerTestsRotation", + exclude_filters: [ + "com.android.server.wm.flicker.rotation.ChangeAppRotationTest", + "com.android.server.wm.flicker.rotation.OpenShowWhenLockedSeamlessAppRotationTest", + "com.android.server.wm.flicker.rotation.SeamlessAppRotationTest", + ], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppRotation-ChangeAppRotationTest", + base: "FlickerTestsRotation", + include_filters: ["com.android.server.wm.flicker.rotation.ChangeAppRotationTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppRotation-OpenShowWhenLockedSeamlessAppRotationTest", + base: "FlickerTestsRotation", + include_filters: ["com.android.server.wm.flicker.rotation.OpenShowWhenLockedSeamlessAppRotationTest"], + test_suites: ["device-tests"], +} + +test_module_config { + name: "FlickerTestsAppRotation-SeamlessAppRotationTest", + base: "FlickerTestsRotation", + include_filters: ["com.android.server.wm.flicker.rotation.SeamlessAppRotationTest"], + test_suites: ["device-tests"], +} + +// End breakdowns for FlickerTestsRotation module +//////////////////////////////////////////////////////////////////////////////// diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml index 91ece214aad5..ec186723b4a4 100644 --- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml +++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml @@ -12,6 +12,10 @@ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> <!-- keeps the screen on during tests --> <option name="screen-always-on" value="on"/> + <!-- Turns off Wi-fi --> + <option name="wifi" value="off"/> + <!-- Turns off Bluetooth --> + <option name="bluetooth" value="off"/> <!-- prevents the phone from restarting --> <option name="force-skip-system-props" value="true"/> <!-- set WM tracing verbose level to all --> diff --git a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt index 05ab364ed72c..49e2553ab4a1 100644 --- a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt +++ b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt @@ -48,7 +48,7 @@ import org.junit.runners.Parameterized * Stop tracing * ``` * - * To run this test: `atest FlickerTests:ChangeAppRotationTest` + * To run this test: `atest FlickerTestsRotation:ChangeAppRotationTest` * * To run only the presubmit assertions add: `-- * diff --git a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt index a41362857420..d7f91e009c92 100644 --- a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt +++ b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt @@ -55,7 +55,7 @@ import org.junit.runners.Parameterized * Stop tracing * ``` * - * To run this test: `atest FlickerTests:SeamlessAppRotationTest` + * To run this test: `atest FlickerTestsRotation:SeamlessAppRotationTest` * * To run only the presubmit assertions add: `-- * diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt index 060015bcc4b2..851ce022bd81 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt @@ -26,6 +26,7 @@ import android.tools.traces.component.ComponentNameMatcher import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.wm.shell.Flags import org.junit.Assume import org.junit.AssumptionViolatedException import org.junit.Test @@ -48,6 +49,9 @@ constructor( private val logTag = this::class.java.simpleName + protected val usesTaskbar: Boolean + get() = flicker.scenario.isTablet || Flags.enableTaskbarOnPhones() + /** Specification of the test transition to execute */ abstract val transition: FlickerBuilder.() -> Unit @@ -87,7 +91,7 @@ constructor( @Presubmit @Test open fun navBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerIsVisibleAtStartAndEnd() } @@ -100,7 +104,7 @@ constructor( @Presubmit @Test open fun navBarLayerPositionAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarLayerPositionAtStartAndEnd() } @@ -112,7 +116,7 @@ constructor( @Presubmit @Test open fun navBarWindowIsAlwaysVisible() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) Assume.assumeFalse(flicker.scenario.isLandscapeOrSeascapeAtStart) flicker.navBarWindowIsAlwaysVisible() } @@ -126,32 +130,28 @@ constructor( @Presubmit @Test open fun navBarWindowIsVisibleAtStartAndEnd() { - Assume.assumeFalse(flicker.scenario.isTablet) + Assume.assumeFalse(usesTaskbar) flicker.navBarWindowIsVisibleAtStartAndEnd() } /** * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible at the start and end of the * transition - * - * Note: Large screen only */ @Presubmit @Test open fun taskBarLayerIsVisibleAtStartAndEnd() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarLayerIsVisibleAtStartAndEnd() } /** * Checks that the [ComponentNameMatcher.TASK_BAR] window is visible during the whole transition - * - * Note: Large screen only */ @Presubmit @Test open fun taskBarWindowIsAlwaysVisible() { - Assume.assumeTrue(flicker.scenario.isTablet) + Assume.assumeTrue(usesTaskbar) flicker.taskBarWindowIsAlwaysVisible() } diff --git a/tests/FlickerTests/test-apps/app-helpers/Android.bp b/tests/FlickerTests/test-apps/app-helpers/Android.bp index fc4d71c652d5..e8bb64aa6c55 100644 --- a/tests/FlickerTests/test-apps/app-helpers/Android.bp +++ b/tests/FlickerTests/test-apps/app-helpers/Android.bp @@ -25,7 +25,6 @@ package { java_library { name: "wm-flicker-common-app-helpers", - platform_apis: true, optimize: { enabled: false, }, diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt index 4a675be65549..0bcd2f334c32 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt @@ -17,6 +17,7 @@ package com.android.server.wm.flicker.helpers import android.app.Instrumentation +import android.os.SystemClock import android.tools.PlatformConsts import android.tools.device.apphelpers.StandardAppHelper import android.tools.helpers.FIND_TIMEOUT @@ -25,6 +26,7 @@ import android.tools.traces.parsers.WindowManagerStateHelper import android.tools.traces.parsers.toFlickerComponent import android.util.Log import androidx.test.uiautomator.By +import androidx.test.uiautomator.Direction import androidx.test.uiautomator.Until import androidx.window.extensions.WindowExtensions import androidx.window.extensions.WindowExtensionsProvider @@ -83,6 +85,7 @@ constructor( * activity and finish itself. */ fun launchTrampolineActivity(wmHelper: WindowManagerStateHelper) { + scrollToBottom() val launchButton = uiDevice.wait( Until.findObject(By.res(packageName, "launch_trampoline_button")), @@ -210,6 +213,7 @@ constructor( * placeholder secondary activity based on the placeholder rule. */ fun launchPlaceholderSplitRTL(wmHelper: WindowManagerStateHelper) { + scrollToBottom() val launchButton = uiDevice.wait( Until.findObject(By.res(packageName, "launch_placeholder_split_rtl_button")), @@ -224,6 +228,21 @@ constructor( .waitForAndVerify() } + /** + * Scrolls to the bottom of the launch options. This is needed if the launch button is at the + * bottom. Otherwise the click may trigger touch on navBar. + */ + private fun scrollToBottom() { + val launchOptionsList = uiDevice.wait( + Until.findObject(By.res(packageName, "launch_options_list")), + FIND_TIMEOUT + ) + requireNotNull(launchOptionsList) { "Unable to find the list of launch options" } + launchOptionsList.scrollUntil(Direction.DOWN, Until.scrollFinished(Direction.DOWN)) + // Wait a bit after scrolling, otherwise the immediate click may not be treated as "click". + SystemClock.sleep(1000L) + } + companion object { private const val TAG = "ActivityEmbeddingAppHelper" diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 9a5e88becf1e..332b9b832037 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -16,16 +16,27 @@ package com.android.server.wm.flicker.helpers +import android.content.Context +import android.graphics.Insets import android.graphics.Rect +import android.graphics.Region +import android.os.SystemClock +import android.platform.uiautomator_helpers.DeviceHelpers import android.tools.device.apphelpers.IStandardAppHelper import android.tools.helpers.SYSTEMUI_PACKAGE import android.tools.traces.parsers.WindowManagerStateHelper import android.tools.traces.wm.WindowingMode +import android.view.WindowInsets +import android.view.WindowManager +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod.TOUCH +import com.android.window.flags.Flags +import java.time.Duration /** * Wrapper class around App helper classes. This class adds functionality to the apps that the @@ -41,15 +52,17 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : RIGHT_BOTTOM } - private val TIMEOUT_MS = 3_000L - private val CAPTION = "desktop_mode_caption" - private val CAPTION_HANDLE = "caption_handle" - private val MAXIMIZE_BUTTON = "maximize_window" - private val MAXIMIZE_BUTTON_VIEW = "maximize_button_view" - private val CLOSE_BUTTON = "close_window" + enum class Edges { + LEFT, + RIGHT, + TOP, + BOTTOM + } - private val caption: BySelector - get() = By.res(SYSTEMUI_PACKAGE, CAPTION) + enum class AppProperty { + STANDARD, + NON_RESIZABLE + } /** Wait for an app moved to desktop to finish its transition. */ private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) { @@ -65,42 +78,117 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : fun enterDesktopWithDrag( wmHelper: WindowManagerStateHelper, device: UiDevice, + motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH) ) { innerHelper.launchViaIntent(wmHelper) - dragToDesktop(wmHelper, device) + dragToDesktop( + wmHelper = wmHelper, + device = device, + motionEventHelper = motionEventHelper + ) waitForAppToMoveToDesktop(wmHelper) } - private fun dragToDesktop(wmHelper: WindowManagerStateHelper, device: UiDevice) { + private fun dragToDesktop( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + motionEventHelper: MotionEventHelper + ) { val windowRect = wmHelper.getWindowRegion(innerHelper).bounds val startX = windowRect.centerX() // Start dragging a little under the top to prevent dragging the notification shade. val startY = 10 - val displayRect = - wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect - ?: throw IllegalStateException("Default display is null") + val displayRect = getDisplayRect(wmHelper) // The position we want to drag to val endY = displayRect.centerY() / 2 // drag the window to move to desktop - device.drag(startX, startY, startX, endY, 100) + if (motionEventHelper.inputMethod == TOUCH + && Flags.enableHoldToDragAppHandle()) { + // Touch requires hold-to-drag. + motionEventHelper.holdToDrag(startX, startY, startX, endY, steps = 100) + } else { + device.drag(startX, startY, startX, endY, 100) + } + } + + private fun getMaximizeButtonForTheApp(caption: UiObject2?): UiObject2 { + return caption + ?.children + ?.find { it.resourceName.endsWith(MAXIMIZE_BUTTON_VIEW) } + ?.children + ?.get(0) + ?: error("Unable to find resource $MAXIMIZE_BUTTON_VIEW\n") } /** Click maximise button on the app header for the given app. */ fun maximiseDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) { val caption = getCaptionForTheApp(wmHelper, device) - val maximizeButton = - caption - ?.children - ?.find { it.resourceName.endsWith(MAXIMIZE_BUTTON_VIEW) } - ?.children - ?.get(0) - maximizeButton?.click() + val maximizeButton = getMaximizeButtonForTheApp(caption) + maximizeButton.click() wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() } + + private fun getMinimizeButtonForTheApp(caption: UiObject2?): UiObject2 { + return caption + ?.children + ?.find { it.resourceName.endsWith(MINIMIZE_BUTTON_VIEW) } + ?: error("Unable to find resource $MINIMIZE_BUTTON_VIEW\n") + } + + fun minimizeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) { + val caption = getCaptionForTheApp(wmHelper, device) + val minimizeButton = getMinimizeButtonForTheApp(caption) + minimizeButton.click() + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceDisappeared(innerHelper) + .waitForAndVerify() + } + + /** Open maximize menu and click snap resize button on the app header for the given app. */ + fun snapResizeDesktopApp( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + context: Context, + toLeft: Boolean + ) { + val caption = getCaptionForTheApp(wmHelper, device) + val maximizeButton = getMaximizeButtonForTheApp(caption) + maximizeButton?.longClick() + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + + val buttonResId = if (toLeft) SNAP_LEFT_BUTTON else SNAP_RIGHT_BUTTON + val maximizeMenu = getDesktopAppViewByRes(MAXIMIZE_MENU) + + val snapResizeButton = + maximizeMenu + ?.wait(Until.findObject(By.res(SYSTEMUI_PACKAGE, buttonResId)), TIMEOUT.toMillis()) + ?: error("Unable to find object with resource id $buttonResId") + snapResizeButton.click() + + val displayRect = getDisplayRect(wmHelper) + val insets = getWindowInsets( + context, WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars() + ) + displayRect.inset(insets) + + val expectedWidth = displayRect.width() / 2 + val expectedRect = Rect(displayRect).apply { + if (toLeft) right -= expectedWidth else left += expectedWidth + } + + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withSurfaceVisibleRegion(this, Region(expectedRect)) + .waitForAndVerify() + } + /** Click close button on the app header for the given app. */ fun closeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) { val caption = getCaptionForTheApp(wmHelper, device) @@ -119,11 +207,10 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : ): UiObject2? { if ( wmHelper.getWindow(innerHelper)?.windowingMode != - WindowingMode.WINDOWING_MODE_FREEFORM.value - ) - error("expected a freeform window with caption but window is not in freeform mode") + WindowingMode.WINDOWING_MODE_FREEFORM.value + ) error("expected a freeform window with caption but window is not in freeform mode") val captions = - device.wait(Until.findObjects(caption), TIMEOUT_MS) + device.wait(Until.findObjects(caption), TIMEOUT.toMillis()) ?: error("Unable to find view $caption\n") return captions.find { @@ -147,7 +234,80 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : val endX = startX + horizontalChange // drag the specified corner of the window to the end coordinate. - device.drag(startX, startY, endX, endY, 100) + dragWindow(startX, startY, endX, endY, wmHelper, device) + } + + /** Resize a desktop app from its edges. */ + fun edgeResize( + wmHelper: WindowManagerStateHelper, + motionEvent: MotionEventHelper, + edge: Edges + ) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + val (startX, startY) = getStartCoordinatesForEdgeResize(windowRect, edge) + val verticalChange = when (edge) { + Edges.LEFT -> 0 + Edges.RIGHT -> 0 + Edges.TOP -> -100 + Edges.BOTTOM -> 100 + } + val horizontalChange = when (edge) { + Edges.LEFT -> -100 + Edges.RIGHT -> 100 + Edges.TOP -> 0 + Edges.BOTTOM -> 0 + } + + // The position we want to drag to + val endY = startY + verticalChange + val endX = startX + horizontalChange + + val downTime = SystemClock.uptimeMillis() + motionEvent.actionDown(startX, startY, time = downTime) + motionEvent.actionMove(startX, startY, endX, endY, /* steps= */100, downTime = downTime) + motionEvent.actionUp(endX, endY, downTime = downTime) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() + } + + /** Drag a window from a source coordinate to a destination coordinate. */ + fun dragWindow( + startX: Int, startY: Int, + endX: Int, endY: Int, + wmHelper: WindowManagerStateHelper, + device: UiDevice + ) { + device.drag(startX, startY, endX, endY, /* steps= */ 100) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .waitForAndVerify() + } + + /** Drag a window to a snap resize region, found at the left and right edges of the screen. */ + fun dragToSnapResizeRegion( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + isLeft: Boolean, + ) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + // Set start x-coordinate as center of app header. + val startX = windowRect.centerX() + val startY = windowRect.top + + val displayRect = getDisplayRect(wmHelper) + + val endX = if (isLeft) { + displayRect.left + SNAP_RESIZE_DRAG_INSET + } else { + displayRect.right - SNAP_RESIZE_DRAG_INSET + } + val endY = displayRect.centerY() / 2 + + // drag the window to snap resize + device.drag(startX, startY, endX, endY, /* steps= */ 100) wmHelper .StateSyncBuilder() .withAppTransitionIdle() @@ -165,4 +325,101 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : Corners.RIGHT_BOTTOM -> Pair(windowRect.right, windowRect.bottom) } } + + private fun getStartCoordinatesForEdgeResize( + windowRect: Rect, + edge: Edges + ): Pair<Int, Int> { + return when (edge) { + Edges.LEFT -> Pair(windowRect.left, windowRect.bottom / 2) + Edges.RIGHT -> Pair(windowRect.right, windowRect.bottom / 2) + Edges.TOP -> Pair(windowRect.right / 2, windowRect.top) + Edges.BOTTOM -> Pair(windowRect.right / 2, windowRect.bottom) + } + } + + /** Exit desktop mode by dragging the app handle to the top drag zone. */ + fun exitDesktopWithDragToTopDragZone( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + ) { + dragAppWindowToTopDragZone(wmHelper, device) + waitForTransitionToFullscreen(wmHelper) + } + + private fun dragAppWindowToTopDragZone(wmHelper: WindowManagerStateHelper, device: UiDevice) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + val displayRect = getDisplayRect(wmHelper) + + val startX = windowRect.centerX() + val endX = displayRect.centerX() + val startY = windowRect.top + val endY = 0 // top of the screen + + // drag the app window to top drag zone + device.drag(startX, startY, endX, endY, 100) + } + + fun enterDesktopModeFromAppHandleMenu( + wmHelper: WindowManagerStateHelper, + device: UiDevice + ) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + val startX = windowRect.centerX() + // Click a little under the top to prevent opening the notification shade. + val startY = 10 + + // Click on the app handle coordinates. + device.click(startX, startY) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + + val pill = getDesktopAppViewByRes(PILL_CONTAINER) + val desktopModeButton = + pill + ?.children + ?.find { it.resourceName.endsWith(DESKTOP_MODE_BUTTON) } + + desktopModeButton?.click() + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + } + + private fun getDesktopAppViewByRes(viewResId: String): UiObject2? = + DeviceHelpers.waitForObj(By.res(SYSTEMUI_PACKAGE, viewResId), TIMEOUT) + + private fun getDisplayRect(wmHelper: WindowManagerStateHelper): Rect = + wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect + ?: throw IllegalStateException("Default display is null") + + + /** Wait for transition to full screen to finish. */ + private fun waitForTransitionToFullscreen(wmHelper: WindowManagerStateHelper) { + wmHelper + .StateSyncBuilder() + .withFullScreenApp(innerHelper) + .withAppTransitionIdle() + .waitForAndVerify() + } + + private fun getWindowInsets(context: Context, typeMask: Int): Insets { + val wm: WindowManager = context.getSystemService(WindowManager::class.java) + ?: error("Unable to connect to WindowManager service") + val metricInsets = wm.currentWindowMetrics.windowInsets + return metricInsets.getInsetsIgnoringVisibility(typeMask) + } + + private companion object { + val TIMEOUT: Duration = Duration.ofSeconds(3) + const val SNAP_RESIZE_DRAG_INSET: Int = 5 // inset to avoid dragging to display edge + const val CAPTION: String = "desktop_mode_caption" + const val MAXIMIZE_BUTTON_VIEW: String = "maximize_button_view" + const val MAXIMIZE_MENU: String = "maximize_menu" + const val CLOSE_BUTTON: String = "close_window" + const val PILL_CONTAINER: String = "windowing_pill" + const val DESKTOP_MODE_BUTTON: String = "desktop_button" + const val SNAP_LEFT_BUTTON: String = "maximize_menu_snap_left_button" + const val SNAP_RIGHT_BUTTON: String = "maximize_menu_snap_right_button" + const val MINIMIZE_BUTTON_VIEW: String = "minimize_window" + val caption: BySelector + get() = By.res(SYSTEMUI_PACKAGE, CAPTION) + } } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt index 634b6eedd7e6..fd13d14074d4 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/LetterboxAppHelper.kt @@ -33,9 +33,9 @@ class LetterboxAppHelper @JvmOverloads constructor( instr: Instrumentation, - launcherName: String = ActivityOptions.NonResizeablePortraitActivity.LABEL, + launcherName: String = ActivityOptions.NonResizeableFixedAspectRatioPortraitActivity.LABEL, component: ComponentNameMatcher = - ActivityOptions.NonResizeablePortraitActivity.COMPONENT.toFlickerComponent() + ActivityOptions.NonResizeableFixedAspectRatioPortraitActivity.COMPONENT.toFlickerComponent() ) : StandardAppHelper(instr, launcherName, component) { private val gestureHelper: GestureHelper = GestureHelper(instrumentation) @@ -128,6 +128,6 @@ constructor( } companion object { - private const val BOUNDS_OFFSET: Int = 100 + private const val BOUNDS_OFFSET: Int = 50 } } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt new file mode 100644 index 000000000000..1fe60888fa52 --- /dev/null +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.helpers + +import android.app.Instrumentation +import android.os.SystemClock +import android.view.ContentInfo.Source +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_STYLUS +import android.view.InputDevice.SOURCE_TOUCHSCREEN +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_MOVE +import android.view.MotionEvent.ACTION_UP +import android.view.MotionEvent.TOOL_TYPE_FINGER +import android.view.MotionEvent.TOOL_TYPE_MOUSE +import android.view.MotionEvent.TOOL_TYPE_STYLUS +import android.view.MotionEvent.ToolType + +/** + * Helper class for injecting a custom motion event and performing some actions. This is used for + * instrumenting input injections like stylus, mouse and touchpad. + */ +class MotionEventHelper( + private val instr: Instrumentation, + val inputMethod: InputMethod +) { + enum class InputMethod(@ToolType val toolType: Int, @Source val source: Int) { + STYLUS(TOOL_TYPE_STYLUS, SOURCE_STYLUS), + MOUSE(TOOL_TYPE_MOUSE, SOURCE_MOUSE), + TOUCHPAD(TOOL_TYPE_FINGER, SOURCE_MOUSE), + TOUCH(TOOL_TYPE_FINGER, SOURCE_TOUCHSCREEN) + } + + fun actionDown(x: Int, y: Int, time: Long = SystemClock.uptimeMillis()) { + injectMotionEvent(ACTION_DOWN, x, y, downTime = time, eventTime = time) + } + + fun actionUp(x: Int, y: Int, downTime: Long) { + injectMotionEvent(ACTION_UP, x, y, downTime = downTime) + } + + fun actionMove( + startX: Int, + startY: Int, + endX: Int, + endY: Int, + steps: Int, + downTime: Long, + withMotionEventInjectDelay: Boolean = false + ) { + val incrementX = (endX - startX).toFloat() / (steps - 1) + val incrementY = (endY - startY).toFloat() / (steps - 1) + + for (i in 0..steps) { + val time = SystemClock.uptimeMillis() + val x = startX + incrementX * i + val y = startY + incrementY * i + + val moveEvent = getMotionEvent(downTime, time, ACTION_MOVE, x, y) + injectMotionEvent(moveEvent) + if (withMotionEventInjectDelay) { + SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS) + } + } + } + + /** + * Drag from [startX], [startY] to [endX], [endY] with a "hold" period after touching down + * and before moving. + */ + fun holdToDrag(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int) { + val downTime = SystemClock.uptimeMillis() + actionDown(startX, startY, time = downTime) + SystemClock.sleep(100L) // Hold before dragging. + actionMove( + startX, + startY, + endX, + endY, + steps, + downTime, + withMotionEventInjectDelay = true + ) + SystemClock.sleep(REGULAR_CLICK_LENGTH) + actionUp(startX, endX, downTime) + } + + private fun injectMotionEvent( + action: Int, + x: Int, + y: Int, + downTime: Long = SystemClock.uptimeMillis(), + eventTime: Long = SystemClock.uptimeMillis() + ): MotionEvent { + val event = getMotionEvent(downTime, eventTime, action, x.toFloat(), y.toFloat()) + injectMotionEvent(event) + return event + } + + private fun injectMotionEvent(event: MotionEvent) { + instr.uiAutomation.injectInputEvent(event, true, false) + } + + private fun getMotionEvent( + downTime: Long, + eventTime: Long, + action: Int, + x: Float, + y: Float, + ): MotionEvent { + val properties = MotionEvent.PointerProperties.createArray(1) + properties[0].toolType = inputMethod.toolType + properties[0].id = 1 + + val coords = MotionEvent.PointerCoords.createArray(1) + coords[0].x = x + coords[0].y = y + coords[0].pressure = 1f + + val event = + MotionEvent.obtain( + downTime, + eventTime, + action, + /* pointerCount= */ 1, + properties, + coords, + /* metaState= */ 0, + /* buttonState= */ 0, + /* xPrecision = */ 1f, + /* yPrecision = */ 1f, + /* deviceId = */ 0, + /* edgeFlags = */ 0, + inputMethod.source, + /* flags = */ 0 + ) + event.displayId = 0 + return event + } + + companion object { + private const val MOTION_EVENT_INJECTION_DELAY_MILLIS = 5L + private const val REGULAR_CLICK_LENGTH = 100L + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index 43fd57bf39aa..931e4f88aa8d 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -269,9 +269,23 @@ open class PipAppHelper(instrumentation: Instrumentation) : /** Expand the PIP window back to full screen via intent and wait until the app is visible */ fun exitPipToFullScreenViaIntent(wmHelper: WindowManagerStateHelper) = launchViaIntent(wmHelper) - fun changeAspectRatio() { + fun changeAspectRatio(wmHelper: WindowManagerStateHelper) { val intent = Intent("com.android.wm.shell.flicker.testapp.ASPECT_RATIO") context.sendBroadcast(intent) + // Wait on WMHelper on size change upon aspect ratio change + val windowRect = getWindowRect(wmHelper) + wmHelper + .StateSyncBuilder() + .add("pipAspectRatioChanged") { + val pipAppWindow = + it.wmState.visibleWindows.firstOrNull { window -> + this.windowMatchesAnyOf(window) + } + ?: return@add false + val pipRegion = pipAppWindow.frameRegion + return@add pipRegion != Region(windowRect) + } + .waitForAndVerify() } fun clickEnterPipButton(wmHelper: WindowManagerStateHelper) { diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt new file mode 100644 index 000000000000..69fde0168b14 --- /dev/null +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.helpers + +import android.app.Instrumentation +import android.tools.device.apphelpers.StandardAppHelper +import android.tools.helpers.SYSTEMUI_PACKAGE +import android.tools.traces.component.ComponentNameMatcher +import android.tools.traces.parsers.WindowManagerStateHelper +import android.tools.traces.parsers.toFlickerComponent +import android.util.Log +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.UiObjectNotFoundException +import androidx.test.uiautomator.UiScrollable +import androidx.test.uiautomator.UiSelector +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.testapp.ActivityOptions +import java.util.regex.Pattern + +class StartMediaProjectionAppHelper +@JvmOverloads +constructor( + instr: Instrumentation, + launcherName: String = ActivityOptions.StartMediaProjectionActivity.LABEL, + component: ComponentNameMatcher = + ActivityOptions.StartMediaProjectionActivity.COMPONENT.toFlickerComponent() +) : StandardAppHelper(instr, launcherName, component) { + private val packageManager = instr.context.packageManager + + fun startEntireScreenMediaProjection(wmHelper: WindowManagerStateHelper) { + clickStartMediaProjectionButton() + chooseEntireScreenOption() + startScreenSharing() + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + } + + fun startSingleAppMediaProjection( + wmHelper: WindowManagerStateHelper, + targetApp: StandardAppHelper + ) { + clickStartMediaProjectionButton() + chooseSingleAppOption() + startScreenSharing() + selectTargetApp(targetApp.appName) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceAppeared(targetApp) + .waitForAndVerify() + } + + private fun clickStartMediaProjectionButton() { + findObject(By.res(packageName, START_MEDIA_PROJECTION_BUTTON_ID)).also { it.click() } + } + + private fun chooseEntireScreenOption() { + findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() } + + val entireScreenString = getSysUiResourceString(ENTIRE_SCREEN_STRING_RES_NAME) + findObject(By.text(entireScreenString)).also { it.click() } + } + + private fun selectTargetApp(targetAppName: String) { + // Scroll to to find target app to launch then click app icon it to start capture + val scrollable = UiScrollable(UiSelector().scrollable(true)) + try { + scrollable.scrollForward() + if (!scrollable.scrollIntoView(UiSelector().text(targetAppName))) { + Log.e(TAG, "Didn't find target app when scrolling") + return + } + } catch (e: UiObjectNotFoundException) { + Log.d(TAG, "There was no scrolling (UI may not be scrollable") + } + + findObject(By.text(targetAppName)).also { it.click() } + } + + private fun chooseSingleAppOption() { + findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() } + + val singleAppString = getSysUiResourceString(SINGLE_APP_STRING_RES_NAME) + findObject(By.text(singleAppString)).also { it.click() } + } + + private fun startScreenSharing() { + findObject(By.res(ACCEPT_RESOURCE_ID)).also { it.click() } + } + + private fun findObject(selector: BySelector): UiObject2 = + uiDevice.wait(Until.findObject(selector), TIMEOUT) ?: error("Can't find object $selector") + + private fun getSysUiResourceString(resName: String): String = + with(packageManager.getResourcesForApplication(SYSTEMUI_PACKAGE)) { + getString(getIdentifier(resName, "string", SYSTEMUI_PACKAGE)) + } + + companion object { + const val TAG: String = "StartMediaProjectionAppHelper" + const val TIMEOUT: Long = 5000L + const val ACCEPT_RESOURCE_ID: String = "android:id/button1" + const val START_MEDIA_PROJECTION_BUTTON_ID: String = "button_start_mp" + val SCREEN_SHARE_OPTIONS_PATTERN: Pattern = + Pattern.compile("$SYSTEMUI_PACKAGE:id/screen_share_mode_(options|spinner)") + const val ENTIRE_SCREEN_STRING_RES_NAME: String = + "screen_share_permission_dialog_option_entire_screen" + const val SINGLE_APP_STRING_RES_NAME: String = + "screen_share_permission_dialog_option_single_app" + } +} diff --git a/tests/FlickerTests/test-apps/flickerapp/Android.bp b/tests/FlickerTests/test-apps/flickerapp/Android.bp index e3b23b986c83..c55df8604362 100644 --- a/tests/FlickerTests/test-apps/flickerapp/Android.bp +++ b/tests/FlickerTests/test-apps/flickerapp/Android.bp @@ -19,6 +19,7 @@ package { // to get the below license kinds: // SPDX-license-identifier-Apache-2.0 default_applicable_licenses: ["frameworks_base_license"], + default_team: "trendy_team_windowing_tools", } android_test { @@ -46,6 +47,7 @@ android_test { "wm-flicker-common-app-helpers", "wm-flicker-common-assertions", "wm-flicker-window-extensions", + "wm-shell-flicker-utils", ], } diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index 45260bddd355..9ce8e807f612 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -21,6 +21,15 @@ android:targetSdkVersion="35"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> + <uses-permission android:name="android.permission.INSTANT_APP_FOREGROUND_SERVICE"/> + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"/> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> <application android:allowBackup="false" android:supportsRtl="true"> @@ -106,6 +115,30 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <activity android:name=".NonResizeableFixedAspectRatioPortraitActivity" + android:theme="@style/CutoutNever" + android:resizeableActivity="false" + android:screenOrientation="portrait" + android:minAspectRatio="1.77" + android:taskAffinity="com.android.server.wm.flicker.testapp.NonResizeableFixedAspectRatioPortraitActivity" + android:label="NonResizeableFixedAspectRatioPortraitActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".StartMediaProjectionActivity" + android:theme="@style/CutoutNever" + android:resizeableActivity="false" + android:taskAffinity="com.android.server.wm.flicker.testapp.StartMediaProjectionActivity" + android:label="StartMediaProjectionActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> <activity android:name=".PortraitImmersiveActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.PortraitImmersiveActivity" android:immersive="true" @@ -123,6 +156,7 @@ <activity android:name=".LaunchTransparentActivity" android:resizeableActivity="false" android:screenOrientation="portrait" + android:minAspectRatio="1.77" android:theme="@style/OptOutEdgeToEdge" android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchTransparentActivity" android:label="LaunchTransparentActivity" @@ -318,7 +352,8 @@ android:taskAffinity="com.android.server.wm.flicker.testapp.SplitScreenActivity" android:theme="@style/CutoutShortEdges" android:label="SplitScreenPrimaryActivity" - android:exported="true"> + android:exported="true" + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> @@ -329,7 +364,8 @@ android:taskAffinity="com.android.server.wm.flicker.testapp.SplitScreenSecondaryActivity" android:theme="@style/CutoutShortEdges" android:label="SplitScreenSecondaryActivity" - android:exported="true"> + android:exported="true" + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> @@ -404,6 +440,11 @@ android:name="android.voice_interaction" android:resource="@xml/interaction_service"/> </service> + <service android:name="com.android.wm.shell.flicker.utils.MediaProjectionService" + android:foregroundServiceType="mediaProjection" + android:label="WMShellTestsMediaProjectionService" + android:enabled="true"> + </service> </application> <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/> </manifest> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml index 917aec1e809d..939ba81a47ea 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml @@ -21,8 +21,10 @@ android:background="@android:color/holo_orange_light"> <LinearLayout + android:id="@+id/launch_options_list" android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingBottom="48dp" android:orientation="vertical"> <Button diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml index 36cbf1a8fe84..365a0ea017f6 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml @@ -27,15 +27,6 @@ where things are arranged differently and to circle back up to the top once we reach the bottom. --> - <!-- View used for testing sourceRectHint. --> - <View - android:id="@+id/source_rect" - android:layout_width="320dp" - android:layout_height="180dp" - android:visibility="gone" - android:background="@android:color/holo_green_light" - /> - <Button android:id="@+id/enter_pip" android:layout_width="wrap_content" @@ -122,12 +113,11 @@ android:onClick="onRatioSelected"/> </RadioGroup> - <Button + <CheckBox android:id="@+id/set_source_rect_hint" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Set SourceRectHint" - android:onClick="setSourceRectHint"/> + android:text="Set SourceRectHint"/> <TextView android:layout_width="wrap_content" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml new file mode 100644 index 000000000000..46f01e6c9752 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:background="@android:color/holo_orange_light"> + + <Button + android:id="@+id/button_start_mp" + android:layout_width="500dp" + android:layout_height="500dp" + android:gravity="center_vertical|center_horizontal" + android:text="Start Media Projection" + android:textAppearance="?android:attr/textAppearanceLarge"/> + +</LinearLayout>
\ No newline at end of file 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 80c1dd072df7..73625da9dfa5 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 @@ -85,6 +85,18 @@ public class ActivityOptions { FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity"); } + public static class NonResizeableFixedAspectRatioPortraitActivity { + public static final String LABEL = "NonResizeableFixedAspectRatioPortraitActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".NonResizeableFixedAspectRatioPortraitActivity"); + } + + public static class StartMediaProjectionActivity { + public static final String LABEL = "StartMediaProjectionActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".StartMediaProjectionActivity"); + } + public static class PortraitImmersiveActivity { public static final String LABEL = "PortraitImmersiveActivity"; public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/GameActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/GameActivity.java index ef75d4ddcdcd..93254f7247b6 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/GameActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/GameActivity.java @@ -71,7 +71,7 @@ public class GameActivity extends Activity implements SurfaceHolder.Callback { windowInsetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE ); - // Hide both the status bar and the navigation bar. - windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()); + // Hide status bar only to avoid flakiness on gesture quick switch cases. + windowInsetsController.hide(WindowInsetsCompat.Type.statusBars()); } } diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java new file mode 100644 index 000000000000..be38c259d00d --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/NonResizeableFixedAspectRatioPortraitActivity.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.testapp; + +import android.app.Activity; +import android.os.Bundle; + +public class NonResizeableFixedAspectRatioPortraitActivity extends Activity { + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.activity_non_resizeable); + } +} diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java index 27eb5a06451a..13d7f7f0d521 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java @@ -43,10 +43,10 @@ import android.media.MediaMetadata; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.os.Bundle; +import android.util.DisplayMetrics; import android.util.Log; import android.util.Rational; import android.view.View; -import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.widget.CheckBox; @@ -70,7 +70,7 @@ public class PipActivity extends Activity { */ private static final String TITLE_STATE_PAUSED = "TestApp media is paused"; - private static final Rational RATIO_DEFAULT = null; + private static final Rational RATIO_DEFAULT = new Rational(16, 9); private static final Rational RATIO_SQUARE = new Rational(1, 1); private static final Rational RATIO_WIDE = new Rational(2, 1); private static final Rational RATIO_TALL = new Rational(1, 2); @@ -88,8 +88,7 @@ public class PipActivity extends Activity { "com.android.wm.shell.flicker.testapp.ASPECT_RATIO"; private final PictureInPictureParams.Builder mPipParamsBuilder = - new PictureInPictureParams.Builder() - .setAspectRatio(RATIO_DEFAULT); + new PictureInPictureParams.Builder(); private MediaSession mMediaSession; private final PlaybackState.Builder mPlaybackStateBuilder = new PlaybackState.Builder() .setActions(ACTION_PLAY | ACTION_PAUSE | ACTION_STOP) @@ -139,6 +138,9 @@ public class PipActivity extends Activity { } }; + private Rational mAspectRatio = RATIO_DEFAULT; + private boolean mEnableSourceRectHint; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -156,6 +158,14 @@ public class PipActivity extends Activity { findViewById(R.id.media_session_stop) .setOnClickListener(v -> updateMediaSessionState(STATE_STOPPED)); + final CheckBox setSourceRectHintCheckBox = findViewById(R.id.set_source_rect_hint); + setSourceRectHintCheckBox.setOnCheckedChangeListener((v, isChecked) -> { + if (mEnableSourceRectHint != isChecked) { + mEnableSourceRectHint = isChecked; + updateSourceRectHint(); + } + }); + mMediaSession = new MediaSession(this, "WMShell_TestApp"); mMediaSession.setPlaybackState(mPlaybackStateBuilder.build()); mMediaSession.setCallback(new MediaSession.Callback() { @@ -250,47 +260,64 @@ public class PipActivity extends Activity { } } + private void updateSourceRectHint() { + if (!mEnableSourceRectHint) return; + // Similar to PipUtils#getEnterPipWithOverlaySrcRectHint, crop the display bounds + // as source rect hint based on the current aspect ratio. + final DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); + final Rect displayBounds = new Rect(0, 0, + displayMetrics.widthPixels, displayMetrics.heightPixels); + final Rect sourceRectHint = getEnterPipWithOverlaySrcRectHint( + displayBounds, mAspectRatio.floatValue()); + mPipParamsBuilder + .setAspectRatio(mAspectRatio) + .setSourceRectHint(sourceRectHint); + setPictureInPictureParams(mPipParamsBuilder.build()); + } + /** - * Adds a temporary view used for testing sourceRectHint. - * + * Crop a Rect matches the aspect ratio and pivots at the center point. + * This is a counterpart of {@link PipUtils#getEnterPipWithOverlaySrcRectHint} */ - public void setSourceRectHint(View v) { - View rectView = findViewById(R.id.source_rect); - if (rectView != null) { - rectView.setVisibility(View.VISIBLE); - rectView.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - Rect boundingRect = new Rect(); - rectView.getGlobalVisibleRect(boundingRect); - mPipParamsBuilder.setSourceRectHint(boundingRect); - setPictureInPictureParams(mPipParamsBuilder.build()); - rectView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - }); - rectView.invalidate(); // changing the visibility, invalidating to redraw the view + private Rect getEnterPipWithOverlaySrcRectHint(Rect appBounds, float aspectRatio) { + final float appBoundsAspectRatio = appBounds.width() / (float) appBounds.height(); + final int width, height; + int left = appBounds.left; + int top = appBounds.top; + if (appBoundsAspectRatio < aspectRatio) { + width = appBounds.width(); + height = (int) (width / aspectRatio); + top = appBounds.top + (appBounds.height() - height) / 2; + } else { + height = appBounds.height(); + width = (int) (height * aspectRatio); + left = appBounds.left + (appBounds.width() - width) / 2; } + return new Rect(left, top, left + width, top + height); } public void onRatioSelected(View v) { switch (v.getId()) { case R.id.ratio_default: - mPipParamsBuilder.setAspectRatio(RATIO_DEFAULT); + mAspectRatio = RATIO_DEFAULT; break; case R.id.ratio_square: - mPipParamsBuilder.setAspectRatio(RATIO_SQUARE); + mAspectRatio = RATIO_SQUARE; break; case R.id.ratio_wide: - mPipParamsBuilder.setAspectRatio(RATIO_WIDE); + mAspectRatio = RATIO_WIDE; break; case R.id.ratio_tall: - mPipParamsBuilder.setAspectRatio(RATIO_TALL); + mAspectRatio = RATIO_TALL; break; } + setPictureInPictureParams(mPipParamsBuilder.setAspectRatio(mAspectRatio).build()); + if (mEnableSourceRectHint) { + updateSourceRectHint(); + } } private void updateMediaSessionState(int newState) { diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java new file mode 100644 index 000000000000..a24a48269d7c --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java @@ -0,0 +1,156 @@ +/* + * 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 com.android.server.wm.flicker.testapp; + +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.EXTRA_MESSENGER; +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.MSG_SERVICE_DESTROYED; +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.MSG_START_FOREGROUND_DONE; +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.hardware.display.VirtualDisplay; +import android.media.ImageReader; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Messenger; +import android.util.DisplayMetrics; +import android.util.Log; +import android.widget.Button; + +import com.android.wm.shell.flicker.utils.MediaProjectionService; + +public class StartMediaProjectionActivity extends Activity { + + private static final String TAG = "StartMediaProjectionActivity"; + private MediaProjectionManager mService; + private ImageReader mImageReader; + private VirtualDisplay mVirtualDisplay; + private MediaProjection mMediaProjection; + private MediaProjection.Callback mMediaProjectionCallback = new MediaProjection.Callback() { + @Override + public void onStop() { + super.onStop(); + } + + @Override + public void onCapturedContentResize(int width, int height) { + super.onCapturedContentResize(width, height); + } + + @Override + public void onCapturedContentVisibilityChanged(boolean isVisible) { + super.onCapturedContentVisibilityChanged(isVisible); + } + }; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + mService = getSystemService(MediaProjectionManager.class); + setContentView(R.layout.activity_start_media_projection); + + Button startMediaProjectionButton = findViewById(R.id.button_start_mp); + startMediaProjectionButton.setOnClickListener(v -> + startActivityForResult(mService.createScreenCaptureIntent(), REQUEST_CODE)); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode != REQUEST_CODE) { + throw new IllegalStateException("Unknown request code: " + requestCode); + } + if (resultCode != RESULT_OK) { + throw new IllegalStateException("User denied screen sharing permission"); + } + Log.d(TAG, "onActivityResult"); + startMediaProjectionService(resultCode, data); + } + + private void startMediaProjectionService(int resultCode, Intent resultData) { + final Messenger messenger = new Messenger(new Handler(Looper.getMainLooper(), + msg -> { + switch (msg.what) { + case MSG_START_FOREGROUND_DONE: + setupMediaProjection(resultCode, resultData); + return true; + case MSG_SERVICE_DESTROYED: + return true; + } + Log.e(TAG, "Unknown message from the FlickerMPService: " + msg.what); + return false; + } + )); + + final Intent intent = new Intent() + .setComponent(new ComponentName(this, MediaProjectionService.class)) + .putExtra(EXTRA_MESSENGER, messenger); + startForegroundService(intent); + } + + private void setupMediaProjection(int resultCode, Intent resultData) { + mMediaProjection = mService.getMediaProjection(resultCode, resultData); + if (mMediaProjection == null) { + throw new IllegalStateException("cannot create new MediaProjection"); + } + + mMediaProjection.registerCallback( + mMediaProjectionCallback, new Handler(Looper.getMainLooper())); + + Rect displayBounds = getWindowManager().getMaximumWindowMetrics().getBounds(); + mImageReader = ImageReader.newInstance( + displayBounds.width(), displayBounds.height(), PixelFormat.RGBA_8888, 1); + + mVirtualDisplay = mMediaProjection.createVirtualDisplay( + "DanielDisplay", + displayBounds.width(), + displayBounds.height(), + DisplayMetrics.DENSITY_HIGH, + /* flags= */ 0, + mImageReader.getSurface(), + new VirtualDisplay.Callback() { + @Override + public void onStopped() { + if (mMediaProjection != null) { + if (mMediaProjectionCallback != null) { + mMediaProjection.unregisterCallback(mMediaProjectionCallback); + mMediaProjectionCallback = null; + } + mMediaProjection.stop(); + mMediaProjection = null; + } + if (mImageReader != null) { + mImageReader = null; + } + if (mVirtualDisplay != null) { + mVirtualDisplay.getSurface().release(); + mVirtualDisplay.release(); + mVirtualDisplay = null; + } + } + }, + new Handler(Looper.getMainLooper()) + ); + } + +} diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp index 084f9bd6d682..168141bf6e7d 100644 --- a/tests/Input/Android.bp +++ b/tests/Input/Android.bp @@ -39,8 +39,10 @@ android_test { "flag-junit", "frameworks-base-testutils", "hamcrest-library", + "junit-params", "kotlin-test", - "mockito-target-minus-junit4", + "mockito-kotlin-nodeps", + "mockito-target-extended-minus-junit4", "platform-test-annotations", "platform-screenshot-diff-core", "services.core.unboosted", @@ -48,6 +50,7 @@ android_test { "testables", "testng", "truth", + "ui-trace-collector", ], libs: [ "android.test.mock.stubs.system", diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml index a05d08ccceba..914adc40194d 100644 --- a/tests/Input/AndroidManifest.xml +++ b/tests/Input/AndroidManifest.xml @@ -32,6 +32,14 @@ android:process=":externalProcess"> </activity> + <activity android:name="com.android.test.input.CaptureEventActivity" + android:label="Capture events" + android:configChanges="touchscreen|uiMode|orientation|screenSize|screenLayout|keyboardHidden|uiMode|navigation|keyboard|density|fontScale|layoutDirection|locale|mcc|mnc|smallestScreenSize" + android:enableOnBackInvokedCallback="false" + android:turnScreenOn="true" + android:exported="true"> + </activity> + </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.test.input" diff --git a/tests/Input/AndroidTest.xml b/tests/Input/AndroidTest.xml index 8db37058af2b..bc9322fbd3dc 100644 --- a/tests/Input/AndroidTest.xml +++ b/tests/Input/AndroidTest.xml @@ -22,6 +22,10 @@ <option name="shell-timeout" value="660s" /> <option name="test-timeout" value="600s" /> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="device-listeners" value="android.tools.collectors.DefaultUITraceListener"/> + <!-- DefaultUITraceListener args --> + <option name="instrumentation-arg" key="skip_test_success_metrics" value="true"/> + <option name="instrumentation-arg" key="per_class" value="true"/> </test> <object class="com.android.tradefed.testtype.suite.module.TestFailureModuleController" type="module_controller"> @@ -31,7 +35,9 @@ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="input_.*" /> <!-- Pull files created by tests, like the output of screenshot tests --> - <option name="directory-keys" value="/storage/emulated/0/InputTests" /> + <option name="directory-keys" value="/sdcard/Download/InputTests" /> + <!-- Pull perfetto traces from DefaultUITraceListener --> + <option name="pull-pattern-keys" value="perfetto_file_path*" /> <option name="collect-on-run-ended-only" value="false" /> </metrics_collector> </configuration> diff --git a/tests/Input/assets/testPointerFillStyle.png b/tests/Input/assets/testPointerFillStyle.png Binary files differindex b2354f8f4799..297244f9d6d1 100644 --- a/tests/Input/assets/testPointerFillStyle.png +++ b/tests/Input/assets/testPointerFillStyle.png diff --git a/tests/Input/assets/testPointerStrokeStyle.png b/tests/Input/assets/testPointerStrokeStyle.png Binary files differnew file mode 100644 index 000000000000..4ddde70b2f0a --- /dev/null +++ b/tests/Input/assets/testPointerStrokeStyle.png diff --git a/tests/Input/res/drawable/test_key_drawable.xml b/tests/Input/res/drawable/test_key_drawable.xml new file mode 100644 index 000000000000..2addf8fcf0bd --- /dev/null +++ b/tests/Input/res/drawable/test_key_drawable.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="4dp" /> + <solid android:color="#ffffffff"/> +</shape>
\ No newline at end of file diff --git a/tests/Input/res/drawable/test_modifier_drawable.xml b/tests/Input/res/drawable/test_modifier_drawable.xml new file mode 100644 index 000000000000..2addf8fcf0bd --- /dev/null +++ b/tests/Input/res/drawable/test_modifier_drawable.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="4dp" /> + <solid android:color="#ffffffff"/> +</shape>
\ No newline at end of file diff --git a/tests/Input/res/raw/google_pixel_tablet_touchscreen.evemu b/tests/Input/res/raw/google_pixel_tablet_touchscreen.evemu new file mode 100644 index 000000000000..1a9112b97301 --- /dev/null +++ b/tests/Input/res/raw/google_pixel_tablet_touchscreen.evemu @@ -0,0 +1,150 @@ +# EVEMU 1.2 +# One finger swipe gesture on the Google Pixel Tablet touchscreen +N: NVTCapacitiveTouchScreen +I: 001c 0603 7806 0100 +P: 02 00 00 00 00 00 00 00 +B: 00 0b 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 80 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 04 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 01 00 00 00 00 00 00 00 00 +B: 02 00 00 00 00 00 00 00 00 +B: 03 00 00 00 00 00 80 f3 46 +B: 04 00 00 00 00 00 00 00 00 +B: 05 00 00 00 00 00 00 00 00 +B: 11 00 00 00 00 00 00 00 00 +B: 12 00 00 00 00 00 00 00 00 +A: 2f 0 9 0 0 0 +A: 30 0 2559 0 0 11 +A: 31 0 1599 0 0 11 +A: 34 -4096 4096 0 0 0 +A: 35 0 1599 0 0 11 +A: 36 0 2559 0 0 11 +A: 37 0 2 0 0 0 +A: 39 0 65535 0 0 0 +A: 3a 0 256 0 0 0 +A: 3e 0 8 0 0 0 +E: 0.000001 0001 014a 0001 +E: 0.000001 0003 0039 0003 +E: 0.000001 0003 0035 0810 +E: 0.000001 0003 0036 1650 +E: 0.000001 0003 0030 0082 +E: 0.000001 0003 0031 0077 +E: 0.000001 0003 003a 0215 +E: 0.000001 0003 0034 3218 +E: 0.000001 0000 0000 0000 +E: 0.008818 0003 0035 0825 +E: 0.008818 0003 0036 1645 +E: 0.008818 0003 0034 3217 +E: 0.008818 0000 0000 0000 +E: 0.016306 0003 0035 0841 +E: 0.016306 0003 0036 1639 +E: 0.016306 0003 0034 3102 +E: 0.016306 0000 0000 0000 +E: 0.025653 0003 0035 0862 +E: 0.025653 0003 0036 1630 +E: 0.025653 0003 0034 3092 +E: 0.025653 0000 0000 0000 +E: 0.032936 0003 0035 0883 +E: 0.032936 0003 0036 1619 +E: 0.032936 0003 0034 3030 +E: 0.032936 0000 0000 0000 +E: 0.042072 0003 0035 0905 +E: 0.042072 0003 0036 1604 +E: 0.042072 0003 0034 2848 +E: 0.042072 0000 0000 0000 +E: 0.049569 0003 0035 0924 +E: 0.049569 0003 0036 1591 +E: 0.049569 0003 0034 2830 +E: 0.049569 0000 0000 0000 +E: 0.058706 0003 0035 0942 +E: 0.058706 0003 0036 1573 +E: 0.058706 0000 0000 0000 +E: 0.066207 0003 0035 0954 +E: 0.066207 0003 0036 1557 +E: 0.066207 0003 0034 2790 +E: 0.066207 0000 0000 0000 +E: 0.075337 0003 0035 0966 +E: 0.075337 0003 0036 1535 +E: 0.075337 0000 0000 0000 +E: 0.082841 0003 0035 0973 +E: 0.082841 0003 0036 1511 +E: 0.082841 0003 0034 2788 +E: 0.082841 0000 0000 0000 +E: 0.091972 0003 0035 0971 +E: 0.091972 0003 0036 1480 +E: 0.091972 0003 0034 2770 +E: 0.091972 0000 0000 0000 +E: 0.099474 0003 0035 0961 +E: 0.099474 0003 0036 1445 +E: 0.099474 0003 0034 2644 +E: 0.099474 0000 0000 0000 +E: 0.108631 0003 0035 0937 +E: 0.108631 0003 0036 1400 +E: 0.108631 0003 0030 0083 +E: 0.108631 0003 0034 2461 +E: 0.108631 0000 0000 0000 +E: 0.116109 0003 0035 0909 +E: 0.116109 0003 0036 1361 +E: 0.116109 0003 0034 2278 +E: 0.116109 0000 0000 0000 +E: 0.125263 0003 0035 0865 +E: 0.125263 0003 0036 1311 +E: 0.125263 0003 0034 2096 +E: 0.125263 0000 0000 0000 +E: 0.132741 0003 0035 0820 +E: 0.132741 0003 0036 1261 +E: 0.132741 0003 0034 2083 +E: 0.132741 0000 0000 0000 +E: 0.141876 0003 0035 0755 +E: 0.141876 0003 0036 1193 +E: 0.141876 0003 003a 0216 +E: 0.141876 0003 0034 2266 +E: 0.141876 0000 0000 0000 +E: 0.149376 0003 0035 0691 +E: 0.149376 0003 0036 1124 +E: 0.149376 0003 0034 2448 +E: 0.149376 0000 0000 0000 +E: 0.158510 0003 0035 0609 +E: 0.158510 0003 0036 1033 +E: 0.158510 0003 0034 2631 +E: 0.158510 0000 0000 0000 +E: 0.166011 0003 0035 0543 +E: 0.166011 0003 0036 0957 +E: 0.166011 0003 0034 2813 +E: 0.166011 0000 0000 0000 +E: 0.175182 0003 0035 0471 +E: 0.175182 0003 0036 0864 +E: 0.175182 0003 0031 0076 +E: 0.175182 0003 0034 2996 +E: 0.175182 0000 0000 0000 +E: 0.182683 0003 0035 0417 +E: 0.182683 0003 0036 0792 +E: 0.182683 0003 003a 0214 +E: 0.182683 0003 0034 3178 +E: 0.182683 0000 0000 0000 +E: 0.191777 0003 0035 0361 +E: 0.191777 0003 0036 0719 +E: 0.191777 0003 0031 0075 +E: 0.191777 0003 003a 0213 +E: 0.191777 0003 0034 2996 +E: 0.191777 0000 0000 0000 +E: 0.199431 0003 0035 0271 +E: 0.199431 0003 0036 0603 +E: 0.199431 0003 0030 0060 +E: 0.199431 0003 0031 0029 +E: 0.199431 0003 003a 0060 +E: 0.199431 0003 0034 2813 +E: 0.199431 0000 0000 0000 +E: 0.207943 0003 003a 0000 +E: 0.207943 0003 0039 -001 +E: 0.207943 0001 014a 0000 +E: 0.207943 0000 0000 0000 diff --git a/tests/Input/res/raw/google_pixel_tablet_touchscreen_events.json b/tests/Input/res/raw/google_pixel_tablet_touchscreen_events.json new file mode 100644 index 000000000000..df4f9fb4e1df --- /dev/null +++ b/tests/Input/res/raw/google_pixel_tablet_touchscreen_events.json @@ -0,0 +1,34 @@ +[ + { + "name": "One finger swipe", + "source": "TOUCHSCREEN", + "events": [ + {"action":"DOWN","axes":{"AXIS_X":810,"AXIS_Y":1650,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.234087586402893},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":825,"AXIS_Y":1645,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.2337040901184082},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":841,"AXIS_Y":1639,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.1896021366119385},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":862,"AXIS_Y":1630,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.1857671737670898},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":883,"AXIS_Y":1619,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.1619905233383179},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":905,"AXIS_Y":1604,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.0921943187713623},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":924,"AXIS_Y":1591,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.0852913856506348},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":942,"AXIS_Y":1573,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.0852913856506348},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":954,"AXIS_Y":1557,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.0699516534805298},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":966,"AXIS_Y":1535,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.0699516534805298},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":973,"AXIS_Y":1511,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.06918466091156},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":971,"AXIS_Y":1480,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.0622817277908325},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":961,"AXIS_Y":1445,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":82,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":82,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.0139613151550293},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":937,"AXIS_Y":1400,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03126221150159836,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":0.9437817335128784},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":909,"AXIS_Y":1361,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03126221150159836,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":0.8736020922660828},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":865,"AXIS_Y":1311,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03126221150159836,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":0.803805947303772},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":820,"AXIS_Y":1261,"AXIS_PRESSURE":0.83984375,"AXIS_SIZE":0.03126221150159836,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":0.7988204956054688},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":755,"AXIS_Y":1193,"AXIS_PRESSURE":0.84375,"AXIS_SIZE":0.03126221150159836,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":0.8690001368522644},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":691,"AXIS_Y":1124,"AXIS_PRESSURE":0.84375,"AXIS_SIZE":0.03126221150159836,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":0.9387962818145752},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":609,"AXIS_Y":1033,"AXIS_PRESSURE":0.84375,"AXIS_SIZE":0.03126221150159836,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.008975863456726},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":543,"AXIS_Y":957,"AXIS_PRESSURE":0.84375,"AXIS_SIZE":0.03126221150159836,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":77,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":77,"AXIS_ORIENTATION":1.0787720680236816},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":471,"AXIS_Y":864,"AXIS_PRESSURE":0.84375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":76,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":76,"AXIS_ORIENTATION":1.1489516496658325},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":417,"AXIS_Y":792,"AXIS_PRESSURE":0.8359375,"AXIS_SIZE":0.03106682375073433,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":76,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":76,"AXIS_ORIENTATION":1.2187477350234985},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":361,"AXIS_Y":719,"AXIS_PRESSURE":0.83203125,"AXIS_SIZE":0.03087143413722515,"AXIS_TOUCH_MAJOR":83,"AXIS_TOUCH_MINOR":75,"AXIS_TOOL_MAJOR":83,"AXIS_TOOL_MINOR":75,"AXIS_ORIENTATION":1.1489516496658325},"buttonState":[]}, + {"action":"MOVE","axes":{"AXIS_X":271,"AXIS_Y":603,"AXIS_PRESSURE":0.234375,"AXIS_SIZE":0.017389604821801186,"AXIS_TOUCH_MAJOR":60,"AXIS_TOUCH_MINOR":29,"AXIS_TOOL_MAJOR":60,"AXIS_TOOL_MINOR":29,"AXIS_ORIENTATION":1.0787720680236816},"buttonState":[]}, + {"action":"UP","axes":{"AXIS_X":271,"AXIS_Y":603,"AXIS_PRESSURE":0.234375,"AXIS_SIZE":0.017389604821801186,"AXIS_TOUCH_MAJOR":60,"AXIS_TOUCH_MINOR":29,"AXIS_TOOL_MAJOR":60,"AXIS_TOOL_MINOR":29,"AXIS_ORIENTATION":1.0787720680236816},"buttonState":[]} + ] + } +] diff --git a/tests/Input/res/xml/bookmarks.xml b/tests/Input/res/xml/bookmarks.xml new file mode 100644 index 000000000000..ba3f1871cdec --- /dev/null +++ b/tests/Input/res/xml/bookmarks.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<bookmarks> + <!-- the key combinations for the following shortcuts must be in sync + with the key combinations sent by the test in KeyGestureControllerTests.java --> + <bookmark + role="android.app.role.BROWSER" + shortcut="b" /> + <bookmark + category="android.intent.category.APP_CONTACTS" + shortcut="c" /> + <bookmark + category="android.intent.category.APP_EMAIL" + shortcut="e" /> + <bookmark + category="android.intent.category.APP_CALENDAR" + shortcut="k" /> + <bookmark + category="android.intent.category.APP_MAPS" + shortcut="m" /> + <bookmark + category="android.intent.category.APP_MUSIC" + shortcut="p" /> + <bookmark + role="android.app.role.SMS" + shortcut="s" /> + <bookmark + category="android.intent.category.APP_CALCULATOR" + shortcut="u" /> + + <bookmark + role="android.app.role.BROWSER" + shortcut="b" + shift="true" /> + + <bookmark + category="android.intent.category.APP_CONTACTS" + shortcut="c" + shift="true" /> + + <bookmark + package="com.test" + class="com.test.BookmarkTest" + shortcut="j" + shift="true" /> +</bookmarks>
\ No newline at end of file diff --git a/tests/Input/res/xml/keyboard_glyph_maps.xml b/tests/Input/res/xml/keyboard_glyph_maps.xml new file mode 100644 index 000000000000..42561c1a9923 --- /dev/null +++ b/tests/Input/res/xml/keyboard_glyph_maps.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<keyboard-glyph-maps xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <keyboard-glyph-map + androidprv:glyphMap="@xml/test_glyph_map" + androidprv:vendorId="0x1234" + androidprv:productId="0x3456" /> + <keyboard-glyph-map + androidprv:glyphMap="@xml/test_glyph_map2" + androidprv:vendorId="0x1235" + androidprv:productId="0x3457" /> +</keyboard-glyph-maps>
\ No newline at end of file diff --git a/tests/Input/res/xml/test_glyph_map.xml b/tests/Input/res/xml/test_glyph_map.xml new file mode 100644 index 000000000000..7a7c1accb7fd --- /dev/null +++ b/tests/Input/res/xml/test_glyph_map.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<keyboard-glyph-map xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <key-glyph + androidprv:keycode="KEYCODE_BACK" + androidprv:glyphDrawable="@drawable/test_key_drawable" /> + <modifier-glyph + androidprv:modifier="META" + androidprv:glyphDrawable="@drawable/test_modifier_drawable" /> + <function-row-key androidprv:keycode="KEYCODE_EMOJI_PICKER" /> + <hardware-defined-shortcut + androidprv:keycode="KEYCODE_1" + androidprv:modifierState="FUNCTION" + androidprv:outKeycode="KEYCODE_BACK" /> + <hardware-defined-shortcut + androidprv:keycode="KEYCODE_2" + androidprv:modifierState="FUNCTION|META" + androidprv:outKeycode="KEYCODE_HOME" /> +</keyboard-glyph-map>
\ No newline at end of file diff --git a/tests/Input/res/xml/test_glyph_map2.xml b/tests/Input/res/xml/test_glyph_map2.xml new file mode 100644 index 000000000000..7a7c1accb7fd --- /dev/null +++ b/tests/Input/res/xml/test_glyph_map2.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<keyboard-glyph-map xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <key-glyph + androidprv:keycode="KEYCODE_BACK" + androidprv:glyphDrawable="@drawable/test_key_drawable" /> + <modifier-glyph + androidprv:modifier="META" + androidprv:glyphDrawable="@drawable/test_modifier_drawable" /> + <function-row-key androidprv:keycode="KEYCODE_EMOJI_PICKER" /> + <hardware-defined-shortcut + androidprv:keycode="KEYCODE_1" + androidprv:modifierState="FUNCTION" + androidprv:outKeycode="KEYCODE_BACK" /> + <hardware-defined-shortcut + androidprv:keycode="KEYCODE_2" + androidprv:modifierState="FUNCTION|META" + androidprv:outKeycode="KEYCODE_HOME" /> +</keyboard-glyph-map>
\ No newline at end of file diff --git a/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt b/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt index 90dff47ab706..a1e165551b5b 100644 --- a/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt +++ b/tests/Input/src/android/hardware/input/InputDeviceBatteryListenerTest.kt @@ -24,17 +24,16 @@ import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import androidx.test.core.app.ApplicationProvider import com.android.server.testutils.any +import com.android.test.input.MockInputManagerRule 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 @@ -61,9 +60,8 @@ class InputDeviceBatteryListenerTest { private lateinit var context: Context private lateinit var inputManager: InputManager - @Mock - private lateinit var iInputManagerMock: IInputManager - private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + @get:Rule + val inputManagerRule = MockInputManagerRule() @Before fun setUp() { @@ -72,7 +70,6 @@ class InputDeviceBatteryListenerTest { 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) @@ -92,7 +89,7 @@ class InputDeviceBatteryListenerTest { monitoredDevices.add(deviceId) registeredListener = listener null - }.`when`(iInputManagerMock).registerBatteryListener(anyInt(), any()) + }.`when`(inputManagerRule.mock).registerBatteryListener(anyInt(), any()) // Handle battery listener being unregistered. doAnswer { @@ -108,14 +105,7 @@ class InputDeviceBatteryListenerTest { if (monitoredDevices.isEmpty()) { registeredListener = null } - }.`when`(iInputManagerMock).unregisterBatteryListener(anyInt(), any()) - } - - @After - fun tearDown() { - if (this::inputManagerGlobalSession.isInitialized) { - inputManagerGlobalSession.close() - } + }.`when`(inputManagerRule.mock).unregisterBatteryListener(anyInt(), any()) } private fun notifyBatteryStateChanged( diff --git a/tests/Input/src/android/hardware/input/InputDeviceLightsManagerTest.java b/tests/Input/src/android/hardware/input/InputDeviceLightsManagerTest.java index 080186e4a2c1..3fc9ce12e718 100644 --- a/tests/Input/src/android/hardware/input/InputDeviceLightsManagerTest.java +++ b/tests/Input/src/android/hardware/input/InputDeviceLightsManagerTest.java @@ -45,15 +45,14 @@ import android.view.InputDevice; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.test.input.MockInputManagerRule; + 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; @@ -73,23 +72,22 @@ public class InputDeviceLightsManagerTest { private static final int DEVICE_ID = 1000; private static final int PLAYER_ID = 3; - @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + @Rule + public final MockInputManagerRule mInputManagerRule = new MockInputManagerRule(); 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(mInputManagerRule.getMock().getInputDeviceIds()).thenReturn(new int[]{DEVICE_ID}); - when(mIInputManagerMock.getInputDevice(eq(DEVICE_ID))).thenReturn( + when(mInputManagerRule.getMock().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); @@ -102,7 +100,7 @@ public class InputDeviceLightsManagerTest { lightStatesById.put(lightIds[i], lightStates[i]); } return null; - }).when(mIInputManagerMock).setLightStates(eq(DEVICE_ID), + }).when(mInputManagerRule.getMock()).setLightStates(eq(DEVICE_ID), any(int[].class), any(LightState[].class), any(IBinder.class)); doAnswer(invocation -> { @@ -111,7 +109,7 @@ public class InputDeviceLightsManagerTest { return lightStatesById.get(lightId); } return new LightState(0); - }).when(mIInputManagerMock).getLightState(eq(DEVICE_ID), anyInt()); + }).when(mInputManagerRule.getMock()).getLightState(eq(DEVICE_ID), anyInt()); } @After @@ -130,7 +128,7 @@ public class InputDeviceLightsManagerTest { private void mockLights(Light[] lights) throws Exception { // Mock the Lights returned form InputManagerService - when(mIInputManagerMock.getLights(eq(DEVICE_ID))).thenReturn( + when(mInputManagerRule.getMock().getLights(eq(DEVICE_ID))).thenReturn( new ArrayList(Arrays.asList(lights))); } @@ -151,7 +149,7 @@ public class InputDeviceLightsManagerTest { LightsManager lightsManager = device.getLightsManager(); List<Light> lights = lightsManager.getLights(); - verify(mIInputManagerMock).getLights(eq(DEVICE_ID)); + verify(mInputManagerRule.getMock()).getLights(eq(DEVICE_ID)); assertEquals(lights, Arrays.asList(mockedLights)); } @@ -185,9 +183,9 @@ public class InputDeviceLightsManagerTest { .build()); IBinder token = session.getToken(); - verify(mIInputManagerMock).openLightSession(eq(DEVICE_ID), + verify(mInputManagerRule.getMock()).openLightSession(eq(DEVICE_ID), any(String.class), eq(token)); - verify(mIInputManagerMock).setLightStates(eq(DEVICE_ID), eq(new int[]{1, 2, 3}), + verify(mInputManagerRule.getMock()).setLightStates(eq(DEVICE_ID), eq(new int[]{1, 2, 3}), eq(states), eq(token)); // Then all 3 should turn on. @@ -204,7 +202,7 @@ public class InputDeviceLightsManagerTest { // close session session.close(); - verify(mIInputManagerMock).closeLightSession(eq(DEVICE_ID), eq(token)); + verify(mInputManagerRule.getMock()).closeLightSession(eq(DEVICE_ID), eq(token)); } @Test @@ -232,9 +230,9 @@ public class InputDeviceLightsManagerTest { .build()); IBinder token = session.getToken(); - verify(mIInputManagerMock).openLightSession(eq(DEVICE_ID), + verify(mInputManagerRule.getMock()).openLightSession(eq(DEVICE_ID), any(String.class), eq(token)); - verify(mIInputManagerMock).setLightStates(eq(DEVICE_ID), eq(new int[]{1}), + verify(mInputManagerRule.getMock()).setLightStates(eq(DEVICE_ID), eq(new int[]{1}), eq(states), eq(token)); // Verify the light state @@ -245,7 +243,7 @@ public class InputDeviceLightsManagerTest { // close session session.close(); - verify(mIInputManagerMock).closeLightSession(eq(DEVICE_ID), eq(token)); + verify(mInputManagerRule.getMock()).closeLightSession(eq(DEVICE_ID), eq(token)); } @Test diff --git a/tests/Input/src/android/hardware/input/InputDeviceSensorManagerTest.java b/tests/Input/src/android/hardware/input/InputDeviceSensorManagerTest.java index 0e3c200699d2..3057f5ddb540 100644 --- a/tests/Input/src/android/hardware/input/InputDeviceSensorManagerTest.java +++ b/tests/Input/src/android/hardware/input/InputDeviceSensorManagerTest.java @@ -41,16 +41,13 @@ import android.view.InputDevice; import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.annotations.GuardedBy; +import com.android.test.input.MockInputManagerRule; -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; @@ -70,43 +67,34 @@ public class InputDeviceSensorManagerTest { private static final int DEVICE_ID = 1000; - @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + @Rule + public final MockInputManagerRule mInputManagerRule = new MockInputManagerRule(); 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(mInputManagerRule.getMock().getInputDeviceIds()).thenReturn(new int[]{DEVICE_ID}); - when(mIInputManagerMock.getInputDevice(eq(DEVICE_ID))).thenReturn( + when(mInputManagerRule.getMock().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(mInputManagerRule.getMock().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())) + when(mInputManagerRule.getMock().enableSensor(eq(DEVICE_ID), anyInt(), anyInt(), anyInt())) .thenReturn(true); - when(mIInputManagerMock.registerSensorListener(any())).thenReturn(true); - } - - @After - public void tearDown() { - if (mInputManagerGlobalSession != null) { - mInputManagerGlobalSession.close(); - } + when(mInputManagerRule.getMock().registerSensorListener(any())).thenReturn(true); } private class InputTestSensorEventListener implements SensorEventListener { @@ -175,13 +163,13 @@ public class InputDeviceSensorManagerTest { SensorManager sensorManager = device.getSensorManager(); List<Sensor> accelList = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER); - verify(mIInputManagerMock).getSensorList(eq(DEVICE_ID)); + verify(mInputManagerRule.getMock()).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)); + verify(mInputManagerRule.getMock()).getSensorList(eq(DEVICE_ID)); assertEquals(1, gyroList.size()); assertEquals(DEVICE_ID, gyroList.get(0).getId()); assertEquals(Sensor.TYPE_GYROSCOPE, gyroList.get(0).getType()); @@ -197,11 +185,11 @@ public class InputDeviceSensorManagerTest { List<Sensor> gameRotationList = sensorManager.getSensorList( Sensor.TYPE_GAME_ROTATION_VECTOR); - verify(mIInputManagerMock).getSensorList(eq(DEVICE_ID)); + verify(mInputManagerRule.getMock()).getSensorList(eq(DEVICE_ID)); assertEquals(0, gameRotationList.size()); List<Sensor> gravityList = sensorManager.getSensorList(Sensor.TYPE_GRAVITY); - verify(mIInputManagerMock).getSensorList(eq(DEVICE_ID)); + verify(mInputManagerRule.getMock()).getSensorList(eq(DEVICE_ID)); assertEquals(0, gravityList.size()); } @@ -218,13 +206,13 @@ public class InputDeviceSensorManagerTest { mIInputSensorEventListener = invocation.getArgument(0); assertNotNull(mIInputSensorEventListener); return true; - }).when(mIInputManagerMock).registerSensorListener(any()); + }).when(mInputManagerRule.getMock()).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()), + verify(mInputManagerRule.getMock()).registerSensorListener(any()); + verify(mInputManagerRule.getMock()).enableSensor(eq(DEVICE_ID), eq(sensor.getType()), anyInt(), anyInt()); float[] values = new float[] {0.12f, 9.8f, 0.2f}; @@ -240,7 +228,7 @@ public class InputDeviceSensorManagerTest { } sensorManager.unregisterListener(listener); - verify(mIInputManagerMock).disableSensor(eq(DEVICE_ID), eq(sensor.getType())); + verify(mInputManagerRule.getMock()).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 index 152dde94f006..4c6bb849155c 100644 --- a/tests/Input/src/android/hardware/input/InputManagerTest.kt +++ b/tests/Input/src/android/hardware/input/InputManagerTest.kt @@ -23,18 +23,16 @@ 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 com.android.test.input.MockInputManagerRule import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull 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 /** @@ -54,35 +52,23 @@ class InputManagerTest { } @get:Rule - val rule = MockitoJUnit.rule()!! + val inputManagerRule = MockInputManagerRule() 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 { + `when`(inputManagerRule.mock.inputDeviceIds).then { deviceGenerationMap.keys.toIntArray() } } - @After - fun tearDown() { - if (this::inputManagerGlobalSession.isInitialized) { - inputManagerGlobalSession.close() - } - } - private fun notifyDeviceChanged( deviceId: Int, associatedDisplayId: Int, @@ -92,7 +78,7 @@ class InputManagerTest { ?: throw IllegalArgumentException("Device $deviceId was never added!") deviceGenerationMap[deviceId] = generation - `when`(iInputManager.getInputDevice(deviceId)) + `when`(inputManagerRule.mock.getInputDevice(deviceId)) .thenReturn(createInputDevice(deviceId, associatedDisplayId, usiVersion, generation)) val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) } if (::devicesChangedListener.isInitialized) { @@ -125,7 +111,7 @@ class InputManagerTest { fun testUsiVersionFallBackToDisplayConfig() { addInputDevice(DEVICE_ID, Display.DEFAULT_DISPLAY, null) - `when`(iInputManager.getHostUsiVersionFromDisplayConfig(eq(42))) + `when`(inputManagerRule.mock.getHostUsiVersionFromDisplayConfig(eq(42))) .thenReturn(HostUsiVersion(9, 8)) val usiVersion = inputManager.getHostUsiVersion(createDisplay(42)) assertEquals(HostUsiVersion(9, 8), usiVersion) diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt new file mode 100644 index 000000000000..e99c81493394 --- /dev/null +++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input + +import android.content.Context +import android.content.ContextWrapper +import android.os.IBinder +import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import com.android.server.testutils.any +import com.android.test.input.MockInputManagerRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail + +/** + * Tests for [InputManager.KeyGestureEventHandler]. + * + * Build/Install/Run: + * atest InputTests:KeyGestureEventHandlerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class KeyGestureEventHandlerTest { + + companion object { + const val DEVICE_ID = 1 + val HOME_GESTURE_EVENT = KeyGestureEvent.Builder() + .setDeviceId(DEVICE_ID) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_H)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + val BACK_GESTURE_EVENT = KeyGestureEvent.Builder() + .setDeviceId(DEVICE_ID) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_DEL)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() + } + + @get:Rule + val rule = SetFlagsRule() + @get:Rule + val inputManagerRule = MockInputManagerRule() + + private var registeredListener: IKeyGestureHandler? = null + private lateinit var context: Context + private lateinit var inputManager: InputManager + + @Before + fun setUp() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + inputManager = InputManager(context) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + // Handle key gesture handler registration. + doAnswer { + val listener = it.getArgument(0) as IKeyGestureHandler + if (registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder()) { + // There can only be one registered key gesture handler per process. + fail("Trying to register a new listener when one already exists") + } + registeredListener = listener + null + }.`when`(inputManagerRule.mock).registerKeyGestureHandler(any()) + + // Handle key gesture handler being unregistered. + doAnswer { + val listener = it.getArgument(0) as IKeyGestureHandler + if (registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder()) { + fail("Trying to unregister a listener that is not registered") + } + registeredListener = null + null + }.`when`(inputManagerRule.mock).unregisterKeyGestureHandler(any()) + } + + private fun handleKeyGestureEvent(event: KeyGestureEvent) { + val eventToSend = AidlKeyGestureEvent() + eventToSend.deviceId = event.deviceId + eventToSend.keycodes = event.keycodes + eventToSend.modifierState = event.modifierState + eventToSend.gestureType = event.keyGestureType + eventToSend.action = event.action + eventToSend.displayId = event.displayId + eventToSend.flags = event.flags + registeredListener!!.handleKeyGesture(eventToSend, null) + } + + @Test + fun testHandlerHasCorrectGestureNotified() { + var callbackCount = 0 + + // Add a key gesture event listener + inputManager.registerKeyGestureEventHandler(KeyGestureHandler { event, _ -> + assertEquals(HOME_GESTURE_EVENT, event) + callbackCount++ + true + }) + + // Request handling for key gesture event will notify the handler. + handleKeyGestureEvent(HOME_GESTURE_EVENT) + assertEquals(1, callbackCount) + } + + @Test + fun testAddingHandlersRegistersInternalCallbackHandler() { + // Set up two callbacks. + val callback1 = KeyGestureHandler { _, _ -> false } + val callback2 = KeyGestureHandler { _, _ -> false } + + assertNull(registeredListener) + + // Adding the handler should register the callback with InputManagerService. + inputManager.registerKeyGestureEventHandler(callback1) + assertNotNull(registeredListener) + + // Adding another handler should not register new internal listener. + val currListener = registeredListener + inputManager.registerKeyGestureEventHandler(callback2) + assertEquals(currListener, registeredListener) + } + + @Test + fun testRemovingHandlersUnregistersInternalCallbackHandler() { + // Set up two callbacks. + val callback1 = KeyGestureHandler { _, _ -> false } + val callback2 = KeyGestureHandler { _, _ -> false } + + inputManager.registerKeyGestureEventHandler(callback1) + inputManager.registerKeyGestureEventHandler(callback2) + + // Only removing all handlers should remove the internal callback + inputManager.unregisterKeyGestureEventHandler(callback1) + assertNotNull(registeredListener) + inputManager.unregisterKeyGestureEventHandler(callback2) + assertNull(registeredListener) + } + + @Test + fun testMultipleHandlers() { + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + // Handler 1 captures all home gestures + val callback1 = KeyGestureHandler { event, _ -> + callbackCount1++ + event.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_HOME + } + // Handler 2 captures all gestures + val callback2 = KeyGestureHandler { _, _ -> + callbackCount2++ + true + } + + // Add both key gesture event handlers + inputManager.registerKeyGestureEventHandler(callback1) + inputManager.registerKeyGestureEventHandler(callback2) + + // Request handling for key gesture event, should notify callbacks in order. So, only the + // first handler should receive a callback since it captures the event. + handleKeyGestureEvent(HOME_GESTURE_EVENT) + assertEquals(1, callbackCount1) + assertEquals(0, callbackCount2) + + // Second handler should receive the event since the first handler doesn't capture the event + handleKeyGestureEvent(BACK_GESTURE_EVENT) + assertEquals(2, callbackCount1) + assertEquals(1, callbackCount2) + + inputManager.unregisterKeyGestureEventHandler(callback1) + // Request handling for key gesture event, should still trigger callback2 but not callback1. + handleKeyGestureEvent(HOME_GESTURE_EVENT) + assertEquals(2, callbackCount1) + assertEquals(2, callbackCount2) + } + + inner class KeyGestureHandler( + private var handler: (event: KeyGestureEvent, token: IBinder?) -> Boolean + ) : InputManager.KeyGestureEventHandler { + + override fun handleKeyGestureEvent( + event: KeyGestureEvent, + focusedToken: IBinder? + ): Boolean { + return handler(event, focusedToken) + } + + override fun isKeyGestureSupported(gestureType: Int): Boolean { + return true + } + } +} diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt new file mode 100644 index 000000000000..cf0bfcc4f6df --- /dev/null +++ b/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package 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 android.platform.test.flag.junit.SetFlagsRule +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import com.android.server.testutils.any +import com.android.test.input.MockInputManagerRule +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnitRunner + +/** + * Tests for [InputManager.KeyGestureEventListener]. + * + * Build/Install/Run: + * atest InputTests:KeyGestureEventListenerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class KeyGestureEventListenerTest { + + companion object { + const val DEVICE_ID = 1 + val HOME_GESTURE_EVENT = KeyGestureEvent.Builder() + .setDeviceId(DEVICE_ID) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_H)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + } + + @get:Rule + val rule = SetFlagsRule() + @get:Rule + val inputManagerRule = MockInputManagerRule() + + private val testLooper = TestLooper() + private val executor = HandlerExecutor(Handler(testLooper.looper)) + private var registeredListener: IKeyGestureEventListener? = null + private lateinit var context: Context + private lateinit var inputManager: InputManager + + @Before + fun setUp() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + inputManager = InputManager(context) + `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + // Handle key gesture event listener registration. + doAnswer { + val listener = it.getArgument(0) as IKeyGestureEventListener + if (registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder()) { + // There can only be one registered key gesture event listener per process. + fail("Trying to register a new listener when one already exists") + } + registeredListener = listener + null + }.`when`(inputManagerRule.mock).registerKeyGestureEventListener(any()) + + // Handle key gesture event listener being unregistered. + doAnswer { + val listener = it.getArgument(0) as IKeyGestureEventListener + if (registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder()) { + fail("Trying to unregister a listener that is not registered") + } + registeredListener = null + null + }.`when`(inputManagerRule.mock).unregisterKeyGestureEventListener(any()) + } + + private fun notifyKeyGestureEvent(event: KeyGestureEvent) { + val eventToSend = AidlKeyGestureEvent() + eventToSend.deviceId = event.deviceId + eventToSend.keycodes = event.keycodes + eventToSend.modifierState = event.modifierState + eventToSend.gestureType = event.keyGestureType + eventToSend.action = event.action + eventToSend.displayId = event.displayId + eventToSend.flags = event.flags + registeredListener!!.onKeyGestureEvent(eventToSend) + } + + @Test + fun testListenerHasCorrectGestureNotified() { + var callbackCount = 0 + + // Add a key gesture event listener + inputManager.registerKeyGestureEventListener(executor) { + event: KeyGestureEvent -> + assertEquals(HOME_GESTURE_EVENT, event) + callbackCount++ + } + + // Notifying key gesture event will notify the listener. + notifyKeyGestureEvent(HOME_GESTURE_EVENT) + testLooper.dispatchNext() + assertEquals(1, callbackCount) + } + + @Test + fun testAddingListenersRegistersInternalCallbackListener() { + // Set up two callbacks. + val callback1 = InputManager.KeyGestureEventListener { _ -> } + val callback2 = InputManager.KeyGestureEventListener { _ -> } + + assertNull(registeredListener) + + // Adding the listener should register the callback with InputManagerService. + inputManager.registerKeyGestureEventListener(executor, callback1) + assertNotNull(registeredListener) + + // Adding another listener should not register new internal listener. + val currListener = registeredListener + inputManager.registerKeyGestureEventListener(executor, callback2) + assertEquals(currListener, registeredListener) + } + + @Test + fun testRemovingListenersUnregistersInternalCallbackListener() { + // Set up two callbacks. + val callback1 = InputManager.KeyGestureEventListener { _ -> } + val callback2 = InputManager.KeyGestureEventListener { _ -> } + + inputManager.registerKeyGestureEventListener(executor, callback1) + inputManager.registerKeyGestureEventListener(executor, callback2) + + // Only removing all listeners should remove the internal callback + inputManager.unregisterKeyGestureEventListener(callback1) + assertNotNull(registeredListener) + inputManager.unregisterKeyGestureEventListener(callback2) + assertNull(registeredListener) + } + + @Test + fun testMultipleListeners() { + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.KeyGestureEventListener { _ -> callbackCount1++ } + val callback2 = InputManager.KeyGestureEventListener { _ -> callbackCount2++ } + + // Add both key gesture event listeners + inputManager.registerKeyGestureEventListener(executor, callback1) + inputManager.registerKeyGestureEventListener(executor, callback2) + + // Notifying key gesture event, should notify both the callbacks. + notifyKeyGestureEvent(HOME_GESTURE_EVENT) + testLooper.dispatchAll() + assertEquals(1, callbackCount1) + assertEquals(1, callbackCount2) + + inputManager.unregisterKeyGestureEventListener(callback2) + // Notifying key gesture event, should still trigger callback1 but not + // callback2. + notifyKeyGestureEvent(HOME_GESTURE_EVENT) + testLooper.dispatchAll() + assertEquals(2, callbackCount1) + assertEquals(1, callbackCount2) + } +} diff --git a/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt b/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt index 23135b2550b0..d25dee1d402c 100644 --- a/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyboardBacklightListenerTest.kt @@ -24,22 +24,20 @@ 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 com.android.test.input.MockInputManagerRule +import java.util.concurrent.Executor +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail 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]. @@ -50,23 +48,19 @@ import kotlin.test.fail @Presubmit @RunWith(MockitoJUnitRunner::class) class KeyboardBacklightListenerTest { + @get:Rule - val rule = MockitoJUnit.rule()!! + val inputManagerRule = MockInputManagerRule() 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 @@ -84,7 +78,7 @@ class KeyboardBacklightListenerTest { } registeredListener = listener null - }.`when`(iInputManagerMock).registerKeyboardBacklightListener(any()) + }.`when`(inputManagerRule.mock).registerKeyboardBacklightListener(any()) // Handle keyboard backlight listener being unregistered. doAnswer { @@ -95,14 +89,7 @@ class KeyboardBacklightListenerTest { } registeredListener = null null - }.`when`(iInputManagerMock).unregisterKeyboardBacklightListener(any()) - } - - @After - fun tearDown() { - if (this::inputManagerGlobalSession.isInitialized) { - inputManagerGlobalSession.close() - } + }.`when`(inputManagerRule.mock).unregisterKeyboardBacklightListener(any()) } private fun notifyKeyboardBacklightChanged( diff --git a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt index bcd56ad0c669..1c2a0538e552 100644 --- a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt +++ b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt @@ -27,21 +27,20 @@ import android.platform.test.flag.junit.SetFlagsRule import android.view.KeyEvent import androidx.test.core.app.ApplicationProvider import com.android.server.testutils.any -import org.junit.After +import com.android.test.input.MockInputManagerRule +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail 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.MockitoJUnitRunner -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.test.fail /** * Tests for [InputManager.StickyModifierStateListener]. @@ -59,21 +58,18 @@ class StickyModifierStateListenerTest { @get:Rule val rule = SetFlagsRule() + @get:Rule + val inputManagerRule = MockInputManagerRule() private val testLooper = TestLooper() private val executor = HandlerExecutor(Handler(testLooper.looper)) private var registeredListener: IStickyModifierStateListener? = null 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) inputManager = InputManager(context) `when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) .thenReturn(inputManager) @@ -88,7 +84,7 @@ class StickyModifierStateListenerTest { } registeredListener = listener null - }.`when`(iInputManagerMock).registerStickyModifierStateListener(any()) + }.`when`(inputManagerRule.mock).registerStickyModifierStateListener(any()) // Handle sticky modifier state listener being unregistered. doAnswer { @@ -99,14 +95,7 @@ class StickyModifierStateListenerTest { } registeredListener = null null - }.`when`(iInputManagerMock).unregisterStickyModifierStateListener(any()) - } - - @After - fun tearDown() { - if (this::inputManagerGlobalSession.isInitialized) { - inputManagerGlobalSession.close() - } + }.`when`(inputManagerRule.mock).unregisterStickyModifierStateListener(any()) } private fun notifyStickyModifierStateChanged(modifierState: Int, lockedModifierState: Int) { diff --git a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt index f2724e605553..044f11d6904c 100644 --- a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt +++ b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt @@ -27,7 +27,6 @@ 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 @@ -42,13 +41,13 @@ import com.android.server.input.BatteryController.BluetoothBatteryManager.Blueto 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 com.android.test.input.MockInputManagerRule 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 @@ -184,12 +183,12 @@ class BatteryControllerTests { @get:Rule val rule = MockitoJUnit.rule()!! + @get:Rule + val inputManagerRule = MockInputManagerRule() @Mock private lateinit var native: NativeInputManagerService @Mock - private lateinit var iInputManager: IInputManager - @Mock private lateinit var uEventManager: UEventManager @Mock private lateinit var bluetoothBatteryManager: BluetoothBatteryManager @@ -205,10 +204,9 @@ class BatteryControllerTests { 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 { + `when`(inputManagerRule.mock.inputDeviceIds).then { deviceGenerationMap.keys.toIntArray() } addInputDevice(DEVICE_ID) @@ -218,18 +216,11 @@ class BatteryControllerTests { bluetoothBatteryManager) batteryController.systemRunning() val listenerCaptor = ArgumentCaptor.forClass(IInputDevicesChangedListener::class.java) - verify(iInputManager).registerInputDevicesChangedListener(listenerCaptor.capture()) + verify(inputManagerRule.mock).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, @@ -239,7 +230,7 @@ class BatteryControllerTests { ?: throw IllegalArgumentException("Device $deviceId was never added!") deviceGenerationMap[deviceId] = generation - `when`(iInputManager.getInputDevice(deviceId)) + `when`(inputManagerRule.mock.getInputDevice(deviceId)) .thenReturn(createInputDevice(deviceId, hasBattery, supportsUsi, generation)) val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) } if (::devicesChangedListener.isInitialized) { @@ -657,9 +648,9 @@ class BatteryControllerTests { @Test fun testRegisterBluetoothListenerForMonitoredBluetoothDevices() { - `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + `when`(inputManagerRule.mock.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) .thenReturn("AA:BB:CC:DD:EE:FF") - `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + `when`(inputManagerRule.mock.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) .thenReturn("11:22:33:44:55:66") addInputDevice(BT_DEVICE_ID) testLooper.dispatchNext() @@ -686,7 +677,7 @@ class BatteryControllerTests { batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) verify(bluetoothBatteryManager, never()).removeBatteryListener(any()) - `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + `when`(inputManagerRule.mock.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) .thenReturn(null) notifyDeviceChanged(SECOND_BT_DEVICE_ID) testLooper.dispatchNext() @@ -695,7 +686,7 @@ class BatteryControllerTests { @Test fun testNotifiesBluetoothBatteryChanges() { - `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + `when`(inputManagerRule.mock.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) @@ -716,7 +707,7 @@ class BatteryControllerTests { @Test fun testBluetoothBatteryIsPrioritized() { `when`(native.getBatteryDevicePath(BT_DEVICE_ID)).thenReturn("/sys/dev/bt_device") - `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + `when`(inputManagerRule.mock.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) @@ -745,7 +736,7 @@ class BatteryControllerTests { @Test fun testFallBackToNativeBatteryStateWhenBluetoothStateInvalid() { `when`(native.getBatteryDevicePath(BT_DEVICE_ID)).thenReturn("/sys/dev/bt_device") - `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + `when`(inputManagerRule.mock.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) @@ -776,9 +767,9 @@ class BatteryControllerTests { @Test fun testRegisterBluetoothMetadataListenerForMonitoredBluetoothDevices() { - `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + `when`(inputManagerRule.mock.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) .thenReturn("AA:BB:CC:DD:EE:FF") - `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + `when`(inputManagerRule.mock.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) .thenReturn("11:22:33:44:55:66") addInputDevice(BT_DEVICE_ID) testLooper.dispatchNext() @@ -811,7 +802,7 @@ class BatteryControllerTests { verify(bluetoothBatteryManager) .removeMetadataListener("AA:BB:CC:DD:EE:FF", metadataListener1.value) - `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + `when`(inputManagerRule.mock.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) .thenReturn(null) notifyDeviceChanged(SECOND_BT_DEVICE_ID) testLooper.dispatchNext() @@ -821,7 +812,7 @@ class BatteryControllerTests { @Test fun testNotifiesBluetoothMetadataBatteryChanges() { - `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + `when`(inputManagerRule.mock.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) .thenReturn("AA:BB:CC:DD:EE:FF") `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", BluetoothDevice.METADATA_MAIN_BATTERY)) @@ -861,7 +852,7 @@ class BatteryControllerTests { @Test fun testBluetoothMetadataBatteryIsPrioritized() { - `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + `when`(inputManagerRule.mock.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", diff --git a/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt b/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt new file mode 100644 index 000000000000..862886ce69d2 --- /dev/null +++ b/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input + +import android.hardware.input.InputGestureData +import android.hardware.input.InputManager +import android.hardware.input.KeyGestureEvent +import android.platform.test.annotations.Presubmit +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +/** + * Tests for custom keyboard glyph map configuration. + * + * Build/Install/Run: + * atest InputTests:CustomInputGestureManagerTests + */ +@Presubmit +class InputGestureManagerTests { + + companion object { + const val USER_ID = 1 + } + + private lateinit var inputGestureManager: InputGestureManager + + @Before + fun setup() { + inputGestureManager = InputGestureManager(ApplicationProvider.getApplicationContext()) + } + + @Test + fun addRemoveCustomGesture() { + val customGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + val result = inputGestureManager.addCustomInputGesture(USER_ID, customGesture) + assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, result) + assertEquals( + listOf(customGesture), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + + inputGestureManager.removeCustomInputGesture(USER_ID, customGesture) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + } + + @Test + fun removeNonExistentGesture() { + val customGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + val result = inputGestureManager.removeCustomInputGesture(USER_ID, customGesture) + assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST, result) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + } + + @Test + fun addAlreadyExistentGesture() { + val customGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customGesture) + val customGesture2 = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() + val result = inputGestureManager.addCustomInputGesture(USER_ID, customGesture2) + assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS, result) + assertEquals( + listOf(customGesture), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + } + + @Test + fun addRemoveAllExistentGestures() { + val customGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customGesture) + val customGesture2 = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_DEL, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customGesture2) + + assertEquals( + listOf(customGesture, customGesture2), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + + inputGestureManager.removeAllCustomInputGestures(USER_ID) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + } +}
\ No newline at end of file diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 3c72498082e4..6eb00457a1a6 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -17,8 +17,12 @@ package com.android.server.input +import android.Manifest import android.content.Context import android.content.ContextWrapper +import android.content.PermissionChecker +import android.content.pm.PackageManager +import android.content.pm.PackageManagerInternal import android.hardware.display.DisplayManager import android.hardware.display.DisplayViewport import android.hardware.display.VirtualDisplay @@ -27,20 +31,30 @@ import android.hardware.input.InputManagerGlobal import android.os.InputEventInjectionSync import android.os.SystemClock import android.os.test.TestLooper +import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.Presubmit -import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.view.View.OnKeyListener import android.view.InputDevice +import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.SurfaceHolder import android.view.SurfaceView +import android.view.WindowManager import android.test.mock.MockContentResolver import androidx.test.platform.app.InstrumentationRegistry +import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.internal.policy.KeyInterceptionInfo import com.android.internal.util.test.FakeSettingsProvider +import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.server.LocalServices +import com.android.server.wm.WindowManagerInternal import com.google.common.truth.Truth.assertThat -import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -49,15 +63,15 @@ 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.mock import org.mockito.Mockito.never import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.junit.MockitoJUnit +import org.mockito.Mockito.`when` import org.mockito.stubbing.OngoingStubbing /** @@ -69,14 +83,29 @@ import org.mockito.stubbing.OngoingStubbing @Presubmit class InputManagerServiceTests { + companion object { + val ACTION_KEY_EVENTS = listOf( + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_META_LEFT), + KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_META_RIGHT), + KeyEvent( /* downTime= */0, /* eventTime= */0, /* action= */0, /* code= */0, + /* repeat= */0, KeyEvent.META_META_ON + ) + ) + } + @get:Rule - val mockitoRule = MockitoJUnit.rule()!! + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(LocalServices::class.java) + .mockStatic(PermissionChecker::class.java) + .mockStatic(KeyCharacterMap::class.java) + .build()!! @get:Rule - val fakeSettingsProviderRule = FakeSettingsProvider.rule()!! + val setFlagsRule = SetFlagsRule() @get:Rule - val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()!! + val fakeSettingsProviderRule = FakeSettingsProvider.rule()!! @Mock private lateinit var native: NativeInputManagerService @@ -85,8 +114,20 @@ class InputManagerServiceTests { private lateinit var wmCallbacks: InputManagerService.WindowManagerCallbacks @Mock + private lateinit var windowManagerInternal: WindowManagerInternal + + @Mock + private lateinit var packageManagerInternal: PackageManagerInternal + + @Mock private lateinit var uEventManager: UEventManager + @Mock + private lateinit var kbdController: InputManagerService.KeyboardBacklightControllerInterface + + @Mock + private lateinit var kcm: KeyCharacterMap + private lateinit var service: InputManagerService private lateinit var localService: InputManagerInternal private lateinit var context: Context @@ -113,11 +154,32 @@ class InputManagerServiceTests { override fun registerLocalService(service: InputManagerInternal?) { localService = service!! } + + override fun getKeyboardBacklightController( + nativeService: NativeInputManagerService?, + dataStore: PersistentDataStore? + ): InputManagerService.KeyboardBacklightControllerInterface { + return kbdController + } }) inputManagerGlobalSession = InputManagerGlobal.createTestSession(service) val inputManager = InputManager(context) whenever(context.getSystemService(InputManager::class.java)).thenReturn(inputManager) whenever(context.getSystemService(Context.INPUT_SERVICE)).thenReturn(inputManager) + whenever(context.checkCallingOrSelfPermission(Manifest.permission.MANAGE_KEY_GESTURES)) + .thenReturn( + PackageManager.PERMISSION_GRANTED + ) + + ExtendedMockito.doReturn(windowManagerInternal).`when` { + LocalServices.getService(eq(WindowManagerInternal::class.java)) + } + ExtendedMockito.doReturn(packageManagerInternal).`when` { + LocalServices.getService(eq(PackageManagerInternal::class.java)) + } + ExtendedMockito.doReturn(kcm).`when` { + KeyCharacterMap.load(anyInt()) + } assertTrue("Local service must be registered", this::localService.isInitialized) service.setWindowManagerCallbacks(wmCallbacks) @@ -151,14 +213,17 @@ class InputManagerServiceTests { verify(native).setTouchpadNaturalScrollingEnabled(anyBoolean()) verify(native).setTouchpadTapToClickEnabled(anyBoolean()) verify(native).setTouchpadTapDraggingEnabled(anyBoolean()) + verify(native).setShouldNotifyTouchpadHardwareState(anyBoolean()) verify(native).setTouchpadRightClickZoneEnabled(anyBoolean()) + verify(native).setTouchpadThreeFingerTapShortcutEnabled(anyBoolean()) verify(native).setShowTouches(anyBoolean()) verify(native).setMotionClassifierEnabled(anyBoolean()) verify(native).setMaximumObscuringOpacityForTouch(anyFloat()) verify(native).setStylusPointerIconEnabled(anyBoolean()) - // Called twice at boot, since there are individual callbacks to update the - // key repeat timeout and the key repeat delay. - verify(native, times(2)).setKeyRepeatConfiguration(anyInt(), anyInt()) + // Called thrice at boot, since there are individual callbacks to update the + // key repeat timeout, the key repeat delay and whether key repeat enabled. + verify(native, times(3)).setKeyRepeatConfiguration(anyInt(), anyInt(), + anyBoolean()) } @Test @@ -194,7 +259,7 @@ class InputManagerServiceTests { } @Test - fun testAddAndRemoveVirtualmKeyboardLayoutAssociation() { + fun testAddAndRemoveVirtualKeyboardLayoutAssociation() { val inputPort = "input port" val languageTag = "language" val layoutType = "layoutType" @@ -205,6 +270,48 @@ class InputManagerServiceTests { verify(native, times(2)).changeKeyboardLayoutAssociation() } + @Test + fun testActionKeyEventsForwardedToFocusedWindow_whenCorrectlyRequested() { + service.systemRunning() + overrideSendActionKeyEventsToFocusedWindow( + /* hasPermission = */true, + /* hasPrivateFlag = */true + ) + whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(-1) + + for (event in ACTION_KEY_EVENTS) { + assertEquals(0, service.interceptKeyBeforeDispatching(null, event, 0)) + } + } + + @Test + fun testActionKeyEventsNotForwardedToFocusedWindow_whenNoPermissions() { + service.systemRunning() + overrideSendActionKeyEventsToFocusedWindow( + /* hasPermission = */false, + /* hasPrivateFlag = */true + ) + whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(-1) + + for (event in ACTION_KEY_EVENTS) { + assertNotEquals(0, service.interceptKeyBeforeDispatching(null, event, 0)) + } + } + + @Test + fun testActionKeyEventsNotForwardedToFocusedWindow_whenNoPrivateFlag() { + service.systemRunning() + overrideSendActionKeyEventsToFocusedWindow( + /* hasPermission = */true, + /* hasPrivateFlag = */false + ) + whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(-1) + + for (event in ACTION_KEY_EVENTS) { + assertNotEquals(0, service.interceptKeyBeforeDispatching(null, event, 0)) + } + } + private fun createVirtualDisplays(count: Int): List<VirtualDisplay> { val displayManager: DisplayManager = context.getSystemService( DisplayManager::class.java @@ -372,6 +479,91 @@ class InputManagerServiceTests { verify(mockOnKeyListener).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, upEvent) verify(mockOnKeyListener, never()).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, upEvent) } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER) + fun handleKeyGestures_keyboardBacklight() { + service.systemRunning() + + val backlightDownEvent = createKeyEvent(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN) + service.interceptKeyBeforeDispatching(null, backlightDownEvent, /* policyFlags = */0) + verify(kbdController).decrementKeyboardBacklight(anyInt()) + + val backlightUpEvent = createKeyEvent(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP) + service.interceptKeyBeforeDispatching(null, backlightUpEvent, /* policyFlags = */0) + verify(kbdController).incrementKeyboardBacklight(anyInt()) + } + + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER) + fun handleKeyGestures_toggleCapsLock() { + service.systemRunning() + + val metaDownEvent = createKeyEvent(KeyEvent.KEYCODE_META_LEFT) + service.interceptKeyBeforeDispatching(null, metaDownEvent, /* policyFlags = */0) + val altDownEvent = + createKeyEvent(KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.META_META_ON, KeyEvent.ACTION_DOWN) + service.interceptKeyBeforeDispatching(null, altDownEvent, /* policyFlags = */0) + val altUpEvent = + createKeyEvent(KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.META_META_ON, KeyEvent.ACTION_UP) + service.interceptKeyBeforeDispatching(null, altUpEvent, /* policyFlags = */0) + + verify(native).toggleCapsLock(anyInt()) + } + + fun overrideSendActionKeyEventsToFocusedWindow( + hasPermission: Boolean, + hasPrivateFlag: Boolean + ) { + ExtendedMockito.doReturn( + if (hasPermission) { + PermissionChecker.PERMISSION_GRANTED + } else { + PermissionChecker.PERMISSION_HARD_DENIED + } + ).`when` { + PermissionChecker.checkPermissionForDataDelivery( + any(), + eq(Manifest.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW), + anyInt(), + anyInt(), + any(), + any(), + any() + ) + } + + val info = KeyInterceptionInfo( + /* type = */0, + if (hasPrivateFlag) { + WindowManager.LayoutParams.PRIVATE_FLAG_ALLOW_ACTION_KEY_EVENTS + } else { + 0 + }, + "title", + /* uid = */0 + ) + whenever(windowManagerInternal.getKeyInterceptionInfoFromToken(any())).thenReturn(info) + } + + private fun createKeyEvent( + keycode: Int, + modifierState: Int = 0, + action: Int = KeyEvent.ACTION_DOWN + ): KeyEvent { + return KeyEvent( + /* downTime = */0, + /* eventTime = */0, + action, + keycode, + /* repeat = */0, + modifierState, + KeyCharacterMap.VIRTUAL_KEYBOARD, + /* scancode = */0, + /* flags = */0, + InputDevice.SOURCE_KEYBOARD + ) + } } private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) diff --git a/tests/Input/src/com/android/server/input/InputShellCommandTest.java b/tests/Input/src/com/android/server/input/InputShellCommandTest.java index 11f46335f017..a236244546cb 100644 --- a/tests/Input/src/com/android/server/input/InputShellCommandTest.java +++ b/tests/Input/src/com/android/server/input/InputShellCommandTest.java @@ -133,6 +133,21 @@ public class InputShellCommandTest { assertThat(mInputEventInjector.mInjectedEvents).isEmpty(); } + @Test + public void testSwipeCommandEventFrequency() { + int[] durations = {100, 300, 500}; + for (int durationMillis: durations) { + mInputEventInjector.mInjectedEvents.clear(); + runCommand(String.format("swipe 200 800 200 200 %d", durationMillis)); + + // Add 2 events for ACTION_DOWN and ACTION_UP. + final int maxEventNum = + (int) Math.ceil(InputShellCommand.SWIPE_EVENT_HZ_DEFAULT + * (float) durationMillis / 1000) + 2; + assertThat(mInputEventInjector.mInjectedEvents.size()).isAtMost(maxEventNum); + } + } + private InputEvent getSingleInjectedInputEvent() { assertThat(mInputEventInjector.mInjectedEvents).hasSize(1); return mInputEventInjector.mInjectedEvents.get(0); diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt new file mode 100644 index 000000000000..1574d1b7ce6f --- /dev/null +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -0,0 +1,1466 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input + +import android.app.role.RoleManager +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Resources +import android.content.res.XmlResourceParser +import android.hardware.input.AidlKeyGestureEvent +import android.hardware.input.AppLaunchData +import android.hardware.input.IInputManager +import android.hardware.input.IKeyGestureEventListener +import android.hardware.input.IKeyGestureHandler +import android.hardware.input.InputGestureData +import android.hardware.input.InputManager +import android.hardware.input.InputManagerGlobal +import android.hardware.input.KeyGestureEvent +import android.os.IBinder +import android.os.Process +import android.os.SystemClock +import android.os.SystemProperties +import android.os.test.TestLooper +import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule +import android.view.InputDevice +import android.view.KeyCharacterMap +import android.view.KeyEvent +import android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE +import androidx.test.core.app.ApplicationProvider +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.internal.R +import com.android.internal.annotations.Keep +import com.android.internal.util.FrameworkStatsLog +import com.android.modules.utils.testing.ExtendedMockitoRule +import junitparams.JUnitParamsRunner +import junitparams.Parameters +import org.junit.After +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +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 + +/** + * Tests for {@link KeyGestureController}. + * + * Build/Install/Run: + * atest InputTests:KeyGestureControllerTests + */ +@Presubmit +@RunWith(JUnitParamsRunner::class) +class KeyGestureControllerTests { + + companion object { + const val DEVICE_ID = 1 + val HOME_GESTURE_COMPLETE_EVENT = KeyGestureEvent.Builder() + .setDeviceId(DEVICE_ID) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_H)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + .build() + val MODIFIER = mapOf( + KeyEvent.KEYCODE_CTRL_LEFT to (KeyEvent.META_CTRL_LEFT_ON or KeyEvent.META_CTRL_ON), + KeyEvent.KEYCODE_CTRL_RIGHT to (KeyEvent.META_CTRL_RIGHT_ON or KeyEvent.META_CTRL_ON), + KeyEvent.KEYCODE_ALT_LEFT to (KeyEvent.META_ALT_LEFT_ON or KeyEvent.META_ALT_ON), + KeyEvent.KEYCODE_ALT_RIGHT to (KeyEvent.META_ALT_RIGHT_ON or KeyEvent.META_ALT_ON), + KeyEvent.KEYCODE_SHIFT_LEFT to (KeyEvent.META_SHIFT_LEFT_ON or KeyEvent.META_SHIFT_ON), + KeyEvent.KEYCODE_SHIFT_RIGHT to (KeyEvent.META_SHIFT_RIGHT_ON or KeyEvent.META_SHIFT_ON), + KeyEvent.KEYCODE_META_LEFT to (KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON), + KeyEvent.KEYCODE_META_RIGHT to (KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON), + ) + const val SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH = 0 + const val SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1 + const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0 + const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1 + const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2 + } + + @JvmField + @Rule + val extendedMockitoRule = ExtendedMockitoRule.Builder(this) + .mockStatic(FrameworkStatsLog::class.java) + .mockStatic(SystemProperties::class.java) + .mockStatic(KeyCharacterMap::class.java) + .build()!! + + @JvmField + @Rule + val rule = SetFlagsRule() + + @Mock + private lateinit var iInputManager: IInputManager + + @Mock + private lateinit var resources: Resources + + @Mock + private lateinit var packageManager: PackageManager + + private var currentPid = 0 + private lateinit var context: Context + private lateinit var keyGestureController: KeyGestureController + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + private lateinit var testLooper: TestLooper + private var events = mutableListOf<KeyGestureEvent>() + + @Before + fun setup() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + setupInputDevices() + setupBehaviors() + testLooper = TestLooper() + currentPid = Process.myPid() + } + + @After + fun teardown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + private fun setupBehaviors() { + Mockito.`when`(SystemProperties.get("ro.debuggable")).thenReturn("1") + Mockito.`when`(resources.getBoolean(R.bool.config_enableScreenshotChord)).thenReturn(true) + val testBookmarks: XmlResourceParser = context.resources.getXml( + com.android.test.input.R.xml.bookmarks + ) + Mockito.`when`(resources.getXml(R.xml.bookmarks)).thenReturn(testBookmarks) + Mockito.`when`(context.resources).thenReturn(resources) + Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) + .thenReturn(true) + Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) + .thenReturn(true) + Mockito.`when`(context.packageManager).thenReturn(packageManager) + } + + private fun setupInputDevices() { + val correctIm = context.getSystemService(InputManager::class.java)!! + val virtualDevice = correctIm.getInputDevice(KeyCharacterMap.VIRTUAL_KEYBOARD)!! + val kcm = virtualDevice.keyCharacterMap!! + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + val inputManager = InputManager(context) + Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + val keyboardDevice = InputDevice.Builder().setId(DEVICE_ID).build() + Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) + Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) + ExtendedMockito.`when`(KeyCharacterMap.load(Mockito.anyInt())).thenReturn(kcm) + } + + private fun setupKeyGestureController() { + keyGestureController = KeyGestureController(context, testLooper.looper) + Mockito.`when`(iInputManager.getAppLaunchBookmarks()) + .thenReturn(keyGestureController.appLaunchBookmarks) + keyGestureController.systemRunning() + } + + private fun notifyHomeGestureCompleted() { + keyGestureController.notifyKeyGestureCompleted( + DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H), + KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_HOME + ) + } + + @Test + fun testKeyGestureEvent_registerUnregisterListener() { + setupKeyGestureController() + val listener = KeyGestureEventListener() + + // Register key gesture event listener + keyGestureController.registerKeyGestureEventListener(listener, 0) + notifyHomeGestureCompleted() + testLooper.dispatchAll() + assertEquals( + "Listener should get callbacks on key gesture event completed", + 1, + events.size + ) + assertEquals( + "Listener should get callback for key gesture complete event", + HOME_GESTURE_COMPLETE_EVENT, + events[0] + ) + + // Unregister listener + events.clear() + keyGestureController.unregisterKeyGestureEventListener(listener, 0) + notifyHomeGestureCompleted() + testLooper.dispatchAll() + assertEquals( + "Listener should not get callback after being unregistered", + 0, + events.size + ) + } + + @Test + fun testKeyGestureEvent_multipleGestureHandlers() { + setupKeyGestureController() + + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + var selfCallback = 0 + val externalHandler1 = KeyGestureHandler { _, _ -> + callbackCount1++ + true + } + val externalHandler2 = KeyGestureHandler { _, _ -> + callbackCount2++ + true + } + val selfHandler = KeyGestureHandler { _, _ -> + selfCallback++ + false + } + + // Register key gesture handler: External process (last in priority) + keyGestureController.registerKeyGestureHandler(externalHandler1, currentPid + 1) + + // Register key gesture handler: External process (second in priority) + keyGestureController.registerKeyGestureHandler(externalHandler2, currentPid - 1) + + // Register key gesture handler: Self process (first in priority) + keyGestureController.registerKeyGestureHandler(selfHandler, currentPid) + + keyGestureController.handleKeyGesture(/* deviceId = */ 0, intArrayOf(KeyEvent.KEYCODE_HOME), + /* modifierState = */ 0, KeyGestureEvent.KEY_GESTURE_TYPE_HOME, + KeyGestureEvent.ACTION_GESTURE_COMPLETE, /* displayId */ 0, + /* focusedToken = */ null, /* flags = */ 0, /* appLaunchData = */null + ) + + assertEquals( + "Self handler should get callbacks first", + 1, + selfCallback + ) + assertEquals( + "Higher priority handler should get callbacks first", + 1, + callbackCount2 + ) + assertEquals( + "Lower priority handler should not get callbacks if already handled", + 0, + callbackCount1 + ) + } + + class TestData( + val name: String, + val keys: IntArray, + val expectedKeyGestureType: Int, + val expectedKeys: IntArray, + val expectedModifierState: Int, + val expectedActions: IntArray, + val expectedAppLaunchData: AppLaunchData? = null, + ) { + override fun toString(): String = name + } + + @Keep + private fun systemGesturesTestArguments(): Array<TestData> { + return arrayOf( + TestData( + "META + A -> Launch Assistant", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_A), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, + intArrayOf(KeyEvent.KEYCODE_A), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + H -> Go Home", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_H), + KeyGestureEvent.KEY_GESTURE_TYPE_HOME, + intArrayOf(KeyEvent.KEYCODE_H), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ENTER -> Go Home", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ENTER), + KeyGestureEvent.KEY_GESTURE_TYPE_HOME, + intArrayOf(KeyEvent.KEYCODE_ENTER), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + I -> Launch System Settings", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_I), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS, + intArrayOf(KeyEvent.KEYCODE_I), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + L -> Lock", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_L), + KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, + intArrayOf(KeyEvent.KEYCODE_L), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + N -> Toggle Notification", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_N), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, + intArrayOf(KeyEvent.KEYCODE_N), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + CTRL + N -> Open Notes", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_N + ), + KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, + intArrayOf(KeyEvent.KEYCODE_N), + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + CTRL + S -> Take Screenshot", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_S + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, + intArrayOf(KeyEvent.KEYCODE_S), + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + DEL -> Back", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_DEL), + KeyGestureEvent.KEY_GESTURE_TYPE_BACK, + intArrayOf(KeyEvent.KEYCODE_DEL), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ESC -> Back", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ESCAPE), + KeyGestureEvent.KEY_GESTURE_TYPE_BACK, + intArrayOf(KeyEvent.KEYCODE_ESCAPE), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + DPAD_LEFT -> Back", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_DPAD_LEFT), + KeyGestureEvent.KEY_GESTURE_TYPE_BACK, + intArrayOf(KeyEvent.KEYCODE_DPAD_LEFT), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + CTRL + DPAD_UP -> Multi Window Navigation", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_DPAD_UP + ), + KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION, + intArrayOf(KeyEvent.KEYCODE_DPAD_UP), + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + CTRL + DPAD_DOWN -> Desktop Mode", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_DPAD_DOWN + ), + KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE, + intArrayOf(KeyEvent.KEYCODE_DPAD_DOWN), + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + CTRL + DPAD_LEFT -> Splitscreen Navigation Left", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_DPAD_LEFT + ), + KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT, + intArrayOf(KeyEvent.KEYCODE_DPAD_LEFT), + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + CTRL + DPAD_RIGHT -> Splitscreen Navigation Right", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT + ), + KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT, + intArrayOf(KeyEvent.KEYCODE_DPAD_RIGHT), + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ALT + DPAD_LEFT -> Change Splitscreen Focus Left", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_DPAD_LEFT + ), + KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT, + intArrayOf(KeyEvent.KEYCODE_DPAD_LEFT), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + CTRL + DPAD_RIGHT -> Change Splitscreen Focus Right", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT + ), + KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT, + intArrayOf(KeyEvent.KEYCODE_DPAD_RIGHT), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + / -> Open Shortcut Helper", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SLASH), + KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER, + intArrayOf(KeyEvent.KEYCODE_SLASH), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ALT -> Toggle Caps Lock", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "ALT + META -> Toggle Caps Lock", + intArrayOf(KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_META_LEFT), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + TAB -> Open Overview", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_TAB), + KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, + intArrayOf(KeyEvent.KEYCODE_TAB), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "ALT + TAB -> Toggle Recent Apps Switcher", + intArrayOf(KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_TAB), + KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER, + intArrayOf(KeyEvent.KEYCODE_TAB), + KeyEvent.META_ALT_ON, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_START, + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ) + ), + TestData( + "CTRL + SPACE -> Switch Language Forward", + intArrayOf(KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_SPACE), + KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, + intArrayOf(KeyEvent.KEYCODE_SPACE), + KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "CTRL + SHIFT + SPACE -> Switch Language Backward", + intArrayOf( + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_SPACE + ), + KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, + intArrayOf(KeyEvent.KEYCODE_SPACE), + KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "CTRL + ALT + Z -> Accessibility Shortcut", + intArrayOf( + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_Z + ), + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, + intArrayOf(KeyEvent.KEYCODE_Z), + KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + B -> Launch Default Browser", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_B), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_B), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER) + ), + TestData( + "META + C -> Launch Default Contacts", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_C), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_C), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS) + ), + TestData( + "META + E -> Launch Default Email", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_E), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_E), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_EMAIL) + ), + TestData( + "META + K -> Launch Default Calendar", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_K), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_K), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALENDAR) + ), + TestData( + "META + M -> Launch Default Maps", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_M), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_M), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_MAPS) + ), + TestData( + "META + P -> Launch Default Music", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_P), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_P), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_MUSIC) + ), + TestData( + "META + S -> Launch Default SMS", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_S), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_S), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_SMS) + ), + TestData( + "META + U -> Launch Default Calculator", + intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_U), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_U), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR) + ), + TestData( + "META + SHIFT + B -> Launch Default Browser", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_B + ), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_B), + KeyEvent.META_META_ON or KeyEvent.META_SHIFT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER) + ), + TestData( + "META + SHIFT + C -> Launch Default Contacts", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_C + ), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_C), + KeyEvent.META_META_ON or KeyEvent.META_SHIFT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS) + ), + TestData( + "META + SHIFT + J -> Launch Target Activity", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_J + ), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_J), + KeyEvent.META_META_ON or KeyEvent.META_SHIFT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest") + ), + TestData( + "META + CTRL + DEL -> Trigger Bug Report", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_DEL + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT, + intArrayOf(KeyEvent.KEYCODE_DEL), + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "Meta + Alt + 3 -> Toggle Bounce Keys", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_3 + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS, + intArrayOf(KeyEvent.KEYCODE_3), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "Meta + Alt + 4 -> Toggle Mouse Keys", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_4 + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS, + intArrayOf(KeyEvent.KEYCODE_4), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "Meta + Alt + 5 -> Toggle Sticky Keys", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_5 + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS, + intArrayOf(KeyEvent.KEYCODE_5), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "Meta + Alt + 6 -> Toggle Slow Keys", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_6 + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS, + intArrayOf(KeyEvent.KEYCODE_6), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + CTRL + D -> Move a task to next display", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_D + ), + KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY, + intArrayOf(KeyEvent.KEYCODE_D), + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "ALT + [ -> Resizes a task to fit the left half of the screen", + intArrayOf( + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_LEFT_BRACKET + ), + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET), + KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "ALT + ] -> Resizes a task to fit the right half of the screen", + intArrayOf( + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_RIGHT_BRACKET + ), + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET), + KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "ALT + '=' -> Maximizes a task to fit the screen", + intArrayOf( + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_EQUALS + ), + KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_EQUALS), + KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "ALT + '-' -> Restores a task size to its previous bounds", + intArrayOf( + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_MINUS + ), + KeyGestureEvent.KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE, + intArrayOf(KeyEvent.KEYCODE_MINUS), + KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ) + ) + } + + @Test + @Parameters(method = "systemGesturesTestArguments") + @EnableFlags( + com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SHORTCUT_CONTROL, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FLAG, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, + com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, + com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS + ) + fun testKeyGestures(test: TestData) { + setupKeyGestureController() + testKeyGestureInternal(test) + } + + @Test + @Parameters(method = "systemGesturesTestArguments") + @EnableFlags( + com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SHORTCUT_CONTROL, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_BOUNCE_KEYS_FLAG, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_SLOW_KEYS_FLAG, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, + com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_MOUSE_KEYS, + com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, + com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS + ) + fun testCustomKeyGesturesNotAllowedForSystemGestures(test: TestData) { + setupKeyGestureController() + // Need to re-init so that bookmarks are correctly blocklisted + Mockito.`when`(iInputManager.getAppLaunchBookmarks()) + .thenReturn(keyGestureController.appLaunchBookmarks) + keyGestureController.systemRunning() + + val builder = InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger( + InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) + ) + if (test.expectedAppLaunchData != null) { + builder.setAppLaunchData(test.expectedAppLaunchData) + } + assertEquals( + test.toString(), + InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE, + keyGestureController.addCustomInputGesture(0, builder.build().aidlData) + ) + } + + @Keep + private fun systemKeysTestArguments(): Array<TestData> { + return arrayOf( + TestData( + "RECENT_APPS -> Show Overview", + intArrayOf(KeyEvent.KEYCODE_RECENT_APPS), + KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, + intArrayOf(KeyEvent.KEYCODE_RECENT_APPS), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "APP_SWITCH -> App Switch", + intArrayOf(KeyEvent.KEYCODE_APP_SWITCH), + KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, + intArrayOf(KeyEvent.KEYCODE_APP_SWITCH), + 0, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_START, + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ) + ), + TestData( + "BRIGHTNESS_UP -> Brightness Up", + intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_UP), + KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP, + intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_UP), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "BRIGHTNESS_DOWN -> Brightness Down", + intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_DOWN), + KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN, + intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_DOWN), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "KEYBOARD_BACKLIGHT_UP -> Keyboard Backlight Up", + intArrayOf(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP), + KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP, + intArrayOf(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "KEYBOARD_BACKLIGHT_DOWN -> Keyboard Backlight Down", + intArrayOf(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN), + KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN, + intArrayOf(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "KEYBOARD_BACKLIGHT_TOGGLE -> Keyboard Backlight Toggle", + intArrayOf(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE), + KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE, + intArrayOf(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "ALL_APPS -> Open App Drawer", + intArrayOf(KeyEvent.KEYCODE_ALL_APPS), + KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, + intArrayOf(KeyEvent.KEYCODE_ALL_APPS), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "NOTIFICATION -> Toggle Notification Panel", + intArrayOf(KeyEvent.KEYCODE_NOTIFICATION), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, + intArrayOf(KeyEvent.KEYCODE_NOTIFICATION), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "LANGUAGE_SWITCH -> Switch Language Forward", + intArrayOf(KeyEvent.KEYCODE_LANGUAGE_SWITCH), + KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, + intArrayOf(KeyEvent.KEYCODE_LANGUAGE_SWITCH), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "SHIFT + LANGUAGE_SWITCH -> Switch Language Backward", + intArrayOf(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_LANGUAGE_SWITCH), + KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, + intArrayOf(KeyEvent.KEYCODE_LANGUAGE_SWITCH), + KeyEvent.META_SHIFT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "SCREENSHOT -> Take Screenshot", + intArrayOf(KeyEvent.KEYCODE_SCREENSHOT), + KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, + intArrayOf(KeyEvent.KEYCODE_SCREENSHOT), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META -> Open Apps Drawer", + intArrayOf(KeyEvent.KEYCODE_META_LEFT), + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, + intArrayOf(KeyEvent.KEYCODE_META_LEFT), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "SYSRQ -> Take screenshot", + intArrayOf(KeyEvent.KEYCODE_SYSRQ), + KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, + intArrayOf(KeyEvent.KEYCODE_SYSRQ), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "ESC -> Close All Dialogs", + intArrayOf(KeyEvent.KEYCODE_ESCAPE), + KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS, + intArrayOf(KeyEvent.KEYCODE_ESCAPE), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "EXPLORER -> Launch Default Browser", + intArrayOf(KeyEvent.KEYCODE_EXPLORER), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_EXPLORER), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER) + ), + TestData( + "ENVELOPE -> Launch Default Email", + intArrayOf(KeyEvent.KEYCODE_ENVELOPE), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_ENVELOPE), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_EMAIL) + ), + TestData( + "CONTACTS -> Launch Default Contacts", + intArrayOf(KeyEvent.KEYCODE_CONTACTS), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_CONTACTS), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS) + ), + TestData( + "CALENDAR -> Launch Default Calendar", + intArrayOf(KeyEvent.KEYCODE_CALENDAR), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_CALENDAR), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALENDAR) + ), + TestData( + "MUSIC -> Launch Default Music", + intArrayOf(KeyEvent.KEYCODE_MUSIC), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_MUSIC), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_MUSIC) + ), + TestData( + "CALCULATOR -> Launch Default Calculator", + intArrayOf(KeyEvent.KEYCODE_CALCULATOR), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_CALCULATOR), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE), + AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CALCULATOR) + ), + ) + } + + @Test + @Parameters(method = "systemKeysTestArguments") + fun testSystemKeys(test: TestData) { + setupKeyGestureController() + testKeyGestureInternal(test) + } + + @Test + fun testKeycodesFullyConsumed_irrespectiveOfHandlers() { + setupKeyGestureController() + val testKeys = intArrayOf( + KeyEvent.KEYCODE_RECENT_APPS, + KeyEvent.KEYCODE_APP_SWITCH, + KeyEvent.KEYCODE_BRIGHTNESS_UP, + KeyEvent.KEYCODE_BRIGHTNESS_DOWN, + KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, + KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, + KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, + KeyEvent.KEYCODE_ALL_APPS, + KeyEvent.KEYCODE_NOTIFICATION, + KeyEvent.KEYCODE_SETTINGS, + KeyEvent.KEYCODE_LANGUAGE_SWITCH, + KeyEvent.KEYCODE_SCREENSHOT, + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_META_RIGHT, + KeyEvent.KEYCODE_ASSIST, + KeyEvent.KEYCODE_VOICE_ASSIST, + KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY, + KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY, + KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY, + KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL, + ) + + val handler = KeyGestureHandler { _, _ -> false } + keyGestureController.registerKeyGestureHandler(handler, 0) + + for (key in testKeys) { + sendKeys(intArrayOf(key), assertNotSentToApps = true) + } + } + + @Test + fun testSearchKeyGestures_defaultSearch() { + Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior)) + .thenReturn(SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH) + setupKeyGestureController() + testKeyGestureNotProduced( + "SEARCH -> Default Search", + intArrayOf(KeyEvent.KEYCODE_SEARCH), + ) + } + + @Test + fun testSearchKeyGestures_searchActivity() { + Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior)) + .thenReturn(SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY) + setupKeyGestureController() + testKeyGestureInternal( + TestData( + "SEARCH -> Launch Search Activity", + intArrayOf(KeyEvent.KEYCODE_SEARCH), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH, + intArrayOf(KeyEvent.KEYCODE_SEARCH), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ) + ) + } + + @Test + fun testSettingKeyGestures_doNothing() { + Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior)) + .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTHING) + setupKeyGestureController() + testKeyGestureNotProduced( + "SETTINGS -> Do Nothing", + intArrayOf(KeyEvent.KEYCODE_SETTINGS), + ) + } + + @Test + fun testSettingKeyGestures_settingsActivity() { + Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior)) + .thenReturn(SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY) + setupKeyGestureController() + testKeyGestureInternal( + TestData( + "SETTINGS -> Launch Settings Activity", + intArrayOf(KeyEvent.KEYCODE_SETTINGS), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS, + intArrayOf(KeyEvent.KEYCODE_SETTINGS), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ) + ) + } + + @Test + fun testSettingKeyGestures_notificationPanel() { + Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior)) + .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL) + setupKeyGestureController() + testKeyGestureInternal( + TestData( + "SETTINGS -> Toggle Notification Panel", + intArrayOf(KeyEvent.KEYCODE_SETTINGS), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, + intArrayOf(KeyEvent.KEYCODE_SETTINGS), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ) + ) + } + + @Test + fun testCapsLockPressNotified() { + setupKeyGestureController() + val listener = KeyGestureEventListener() + + keyGestureController.registerKeyGestureEventListener(listener, 0) + sendKeys(intArrayOf(KeyEvent.KEYCODE_CAPS_LOCK)) + testLooper.dispatchAll() + assertEquals( + "Listener should get callbacks on key gesture event completed", + 1, + events.size + ) + assertEquals( + "Listener should get callback for Toggle Caps Lock key gesture complete event", + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, + events[0].keyGestureType + ) + } + + @Keep + private fun systemGesturesTestArguments_forKeyCombinations(): Array<TestData> { + return arrayOf( + TestData( + "VOLUME_DOWN + POWER -> Screenshot Chord", + intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER), + KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD, + intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER), + 0, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_START, + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ) + ), + TestData( + "POWER + STEM_PRIMARY -> Screenshot Chord", + intArrayOf(KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY), + KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD, + intArrayOf(KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY), + 0, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_START, + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ) + ), + TestData( + "VOLUME_DOWN + VOLUME_UP -> Accessibility Chord", + intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP), + KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, + intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP), + 0, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_START, + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ) + ), + TestData( + "BACK + DPAD_DOWN -> TV Accessibility Chord", + intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), + KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, + intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN), + 0, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_START, + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ) + ), + TestData( + "BACK + DPAD_CENTER -> TV Trigger Bug Report", + intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER), + KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT, + intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER), + 0, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_START, + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ) + ), + ) + } + + @Test + @Parameters(method = "systemGesturesTestArguments_forKeyCombinations") + @EnableFlags( + com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER, + com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_PRESS_GESTURES + ) + fun testKeyCombinationGestures(test: TestData) { + setupKeyGestureController() + testKeyGestureInternal(test) + } + + @Keep + private fun customInputGesturesTestArguments(): Array<TestData> { + return arrayOf( + TestData( + "META + ALT + Q -> Go Home", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_Q + ), + KeyGestureEvent.KEY_GESTURE_TYPE_HOME, + intArrayOf(KeyEvent.KEYCODE_Q), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ) + ), + TestData( + "META + ALT + Q -> Launch app", + intArrayOf( + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_Q + ), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_Q), + KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ), + AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest") + ), + ) + } + + @Test + @Parameters(method = "customInputGesturesTestArguments") + fun testCustomKeyGestures(test: TestData) { + setupKeyGestureController() + val builder = InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger( + InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) + ) + if (test.expectedAppLaunchData != null) { + builder.setAppLaunchData(test.expectedAppLaunchData) + } + val inputGestureData = builder.build() + + keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData) + testKeyGestureInternal(test) + } + + class TouchpadTestData( + val name: String, + val touchpadGestureType: Int, + val expectedKeyGestureType: Int, + val expectedAction: Int, + val expectedAppLaunchData: AppLaunchData? = null, + ) { + override fun toString(): String = name + } + + @Keep + private fun customTouchpadGesturesTestArguments(): Array<TouchpadTestData> { + return arrayOf( + TouchpadTestData( + "3 Finger Tap -> Go Home", + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP, + KeyGestureEvent.KEY_GESTURE_TYPE_HOME, + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ), + TouchpadTestData( + "3 Finger Tap -> Launch app", + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest") + ), + ) + } + + @Test + @Parameters(method = "customTouchpadGesturesTestArguments") + fun testCustomTouchpadGesture(test: TouchpadTestData) { + setupKeyGestureController() + val builder = InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger(InputGestureData.createTouchpadTrigger(test.touchpadGestureType)) + if (test.expectedAppLaunchData != null) { + builder.setAppLaunchData(test.expectedAppLaunchData) + } + val inputGestureData = builder.build() + + keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData) + + val handledEvents = mutableListOf<KeyGestureEvent>() + val handler = KeyGestureHandler { event, _ -> + handledEvents.add(KeyGestureEvent(event)) + true + } + keyGestureController.registerKeyGestureHandler(handler, 0) + handledEvents.clear() + + keyGestureController.handleTouchpadGesture(test.touchpadGestureType) + + assertEquals( + "Test: $test doesn't produce correct number of key gesture events", + 1, + handledEvents.size + ) + val event = handledEvents[0] + assertEquals( + "Test: $test doesn't produce correct key gesture type", + test.expectedKeyGestureType, + event.keyGestureType + ) + assertEquals( + "Test: $test doesn't produce correct key gesture action", + test.expectedAction, + event.action + ) + assertEquals( + "Test: $test doesn't produce correct app launch data", + test.expectedAppLaunchData, + event.appLaunchData + ) + + keyGestureController.unregisterKeyGestureHandler(handler, 0) + } + + private fun testKeyGestureInternal(test: TestData) { + val handledEvents = mutableListOf<KeyGestureEvent>() + val handler = KeyGestureHandler { event, _ -> + handledEvents.add(KeyGestureEvent(event)) + true + } + keyGestureController.registerKeyGestureHandler(handler, 0) + handledEvents.clear() + + sendKeys(test.keys) + + assertEquals( + "Test: $test doesn't produce correct number of key gesture events", + test.expectedActions.size, + handledEvents.size + ) + for (i in handledEvents.indices) { + val event = handledEvents[i] + assertArrayEquals( + "Test: $test doesn't produce correct key gesture keycodes", + test.expectedKeys, + event.keycodes + ) + assertEquals( + "Test: $test doesn't produce correct key gesture modifier state", + test.expectedModifierState, + event.modifierState + ) + assertEquals( + "Test: $test doesn't produce correct key gesture type", + test.expectedKeyGestureType, + event.keyGestureType + ) + assertEquals( + "Test: $test doesn't produce correct key gesture action", + test.expectedActions[i], + event.action + ) + assertEquals( + "Test: $test doesn't produce correct app launch data", + test.expectedAppLaunchData, + event.appLaunchData + ) + } + + keyGestureController.unregisterKeyGestureHandler(handler, 0) + } + + private fun testKeyGestureNotProduced(testName: String, testKeys: IntArray) { + var handledEvents = mutableListOf<KeyGestureEvent>() + val handler = KeyGestureHandler { event, _ -> + handledEvents.add(KeyGestureEvent(event)) + true + } + keyGestureController.registerKeyGestureHandler(handler, 0) + handledEvents.clear() + + sendKeys(testKeys) + assertEquals("Test: $testName should not produce Key gesture", 0, handledEvents.size) + } + + private fun sendKeys(testKeys: IntArray, assertNotSentToApps: Boolean = false) { + var metaState = 0 + val now = SystemClock.uptimeMillis() + for (key in testKeys) { + val downEvent = KeyEvent( + now, now, KeyEvent.ACTION_DOWN, key, 0 /*repeat*/, metaState, + DEVICE_ID, 0 /*scancode*/, 0 /*flags*/, + InputDevice.SOURCE_KEYBOARD + ) + interceptKey(downEvent, assertNotSentToApps) + metaState = metaState or MODIFIER.getOrDefault(key, 0) + + downEvent.recycle() + testLooper.dispatchAll() + } + + for (key in testKeys.reversed()) { + val upEvent = KeyEvent( + now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState, + DEVICE_ID, 0 /*scancode*/, 0 /*flags*/, + InputDevice.SOURCE_KEYBOARD + ) + interceptKey(upEvent, assertNotSentToApps) + metaState = metaState and MODIFIER.getOrDefault(key, 0).inv() + + upEvent.recycle() + testLooper.dispatchAll() + } + } + + private fun interceptKey(event: KeyEvent, assertNotSentToApps: Boolean) { + keyGestureController.interceptKeyBeforeQueueing(event, FLAG_INTERACTIVE) + testLooper.dispatchAll() + + val consumed = + keyGestureController.interceptKeyBeforeDispatching(null, event, 0) == -1L + if (assertNotSentToApps) { + assertTrue( + "interceptKeyBeforeDispatching should consume all events $event", + consumed + ) + } + if (!consumed) { + keyGestureController.interceptUnhandledKey(event, null) + } + } + + inner class KeyGestureEventListener : IKeyGestureEventListener.Stub() { + override fun onKeyGestureEvent(event: AidlKeyGestureEvent) { + events.add(KeyGestureEvent(event)) + } + } + + inner class KeyGestureHandler( + private var handler: (event: AidlKeyGestureEvent, token: IBinder?) -> Boolean + ) : IKeyGestureHandler.Stub() { + override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean { + return handler(event, token) + } + + override fun isKeyGestureSupported(gestureType: Int): Boolean { + return true + } + } +}
\ No newline at end of file diff --git a/tests/Input/src/com/android/server/input/KeyRemapperTests.kt b/tests/Input/src/com/android/server/input/KeyRemapperTests.kt index f74fd723d540..4f4c97bef4c0 100644 --- a/tests/Input/src/com/android/server/input/KeyRemapperTests.kt +++ b/tests/Input/src/com/android/server/input/KeyRemapperTests.kt @@ -18,16 +18,18 @@ 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 com.android.test.input.MockInputManagerRule +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -35,10 +37,6 @@ 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() @@ -73,15 +71,15 @@ class KeyRemapperTests { @get:Rule val rule = MockitoJUnit.rule()!! - @Mock - private lateinit var iInputManager: IInputManager + @get:Rule + val inputManagerRule = MockInputManagerRule() + @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() { @@ -104,25 +102,17 @@ class KeyRemapperTests { 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() - } + Mockito.`when`(inputManagerRule.mock.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) } @Test fun testKeyRemapping_whenRemappingEnabled() { ModifierRemappingFlag(true).use { val keyboard = createKeyboard(DEVICE_ID) - Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard) + Mockito.`when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboard) for (i in REMAPPABLE_KEYS.indices) { val fromKeyCode = REMAPPABLE_KEYS[i] @@ -160,7 +150,7 @@ class KeyRemapperTests { fun testKeyRemapping_whenRemappingDisabled() { ModifierRemappingFlag(false).use { val keyboard = createKeyboard(DEVICE_ID) - Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboard) + Mockito.`when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboard) mKeyRemapper.remapKey(REMAPPABLE_KEYS[0], REMAPPABLE_KEYS[1]) testLooper.dispatchAll() diff --git a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt index 59aa96c46336..58fb4e1ed103 100644 --- a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -20,11 +20,9 @@ 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 @@ -35,7 +33,11 @@ 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 com.android.test.input.MockInputManagerRule +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals @@ -52,10 +54,6 @@ 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() @@ -100,10 +98,10 @@ class KeyboardBacklightControllerTests { @get:Rule val rule = MockitoJUnit.rule()!! + @get:Rule + val inputManagerRule = MockInputManagerRule() @Mock - private lateinit var iInputManager: IInputManager - @Mock private lateinit var native: NativeInputManagerService @Mock private lateinit var uEventManager: UEventManager @@ -111,7 +109,6 @@ class KeyboardBacklightControllerTests { 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 @@ -134,10 +131,9 @@ class KeyboardBacklightControllerTests { 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`(inputManagerRule.mock.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) @@ -152,13 +148,6 @@ class KeyboardBacklightControllerTests { } } - @After - fun tearDown() { - if (this::inputManagerGlobalSession.isInitialized) { - inputManagerGlobalSession.close() - } - } - @Test fun testKeyboardBacklightIncrementDecrement() { KeyboardBacklightFlags( @@ -168,8 +157,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, @@ -186,8 +176,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithoutBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) incrementKeyboardBacklight(DEVICE_ID) @@ -205,8 +196,9 @@ class KeyboardBacklightControllerTests { 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( + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn( listOf( keyboardBacklight, keyboardInputLight @@ -230,8 +222,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) for (level in 1 until DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size) { dataStore.setKeyboardBacklightBrightness( @@ -263,7 +256,8 @@ class KeyboardBacklightControllerTests { ).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) dataStore.setKeyboardBacklightBrightness( keyboardWithBacklight.descriptor, LIGHT_ID, @@ -278,7 +272,7 @@ class KeyboardBacklightControllerTests { lightColorMap.isEmpty() ) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) keyboardBacklightController.notifyUserActivity() testLooper.dispatchNext() @@ -300,8 +294,9 @@ class KeyboardBacklightControllerTests { 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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) // Register backlight listener @@ -352,8 +347,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) dataStore.setKeyboardBacklightBrightness( keyboardWithBacklight.descriptor, LIGHT_ID, @@ -388,8 +384,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) dataStore.setKeyboardBacklightBrightness( keyboardWithBacklight.descriptor, LIGHT_ID, @@ -482,8 +479,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) incrementKeyboardBacklight(DEVICE_ID) @@ -511,8 +509,9 @@ class KeyboardBacklightControllerTests { 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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, @@ -531,8 +530,9 @@ class KeyboardBacklightControllerTests { 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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, @@ -551,8 +551,9 @@ class KeyboardBacklightControllerTests { 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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) // Framework will add the lowest and maximum levels if not provided via config @@ -572,8 +573,10 @@ class KeyboardBacklightControllerTests { 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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)) + .thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) // Framework will drop out of bound levels in the config @@ -591,8 +594,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) dataStore.setKeyboardBacklightBrightness( keyboardWithBacklight.descriptor, @@ -619,8 +623,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) incrementKeyboardBacklight(DEVICE_ID) @@ -642,8 +647,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) sendAmbientBacklightValue(1) assertEquals( @@ -671,8 +677,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) sendAmbientBacklightValue(254) assertEquals( @@ -701,8 +708,9 @@ class KeyboardBacklightControllerTests { ).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)) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)) + .thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) incrementKeyboardBacklight(DEVICE_ID) assertEquals( diff --git a/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt new file mode 100644 index 000000000000..5da0beb9cc8a --- /dev/null +++ b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.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.InputManager +import android.hardware.input.KeyGlyphMap.KeyCombination +import android.os.Bundle +import android.os.test.TestLooper +import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule +import android.view.InputDevice +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import com.android.hardware.input.Flags +import com.android.test.input.MockInputManagerRule +import com.android.test.input.R +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +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 + +/** + * Tests for custom keyboard glyph map configuration. + * + * Build/Install/Run: + * atest InputTests:KeyboardGlyphManagerTests + */ +@Presubmit +class KeyboardGlyphManagerTests { + + companion object { + const val DEVICE_ID = 1 + const val VENDOR_ID = 0x1234 + const val PRODUCT_ID = 0x3456 + const val DEVICE_ID2 = 2 + const val VENDOR_ID2 = 0x1235 + const val PRODUCT_ID2 = 0x3457 + const val PACKAGE_NAME = "KeyboardLayoutManagerTests" + const val RECEIVER_NAME = "DummyReceiver" + } + + @get:Rule + val setFlagsRule = SetFlagsRule() + @get:Rule + val mockitoRule = MockitoJUnit.rule()!! + @get:Rule + val inputManagerRule = MockInputManagerRule() + + @Mock + private lateinit var packageManager: PackageManager + + private lateinit var keyboardGlyphManager: KeyboardGlyphManager + private lateinit var context: Context + private lateinit var testLooper: TestLooper + private lateinit var keyboardDevice: InputDevice + + @Before + fun setup() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + testLooper = TestLooper() + keyboardGlyphManager = KeyboardGlyphManager(context, testLooper.looper) + + setupInputDevices() + setupBroadcastReceiver() + keyboardGlyphManager.systemRunning() + testLooper.dispatchAll() + } + + private fun setupInputDevices() { + val inputManager = InputManager(context) + Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + keyboardDevice = createKeyboard(DEVICE_ID, VENDOR_ID, PRODUCT_ID, 0, "", "") + Mockito.`when`(inputManagerRule.mock.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, DEVICE_ID2)) + Mockito.`when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) + + val keyboardDevice2 = createKeyboard(DEVICE_ID2, VENDOR_ID2, PRODUCT_ID2, 0, "", "") + Mockito.`when`(inputManagerRule.mock.getInputDevice(DEVICE_ID2)).thenReturn(keyboardDevice2) + } + + 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 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_GLYPH_MAPS, + R.xml.keyboard_glyph_maps + ) + info.serviceInfo = ServiceInfo() + info.serviceInfo.packageName = PACKAGE_NAME + info.serviceInfo.name = RECEIVER_NAME + return info + } + + @Test + @EnableFlags(Flags.FLAG_KEYBOARD_GLYPH_MAP) + fun testGlyphMapsLoaded() { + assertNotNull( + "Glyph map for test keyboard(deviceId=$DEVICE_ID) must exist", + keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID) + ) + assertNotNull( + "Glyph map for test keyboard(deviceId=$DEVICE_ID2) must exist", + keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID2) + ) + assertNull( + "Glyph map for non-existing keyboard must be null", + keyboardGlyphManager.getKeyGlyphMap(-2) + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYBOARD_GLYPH_MAP) + fun testGlyphMapCorrectlyLoaded() { + val glyphMap = keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID) + // Test glyph map used in this test: {@see test_glyph_map.xml} + assertNotNull(glyphMap!!.getDrawableForKeycode(context, KeyEvent.KEYCODE_BACK)) + + assertNotNull(glyphMap.getDrawableForModifier(context, KeyEvent.KEYCODE_META_LEFT)) + assertNotNull(glyphMap.getDrawableForModifier(context, KeyEvent.KEYCODE_META_RIGHT)) + assertNotNull(glyphMap.getDrawableForModifierState(context, KeyEvent.META_META_ON)) + + val functionRowKeys = glyphMap.functionRowKeys + assertEquals(1, functionRowKeys.size) + assertEquals(KeyEvent.KEYCODE_EMOJI_PICKER, functionRowKeys[0]) + + val hardwareShortcuts = glyphMap.hardwareShortcuts + assertEquals(2, hardwareShortcuts.size) + assertEquals( + KeyEvent.KEYCODE_BACK, + hardwareShortcuts[KeyCombination(KeyEvent.META_FUNCTION_ON, KeyEvent.KEYCODE_1)] + ) + assertEquals( + KeyEvent.KEYCODE_HOME, + hardwareShortcuts[ + KeyCombination( + KeyEvent.META_FUNCTION_ON or KeyEvent.META_META_ON, + KeyEvent.KEYCODE_2 + ) + ] + ) + } +} diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt index 93f97cb4a7ee..d6654cceb458 100644 --- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -24,11 +24,10 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo -import android.hardware.input.KeyboardLayoutSelectionResult -import android.hardware.input.IInputManager import android.hardware.input.InputManager import android.hardware.input.InputManagerGlobal import android.hardware.input.KeyboardLayout +import android.hardware.input.KeyboardLayoutSelectionResult import android.icu.util.ULocale import android.os.Bundle import android.os.test.TestLooper @@ -42,8 +41,12 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito import com.android.internal.os.KeyboardConfiguredProto import com.android.internal.util.FrameworkStatsLog import com.android.modules.utils.testing.ExtendedMockitoRule +import com.android.test.input.MockInputManagerRule import com.android.test.input.R -import org.junit.After +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue @@ -53,12 +56,8 @@ import org.junit.Test import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -private fun createKeyboard( +fun createKeyboard( deviceId: Int, vendorId: Int, productId: Int, @@ -120,8 +119,8 @@ class KeyboardLayoutManagerTests { val extendedMockitoRule = ExtendedMockitoRule.Builder(this) .mockStatic(FrameworkStatsLog::class.java).build()!! - @Mock - private lateinit var iInputManager: IInputManager + @get:Rule + val inputManagerRule = MockInputManagerRule() @Mock private lateinit var native: NativeInputManagerService @@ -148,7 +147,6 @@ class KeyboardLayoutManagerTests { @Before fun setup() { context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) - inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { override fun openRead(): InputStream? { throw FileNotFoundException() @@ -171,13 +169,6 @@ class KeyboardLayoutManagerTests { 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))) @@ -191,19 +182,19 @@ class KeyboardLayoutManagerTests { DEFAULT_PRODUCT_ID, DEFAULT_DEVICE_BUS, "en", "dvorak") englishQwertyKeyboardDevice = createKeyboard(ENGLISH_QWERTY_DEVICE_ID, DEFAULT_VENDOR_ID, DEFAULT_PRODUCT_ID, DEFAULT_DEVICE_BUS, "en", "qwerty") - Mockito.`when`(iInputManager.inputDeviceIds) + Mockito.`when`(inputManagerRule.mock.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)) + Mockito.`when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) + Mockito.`when`(inputManagerRule.mock.getInputDevice(VENDOR_SPECIFIC_DEVICE_ID)) .thenReturn(vendorSpecificKeyboardDevice) - Mockito.`when`(iInputManager.getInputDevice(ENGLISH_DVORAK_DEVICE_ID)) + Mockito.`when`(inputManagerRule.mock.getInputDevice(ENGLISH_DVORAK_DEVICE_ID)) .thenReturn(englishDvorakKeyboardDevice) - Mockito.`when`(iInputManager.getInputDevice(ENGLISH_QWERTY_DEVICE_ID)) + Mockito.`when`(inputManagerRule.mock.getInputDevice(ENGLISH_QWERTY_DEVICE_ID)) .thenReturn(englishQwertyKeyboardDevice) } diff --git a/tests/Input/src/com/android/server/input/PointerIconCacheTest.kt b/tests/Input/src/com/android/server/input/PointerIconCacheTest.kt new file mode 100644 index 000000000000..47e7ac720a08 --- /dev/null +++ b/tests/Input/src/com/android/server/input/PointerIconCacheTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.os.Handler +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.view.Display +import android.view.PointerIcon +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** + * Tests for {@link PointerIconCache}. + */ +@Presubmit +class PointerIconCacheTest { + + @get:Rule + val rule = MockitoJUnit.rule()!! + + @Mock + private lateinit var native: NativeInputManagerService + @Mock + private lateinit var defaultDisplay: Display + + private lateinit var context: Context + private lateinit var testLooper: TestLooper + private lateinit var cache: PointerIconCache + + @Before + fun setup() { + whenever(defaultDisplay.displayId).thenReturn(Display.DEFAULT_DISPLAY) + + context = object : ContextWrapper(InstrumentationRegistry.getInstrumentation().context) { + override fun getDisplay() = defaultDisplay + } + + testLooper = TestLooper() + cache = PointerIconCache(context, native, Handler(testLooper.looper)) + } + + @Test + fun testSetPointerScale() { + val defaultBitmap = getDefaultIcon().bitmap + cache.setPointerScale(2f) + + testLooper.dispatchAll() + verify(native).reloadPointerIcons() + + val bitmap = + cache.getLoadedPointerIcon(Display.DEFAULT_DISPLAY, PointerIcon.TYPE_ARROW).bitmap + + assertEquals(defaultBitmap.height * 2, bitmap.height) + assertEquals(defaultBitmap.width * 2, bitmap.width) + } + + @Test + fun testSetAccessibilityScaleFactor() { + val defaultBitmap = getDefaultIcon().bitmap + cache.setAccessibilityScaleFactor(Display.DEFAULT_DISPLAY, 4f) + + testLooper.dispatchAll() + verify(native).reloadPointerIcons() + + val bitmap = + cache.getLoadedPointerIcon(Display.DEFAULT_DISPLAY, PointerIcon.TYPE_ARROW).bitmap + + assertEquals(defaultBitmap.height * 4, bitmap.height) + assertEquals(defaultBitmap.width * 4, bitmap.width) + } + + @Test + fun testSetAccessibilityScaleFactorOnSecondaryDisplay() { + val defaultBitmap = getDefaultIcon().bitmap + val secondaryDisplayId = Display.DEFAULT_DISPLAY + 1 + cache.setAccessibilityScaleFactor(secondaryDisplayId, 4f) + + testLooper.dispatchAll() + verify(native).reloadPointerIcons() + + val bitmap = + cache.getLoadedPointerIcon(Display.DEFAULT_DISPLAY, PointerIcon.TYPE_ARROW).bitmap + assertEquals(defaultBitmap.height, bitmap.height) + assertEquals(defaultBitmap.width, bitmap.width) + + val bitmapSecondary = + cache.getLoadedPointerIcon(secondaryDisplayId, PointerIcon.TYPE_ARROW).bitmap + assertEquals(defaultBitmap.height * 4, bitmapSecondary.height) + assertEquals(defaultBitmap.width * 4, bitmapSecondary.width) + } + + @Test + fun testSetPointerScaleAndAccessibilityScaleFactor() { + val defaultBitmap = getDefaultIcon().bitmap + cache.setPointerScale(2f) + cache.setAccessibilityScaleFactor(Display.DEFAULT_DISPLAY, 3f) + + testLooper.dispatchAll() + verify(native, times(2)).reloadPointerIcons() + + val bitmap = + cache.getLoadedPointerIcon(Display.DEFAULT_DISPLAY, PointerIcon.TYPE_ARROW).bitmap + + assertEquals(defaultBitmap.height * 6, bitmap.height) + assertEquals(defaultBitmap.width * 6, bitmap.width) + } + + private fun getDefaultIcon() = + PointerIcon.getLoadedSystemIcon(context, PointerIcon.TYPE_ARROW, false, 1f) +} diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java new file mode 100644 index 000000000000..5875520cd259 --- /dev/null +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java @@ -0,0 +1,219 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input.debug; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Rect; +import android.hardware.input.InputManager; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; +import android.view.InputDevice; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.input.InputManagerService; +import com.android.server.input.TouchpadHardwareProperties; + +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.MockitoRule; + +/** + * Build/Install/Run: + * atest TouchpadDebugViewControllerTests + */ + +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class TouchpadDebugViewControllerTests { + private static final int DEVICE_ID = 1000; + private static final String TAG = "TouchpadDebugViewController"; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private Context mContext; + private TouchpadDebugViewController mTouchpadDebugViewController; + @Mock + private InputManager mInputManagerMock; + @Mock + private InputManagerService mInputManagerServiceMock; + @Mock + private WindowManager mWindowManagerMock; + private TestableLooper mTestableLooper; + + @Before + public void setup() throws Exception { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + TestableContext mTestableContext = new TestableContext(mContext); + mTestableContext.addMockSystemService(WindowManager.class, mWindowManagerMock); + + Rect bounds = new Rect(0, 0, 2560, 1600); + WindowMetrics metrics = new WindowMetrics(bounds, new WindowInsets(bounds), 1.0f); + + when(mWindowManagerMock.getCurrentWindowMetrics()).thenReturn(metrics); + + unMockTouchpad(); + + mTestableLooper = TestableLooper.get(this); + + mTestableContext.addMockSystemService(InputManager.class, mInputManagerMock); + when(mInputManagerServiceMock.getTouchpadHardwareProperties(DEVICE_ID)).thenReturn( + new TouchpadHardwareProperties.Builder(-100f, 100f, -100f, 100f, 45f, 45f, -5f, 5f, + (short) 10, true, false).build()); + + mTouchpadDebugViewController = new TouchpadDebugViewController(mTestableContext, + mTestableLooper.getLooper(), mInputManagerServiceMock); + } + + private InputDevice createTouchpadInputDevice(int id) { + return new InputDevice.Builder() + .setId(id) + .setSources(InputDevice.SOURCE_TOUCHPAD | InputDevice.SOURCE_MOUSE) + .setName("Test Device " + id) + .build(); + } + + private void mockTouchpad() { + when(mInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{DEVICE_ID}); + when(mInputManagerMock.getInputDevice(eq(DEVICE_ID))).thenReturn( + createTouchpadInputDevice(DEVICE_ID)); + } + + private void unMockTouchpad() { + when(mInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{}); + when(mInputManagerMock.getInputDevice(eq(DEVICE_ID))).thenReturn(null); + } + + @Test + public void touchpadConnectedWhileSettingDisabled() throws Exception { + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false); + + mockTouchpad(); + mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID); + + verify(mWindowManagerMock, never()).addView(any(), any()); + verify(mWindowManagerMock, never()).removeView(any()); + } + + @Test + public void settingEnabledWhileNoTouchpadConnected() throws Exception { + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true); + + verify(mWindowManagerMock, never()).addView(any(), any()); + verify(mWindowManagerMock, never()).removeView(any()); + } + + @Test + public void touchpadConnectedWhileSettingEnabled() throws Exception { + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true); + + mockTouchpad(); + mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID); + + verify(mWindowManagerMock, times(1)).addView(any(), any()); + verify(mWindowManagerMock, never()).removeView(any()); + } + + @Test + public void touchpadConnectedWhileSettingEnabledThenDisabled() throws Exception { + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true); + + mockTouchpad(); + mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID); + + verify(mWindowManagerMock, times(1)).addView(any(), any()); + verify(mWindowManagerMock, never()).removeView(any()); + + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false); + + verify(mWindowManagerMock, times(1)).addView(any(), any()); + verify(mWindowManagerMock, times(1)).removeView(any()); + } + + @Test + public void touchpadConnectedWhileSettingDisabledThenEnabled() throws Exception { + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false); + + mockTouchpad(); + mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID); + + verify(mWindowManagerMock, never()).addView(any(), any()); + verify(mWindowManagerMock, never()).removeView(any()); + + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true); + + verify(mWindowManagerMock, times(1)).addView(any(), any()); + verify(mWindowManagerMock, never()).removeView(any()); + } + + @Test + public void touchpadConnectedWhileSettingDisabledThenTouchpadDisconnected() throws Exception { + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false); + + mockTouchpad(); + mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID); + + verify(mWindowManagerMock, never()).addView(any(), any()); + verify(mWindowManagerMock, never()).removeView(any()); + + unMockTouchpad(); + mTouchpadDebugViewController.onInputDeviceRemoved(DEVICE_ID); + + verify(mWindowManagerMock, never()).addView(any(), any()); + verify(mWindowManagerMock, never()).removeView(any()); + } + + @Test + public void touchpadConnectedWhileSettingEnabledThenTouchpadDisconnectedThenSettingDisabled() + throws Exception { + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(true); + + mockTouchpad(); + mTouchpadDebugViewController.onInputDeviceAdded(DEVICE_ID); + + verify(mWindowManagerMock, times(1)).addView(any(), any()); + verify(mWindowManagerMock, never()).removeView(any()); + + unMockTouchpad(); + mTouchpadDebugViewController.onInputDeviceRemoved(DEVICE_ID); + + verify(mWindowManagerMock, times(1)).addView(any(), any()); + verify(mWindowManagerMock, times(1)).removeView(any()); + + mTouchpadDebugViewController.updateTouchpadVisualizerEnabled(false); + + verify(mWindowManagerMock, times(1)).addView(any(), any()); + verify(mWindowManagerMock, times(1)).removeView(any()); + } +} diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java new file mode 100644 index 000000000000..60fa52f85e34 --- /dev/null +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java @@ -0,0 +1,474 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input.debug; + +import static android.view.InputDevice.SOURCE_MOUSE; +import static android.view.InputDevice.SOURCE_TOUCHSCREEN; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.hardware.input.InputManager; +import android.testing.TestableContext; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; +import android.widget.TextView; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.cts.input.MotionEventBuilder; +import com.android.cts.input.PointerBuilder; +import com.android.server.input.TouchpadFingerState; +import com.android.server.input.TouchpadHardwareProperties; +import com.android.server.input.TouchpadHardwareState; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.function.Consumer; + +/** + * Build/Install/Run: + * atest TouchpadDebugViewTest + */ +@RunWith(AndroidJUnit4.class) +public class TouchpadDebugViewTest { + private static final int TOUCHPAD_DEVICE_ID = 60; + + private TouchpadDebugView mTouchpadDebugView; + private WindowManager.LayoutParams mWindowLayoutParams; + + @Mock + WindowManager mWindowManager; + @Mock + InputManager mInputManager; + + Rect mWindowBounds; + WindowMetrics mWindowMetrics; + TestableContext mTestableContext; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + mTestableContext = new TestableContext(context); + + mTestableContext.addMockSystemService(WindowManager.class, mWindowManager); + mTestableContext.addMockSystemService(InputManager.class, mInputManager); + + mWindowBounds = new Rect(0, 0, 2560, 1600); + mWindowMetrics = new WindowMetrics(mWindowBounds, new WindowInsets(mWindowBounds), 1.0f); + + when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics); + + InputDevice inputDevice = new InputDevice.Builder() + .setId(TOUCHPAD_DEVICE_ID) + .setSources(InputDevice.SOURCE_TOUCHPAD | SOURCE_MOUSE) + .setName("Test Device " + TOUCHPAD_DEVICE_ID) + .build(); + + when(mInputManager.getInputDevice(TOUCHPAD_DEVICE_ID)).thenReturn(inputDevice); + + Consumer<Integer> touchpadSwitchHandler = id -> {}; + + mTouchpadDebugView = new TouchpadDebugView(mTestableContext, TOUCHPAD_DEVICE_ID, + new TouchpadHardwareProperties.Builder(0f, 0f, 500f, + 500f, 45f, 47f, -4f, 5f, (short) 10, true, + true).build(), touchpadSwitchHandler); + + mTouchpadDebugView.measure( + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ); + + doAnswer(invocation -> { + mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), + mTouchpadDebugView.getMeasuredHeight()); + return null; + }).when(mWindowManager).addView(any(), any()); + + doAnswer(invocation -> { + mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), + mTouchpadDebugView.getMeasuredHeight()); + return null; + }).when(mWindowManager).updateViewLayout(any(), any()); + + mWindowLayoutParams = mTouchpadDebugView.getWindowLayoutParams(); + mWindowLayoutParams.x = 20; + mWindowLayoutParams.y = 20; + + mTouchpadDebugView.layout(0, 0, mTouchpadDebugView.getMeasuredWidth(), + mTouchpadDebugView.getMeasuredHeight()); + } + + @Test + public void testDragView() { + // Initial view position relative to screen. + int initialX = mWindowLayoutParams.x; + int initialY = mWindowLayoutParams.y; + + float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; + float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; + + // Simulate ACTION_DOWN event (initial touch). + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(40f) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + verify(mWindowManager, times(0)).updateViewLayout(any(), any()); + + // Simulate ACTION_MOVE event (dragging to the right). + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f + offsetX) + .y(40f + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = + ArgumentCaptor.forClass(WindowManager.LayoutParams.class); + verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); + + // Verify position after ACTION_MOVE + assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); + assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); + + // Simulate ACTION_UP event (release touch). + MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f + offsetX) + .y(40f + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionUp); + + assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); + assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); + } + + @Test + public void testDragViewOutOfBounds() { + int initialX = mWindowLayoutParams.x; + int initialY = mWindowLayoutParams.y; + + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX + 10f) + .y(initialY + 10f) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + verify(mWindowManager, times(0)).updateViewLayout(any(), any()); + + // Simulate ACTION_MOVE event (dragging far to the right and bottom, beyond screen bounds) + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) + .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = + ArgumentCaptor.forClass(WindowManager.LayoutParams.class); + verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); + + // Verify the view has been clamped to the right and bottom edges of the screen + assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), + mWindowLayoutParamsCaptor.getValue().x); + assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), + mWindowLayoutParamsCaptor.getValue().y); + + MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(mWindowBounds.width() + mTouchpadDebugView.getWidth()) + .y(mWindowBounds.height() + mTouchpadDebugView.getHeight()) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionUp); + + // Verify the view has been clamped to the right and bottom edges of the screen + assertEquals(mWindowBounds.width() - mTouchpadDebugView.getWidth(), + mWindowLayoutParamsCaptor.getValue().x); + assertEquals(mWindowBounds.height() - mTouchpadDebugView.getHeight(), + mWindowLayoutParamsCaptor.getValue().y); + } + + @Test + public void testSlopOffset() { + int initialX = mWindowLayoutParams.x; + int initialY = mWindowLayoutParams.y; + + float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f; + float offsetY = -(ViewConfiguration.get(mTestableContext).getScaledTouchSlop() / 2.0f); + + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX) + .y(initialY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX + offsetX) + .y(initialY + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX) + .y(initialY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionUp); + + // In this case the updateViewLayout() method wouldn't be called because the drag + // distance hasn't exceeded the slop + verify(mWindowManager, times(0)).updateViewLayout(any(), any()); + } + + @Test + public void testViewReturnsToInitialPositionOnCancel() { + int initialX = mWindowLayoutParams.x; + int initialY = mWindowLayoutParams.y; + + float offsetX = 50; + float offsetY = 50; + + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX) + .y(initialY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX + offsetX) + .y(initialY + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + ArgumentCaptor<WindowManager.LayoutParams> mWindowLayoutParamsCaptor = + ArgumentCaptor.forClass(WindowManager.LayoutParams.class); + verify(mWindowManager).updateViewLayout(any(), mWindowLayoutParamsCaptor.capture()); + + assertEquals(initialX + (long) offsetX, mWindowLayoutParamsCaptor.getValue().x); + assertEquals(initialY + (long) offsetY, mWindowLayoutParamsCaptor.getValue().y); + + // Simulate ACTION_CANCEL event (canceling the touch event stream) + MotionEvent actionCancel = new MotionEventBuilder(MotionEvent.ACTION_CANCEL, + SOURCE_TOUCHSCREEN) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(initialX + offsetX) + .y(initialY + offsetY) + ) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionCancel); + + // Verify the view returns to its initial position + verify(mWindowManager, times(2)).updateViewLayout(any(), + mWindowLayoutParamsCaptor.capture()); + assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x); + assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y); + } + + @Test + public void testTouchpadClick() { + View child = mTouchpadDebugView.getChildAt(0); + + mTouchpadDebugView.updateHardwareState( + new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0, + new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID); + + assertEquals(((ColorDrawable) child.getBackground()).getColor(), + Color.parseColor("#769763")); + + mTouchpadDebugView.updateHardwareState( + new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, + new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID); + + assertEquals(((ColorDrawable) child.getBackground()).getColor(), + Color.parseColor("#5455A9")); + + mTouchpadDebugView.updateHardwareState( + new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0, + new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID); + + assertEquals(((ColorDrawable) child.getBackground()).getColor(), + Color.parseColor("#769763")); + + // Color should not change because hardware state of a different touchpad + mTouchpadDebugView.updateHardwareState( + new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0, + new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID + 1); + + assertEquals(((ColorDrawable) child.getBackground()).getColor(), + Color.parseColor("#769763")); + } + + @Test + public void testTouchpadGesture() { + int gestureType = 3; + TextView child = mTouchpadDebugView.getGestureInfoView(); + + mTouchpadDebugView.updateGestureInfo(gestureType, TOUCHPAD_DEVICE_ID); + assertEquals(child.getText().toString(), TouchpadDebugView.getGestureText(gestureType)); + + gestureType = 6; + mTouchpadDebugView.updateGestureInfo(gestureType, TOUCHPAD_DEVICE_ID); + assertEquals(child.getText().toString(), TouchpadDebugView.getGestureText(gestureType)); + } + + @Test + public void testTwoFingerDrag() { + float offsetX = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; + float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; + + // Simulate ACTION_DOWN event (gesture starts). + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_MOUSE) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(40f) + ) + .classification(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + // Simulate ACTION_MOVE event (dragging with two fingers, processed as one pointer). + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_MOUSE) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f + offsetX) + .y(40f + offsetY) + ) + .classification(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + // Simulate ACTION_UP event (gesture ends). + MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_MOUSE) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f + offsetX) + .y(40f + offsetY) + ) + .classification(MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionUp); + + // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture). + verify(mWindowManager, times(0)).updateViewLayout(any(), any()); + } + + @Test + public void testPinchDrag() { + float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10; + + MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_MOUSE) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(40f) + ) + .classification(MotionEvent.CLASSIFICATION_PINCH) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionDown); + + MotionEvent pointerDown = new MotionEventBuilder(MotionEvent.ACTION_POINTER_DOWN, + SOURCE_MOUSE) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(40f) + ) + .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(45f) + ) + .classification(MotionEvent.CLASSIFICATION_PINCH) + .build(); + mTouchpadDebugView.dispatchTouchEvent(pointerDown); + + // Simulate ACTION_MOVE event (both fingers moving apart). + MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_MOUSE) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(40f - offsetY) + ) + .rawXCursorPosition(mWindowLayoutParams.x + 10f) + .rawYCursorPosition(mWindowLayoutParams.y + 10f) + .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(45f + offsetY) + ) + .classification(MotionEvent.CLASSIFICATION_PINCH) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionMove); + + MotionEvent pointerUp = new MotionEventBuilder(MotionEvent.ACTION_POINTER_UP, SOURCE_MOUSE) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(40f - offsetY) + ) + .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(45f + offsetY) + ) + .classification(MotionEvent.CLASSIFICATION_PINCH) + .build(); + mTouchpadDebugView.dispatchTouchEvent(pointerUp); + + MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_MOUSE) + .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER) + .x(40f) + .y(40f - offsetY) + ) + .classification(MotionEvent.CLASSIFICATION_PINCH) + .build(); + mTouchpadDebugView.dispatchTouchEvent(actionUp); + + // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture). + verify(mWindowManager, times(0)).updateViewLayout(any(), any()); + } +} diff --git a/tests/Input/src/com/android/test/input/AnrTest.kt b/tests/Input/src/com/android/test/input/AnrTest.kt index d32cedb24a36..cd6ab30d8678 100644 --- a/tests/Input/src/com/android/test/input/AnrTest.kt +++ b/tests/Input/src/com/android/test/input/AnrTest.kt @@ -166,12 +166,12 @@ class AnrTest { val displayManager = instrumentation.context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager val display = displayManager.getDisplay(obj.getDisplayId()) - val touchScreen = UinputTouchScreen(instrumentation, display) - val rect: Rect = obj.visibleBounds - val pointer = touchScreen.touchDown(rect.centerX(), rect.centerY()) - pointer.lift() - touchScreen.close() + UinputTouchScreen(instrumentation, display).use { touchScreen -> + touchScreen + .touchDown(rect.centerX(), rect.centerY()) + .lift() + } } private fun triggerAnr() { diff --git a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt b/tests/Input/src/com/android/test/input/CaptureEventActivity.kt new file mode 100644 index 000000000000..d54e3470d9c4 --- /dev/null +++ b/tests/Input/src/com/android/test/input/CaptureEventActivity.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.input + +import android.app.Activity +import android.os.Bundle +import android.view.InputEvent +import android.view.KeyEvent +import android.view.MotionEvent +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import org.junit.Assert.assertNull + +class CaptureEventActivity : Activity() { + private val events = LinkedBlockingQueue<InputEvent>() + var shouldHandleKeyEvents = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Set the fixed orientation if requested + if (intent.hasExtra(EXTRA_FIXED_ORIENTATION)) { + val orientation = intent.getIntExtra(EXTRA_FIXED_ORIENTATION, 0) + setRequestedOrientation(orientation) + } + + // Set the flag if requested + if (intent.hasExtra(EXTRA_WINDOW_FLAGS)) { + val flags = intent.getIntExtra(EXTRA_WINDOW_FLAGS, 0) + window.addFlags(flags) + } + } + + override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean { + events.add(MotionEvent.obtain(ev)) + return true + } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + events.add(MotionEvent.obtain(ev)) + return true + } + + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + events.add(KeyEvent(event)) + return shouldHandleKeyEvents + } + + override fun dispatchTrackballEvent(ev: MotionEvent?): Boolean { + events.add(MotionEvent.obtain(ev)) + return true + } + + fun getInputEvent(): InputEvent? { + return events.poll(5, TimeUnit.SECONDS) + } + + fun hasReceivedEvents(): Boolean { + return !events.isEmpty() + } + + fun assertNoEvents() { + val event = events.poll(100, TimeUnit.MILLISECONDS) + assertNull("Expected no events, but received $event", event) + } + + companion object { + const val EXTRA_FIXED_ORIENTATION = "fixed_orientation" + const val EXTRA_WINDOW_FLAGS = "window_flags" + } +} diff --git a/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt index c1a86b3a2dac..015e188fc98e 100644 --- a/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt +++ b/tests/Input/src/com/android/test/input/InputEventAssignerTest.kt @@ -18,12 +18,20 @@ package com.android.test.input import android.view.InputDevice.SOURCE_MOUSE import android.view.InputDevice.SOURCE_TOUCHSCREEN +import android.view.InputDevice.SOURCE_STYLUS +import android.view.InputDevice.SOURCE_TOUCHPAD + import android.view.InputEventAssigner import android.view.KeyEvent import android.view.MotionEvent import org.junit.Assert.assertEquals import org.junit.Test +sealed class StreamEvent +private data object Vsync : StreamEvent() +data class MotionEventData(val action: Int, val source: Int, val id: Int, val expectedId: Int) : + StreamEvent() + /** * Create a MotionEvent with the provided action, eventTime, and source */ @@ -49,64 +57,164 @@ private fun createKeyEvent(action: Int, eventTime: Long): KeyEvent { return KeyEvent(eventTime, eventTime, action, code, repeat) } +/** + * Check that the correct eventIds are assigned in a stream. The stream consists of motion + * events or vsync (processed frame) + * Each streamEvent should have unique ids when writing tests + * The test passes even if two events get assigned the same eventId, since the mapping is + * streamEventId -> motionEventId and streamEvents have unique ids + */ +private fun checkEventStream(vararg streamEvents: StreamEvent) { + val assigner = InputEventAssigner() + var eventTime = 10L + // Maps MotionEventData.id to MotionEvent.id + // We can't control the event id of the generated motion events but for testing it's easier + // to label the events with a custom id for readability + val eventIdMap: HashMap<Int, Int> = HashMap() + for (streamEvent in streamEvents) { + when (streamEvent) { + is MotionEventData -> { + val event = createMotionEvent(streamEvent.action, eventTime, streamEvent.source) + eventIdMap[streamEvent.id] = event.id + val eventId = assigner.processEvent(event) + assertEquals(eventIdMap[streamEvent.expectedId], eventId) + } + is Vsync -> assigner.notifyFrameProcessed() + } + eventTime += 1 + } +} + class InputEventAssignerTest { companion object { private const val TAG = "InputEventAssignerTest" } /** - * A single MOVE event should be assigned to the next available frame. + * A single event should be assigned to the next available frame. */ @Test - fun testTouchGesture() { - val assigner = InputEventAssigner() - val event = createMotionEvent(MotionEvent.ACTION_MOVE, 10, SOURCE_TOUCHSCREEN) - val eventId = assigner.processEvent(event) - assertEquals(event.id, eventId) + fun testTouchMove() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_TOUCHSCREEN, id = 1, expectedId = 1) + ) + } + + @Test + fun testMouseMove() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_MOUSE, id = 1, expectedId = 1) + ) + } + + @Test + fun testMouseScroll() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 1, expectedId = 1) + ) + } + + @Test + fun testStylusMove() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_STYLUS, id = 1, expectedId = 1) + ) + } + + @Test + fun testStylusHover() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_HOVER_MOVE, SOURCE_STYLUS, id = 1, expectedId = 1) + ) + } + + @Test + fun testTouchpadMove() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_MOVE, SOURCE_STYLUS, id = 1, expectedId = 1) + ) } /** - * DOWN event should be used until a vsync comes in. After vsync, the latest event should be - * produced. + * Test that before a VSYNC the event id generated by input event assigner for move events is + * the id of the down event. Move events coming after a VSYNC should be assigned their own event + * id */ + private fun testDownAndMove(source: Int) { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, source, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_MOVE, source, id = 2, expectedId = 1), + Vsync, + MotionEventData(MotionEvent.ACTION_MOVE, source, id = 4, expectedId = 4) + ) + } + @Test - fun testTouchDownWithMove() { - val assigner = InputEventAssigner() - val down = createMotionEvent(MotionEvent.ACTION_DOWN, 10, SOURCE_TOUCHSCREEN) - val move1 = createMotionEvent(MotionEvent.ACTION_MOVE, 12, SOURCE_TOUCHSCREEN) - val move2 = createMotionEvent(MotionEvent.ACTION_MOVE, 13, SOURCE_TOUCHSCREEN) - val move3 = createMotionEvent(MotionEvent.ACTION_MOVE, 14, SOURCE_TOUCHSCREEN) - val move4 = createMotionEvent(MotionEvent.ACTION_MOVE, 15, SOURCE_TOUCHSCREEN) - var eventId = assigner.processEvent(down) - assertEquals(down.id, eventId) - eventId = assigner.processEvent(move1) - assertEquals(down.id, eventId) - eventId = assigner.processEvent(move2) - // Even though we already had 2 move events, there was no choreographer callback yet. - // Therefore, we should still get the id of the down event - assertEquals(down.id, eventId) + fun testTouchDownAndMove() { + testDownAndMove(SOURCE_TOUCHSCREEN) + } - // Now send CALLBACK_INPUT to the assigner. It should provide the latest motion event - assigner.notifyFrameProcessed() - eventId = assigner.processEvent(move3) - assertEquals(move3.id, eventId) - eventId = assigner.processEvent(move4) - assertEquals(move4.id, eventId) + @Test + fun testMouseDownAndMove() { + testDownAndMove(SOURCE_MOUSE) + } + + @Test + fun testStylusDownAndMove() { + testDownAndMove(SOURCE_STYLUS) + } + + @Test + fun testTouchpadDownAndMove() { + testDownAndMove(SOURCE_TOUCHPAD) } /** - * Similar to the above test, but with SOURCE_MOUSE. Since we don't have down latency - * concept for non-touchscreens, the latest input event will be used. + * After an up event, motion events should be assigned their own event id */ @Test - fun testMouseDownWithMove() { - val assigner = InputEventAssigner() - val down = createMotionEvent(MotionEvent.ACTION_DOWN, 10, SOURCE_MOUSE) - val move1 = createMotionEvent(MotionEvent.ACTION_MOVE, 12, SOURCE_MOUSE) - var eventId = assigner.processEvent(down) - assertEquals(down.id, eventId) - eventId = assigner.processEvent(move1) - assertEquals(move1.id, eventId) + fun testMouseDownUpAndScroll() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_MOUSE, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_UP, SOURCE_MOUSE, id = 2, expectedId = 2), + MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3) + ) + } + + /** + * After an up event, motion events should be assigned their own event id + */ + @Test + fun testStylusDownUpAndHover() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_STYLUS, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_UP, SOURCE_STYLUS, id = 2, expectedId = 2), + MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3) + ) + } + + /** + * After a cancel event, motion events should be assigned their own event id + */ + @Test + fun testMouseDownCancelAndScroll() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_MOUSE, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_CANCEL, SOURCE_MOUSE, id = 2, expectedId = 2), + MotionEventData(MotionEvent.ACTION_SCROLL, SOURCE_MOUSE, id = 3, expectedId = 3) + ) + } + + /** + * After a cancel event, motion events should be assigned their own event id + */ + @Test + fun testStylusDownCancelAndHover() { + checkEventStream( + MotionEventData(MotionEvent.ACTION_DOWN, SOURCE_STYLUS, id = 1, expectedId = 1), + MotionEventData(MotionEvent.ACTION_CANCEL, SOURCE_STYLUS, id = 2, expectedId = 2), + MotionEventData(MotionEvent.ACTION_HOVER_ENTER, SOURCE_STYLUS, id = 3, expectedId = 3) + ) } /** diff --git a/tests/Input/src/com/android/test/input/MockInputManagerRule.kt b/tests/Input/src/com/android/test/input/MockInputManagerRule.kt new file mode 100644 index 000000000000..cef985600c40 --- /dev/null +++ b/tests/Input/src/com/android/test/input/MockInputManagerRule.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.test.input + +import android.hardware.input.IInputManager +import android.hardware.input.InputManagerGlobal +import org.junit.rules.ExternalResource +import org.mockito.Mockito + +/** + * A test rule that temporarily replaces the [IInputManager] connection to the server with a mock + * to be used for testing. + */ +class MockInputManagerRule : ExternalResource() { + + private lateinit var session: InputManagerGlobal.TestSession + + val mock: IInputManager = Mockito.mock(IInputManager::class.java) + + override fun before() { + session = InputManagerGlobal.createTestSession(mock) + } + + override fun after() { + if (this::session.isInitialized) { + session.close() + } + } +} diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt index d196b85a7466..abfe549f3d22 100644 --- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt +++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt @@ -19,7 +19,6 @@ package com.android.test.input import android.content.Context import android.content.res.Configuration import android.content.res.Resources -import android.os.Environment import android.view.ContextThemeWrapper import android.view.PointerIcon import android.view.flags.Flags.enableVectorCursorA11ySettings @@ -88,6 +87,35 @@ class PointerIconLoadingTest { theme.applyStyle( PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_GREEN), /* force= */ true) + theme.applyStyle(PointerIcon.vectorStrokeStyleToResource( + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE), /* force= */ true) + + val pointerIcon = + PointerIcon.getLoadedSystemIcon( + ContextThemeWrapper(context, theme), + PointerIcon.TYPE_ARROW, + /* useLargeIcons= */ false, + /* pointerScale= */ 1f) + + pointerIcon.getBitmap().assertAgainstGolden( + screenshotRule, + testName.methodName, + exactScreenshotMatcher + ) + } + + @Test + fun testPointerStrokeStyle() { + assumeTrue(enableVectorCursors()) + assumeTrue(enableVectorCursorA11ySettings()) + + val theme: Resources.Theme = context.getResources().newTheme() + theme.setTo(context.getTheme()) + theme.applyStyle( + PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK), + /* force= */ true) + theme.applyStyle(PointerIcon.vectorStrokeStyleToResource( + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_BLACK), /* force= */ true) val pointerIcon = PointerIcon.getLoadedSystemIcon( @@ -108,11 +136,20 @@ class PointerIconLoadingTest { assumeTrue(enableVectorCursors()) assumeTrue(enableVectorCursorA11ySettings()) + val theme: Resources.Theme = context.getResources().newTheme() + theme.setTo(context.getTheme()) + theme.applyStyle( + PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK), + /* force= */ true) + theme.applyStyle( + PointerIcon.vectorStrokeStyleToResource( + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE), + /* force= */ true) val pointerScale = 2f val pointerIcon = PointerIcon.getLoadedSystemIcon( - context, + ContextThemeWrapper(context, theme), PointerIcon.TYPE_ARROW, /* useLargeIcons= */ false, pointerScale) @@ -129,8 +166,7 @@ class PointerIconLoadingTest { const val SCREEN_WIDTH_DP = 480 const val SCREEN_HEIGHT_DP = 800 const val ASSETS_PATH = "tests/input/assets" - val TEST_OUTPUT_PATH = Environment.getExternalStorageDirectory().absolutePath + - "/InputTests/" + - PointerIconLoadingTest::class.java.simpleName + val TEST_OUTPUT_PATH = + "/sdcard/Download/InputTests/" + PointerIconLoadingTest::class.java.simpleName } } diff --git a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt new file mode 100644 index 000000000000..c61a25021949 --- /dev/null +++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.test.input + +import android.Manifest.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY +import android.app.Instrumentation +import android.cts.input.EventVerifier +import android.graphics.PointF +import android.hardware.input.InputManager +import android.os.ParcelFileDescriptor +import android.util.Log +import android.util.Size +import android.view.InputEvent +import android.view.MotionEvent +import androidx.test.platform.app.InstrumentationRegistry +import com.android.cts.input.BatchedEventSplitter +import com.android.cts.input.InputJsonParser +import com.android.cts.input.VirtualDisplayActivityScenario +import com.android.cts.input.inputeventmatchers.isResampled +import com.android.cts.input.inputeventmatchers.withButtonState +import com.android.cts.input.inputeventmatchers.withHistorySize +import com.android.cts.input.inputeventmatchers.withMotionAction +import com.android.cts.input.inputeventmatchers.withPressure +import com.android.cts.input.inputeventmatchers.withRawCoords +import com.android.cts.input.inputeventmatchers.withSource +import junit.framework.Assert.fail +import org.hamcrest.Matchers.allOf +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestName +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Integration tests for the input pipeline that replays recording taken from physical input devices + * at the evdev interface level, and makes assertions on the events that are received by a test app. + * + * These tests associate the playback input device with a virtual display to make these tests + * agnostic to the device form factor. + * + * New recordings can be taken using the `evemu-record` shell command. + */ +@RunWith(Parameterized::class) +class UinputRecordingIntegrationTests { + + companion object { + /** + * Add new test cases by adding a new [TestData] to the following list. + */ + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): Iterable<Any> = + listOf( + TestData( + "GooglePixelTabletTouchscreen", R.raw.google_pixel_tablet_touchscreen, + R.raw.google_pixel_tablet_touchscreen_events, Size(1600, 2560), + vendorId = 0x0603, productId = 0x7806 + ), + ) + + /** + * Use the debug mode to see the JSON-encoded received events in logcat. + */ + const val DEBUG_RECEIVED_EVENTS = false + + const val INPUT_DEVICE_SOURCE_ALL = -1 + val TAG = UinputRecordingIntegrationTests::class.java.simpleName + } + + class TestData( + val name: String, + val uinputRecordingResource: Int, + val expectedEventsResource: Int, + val displaySize: Size, + val vendorId: Int, + val productId: Int, + ) { + override fun toString(): String = name + } + + private lateinit var instrumentation: Instrumentation + private lateinit var parser: InputJsonParser + + @get:Rule + val testName = TestName() + + @Parameterized.Parameter(0) + lateinit var testData: TestData + + @Before + fun setUp() { + instrumentation = InstrumentationRegistry.getInstrumentation() + parser = InputJsonParser(instrumentation.context) + } + + @Test + fun testEvemuRecording() { + VirtualDisplayActivityScenario.AutoClose<CaptureEventActivity>( + testName, + size = testData.displaySize + ).use { scenario -> + scenario.activity.window.decorView.requestUnbufferedDispatch(INPUT_DEVICE_SOURCE_ALL) + + try { + instrumentation.uiAutomation.adoptShellPermissionIdentity( + ASSOCIATE_INPUT_DEVICE_TO_DISPLAY, + ) + + val inputPort = "uinput:1:${testData.vendorId}:${testData.productId}" + val inputManager = + instrumentation.context.getSystemService(InputManager::class.java)!! + try { + inputManager.addUniqueIdAssociationByPort( + inputPort, + scenario.virtualDisplay.display.uniqueId!!, + ) + + injectUinputEvents().use { + if (DEBUG_RECEIVED_EVENTS) { + printReceivedEventsToLogcat(scenario.activity) + fail("Test cannot pass in debug mode!") + } + + val verifier = EventVerifier( + BatchedEventSplitter { scenario.activity.getInputEvent() } + ) + verifyEvents(verifier) + scenario.activity.assertNoEvents() + } + } finally { + inputManager.removeUniqueIdAssociationByPort(inputPort) + } + } finally { + instrumentation.uiAutomation.dropShellPermissionIdentity() + } + } + } + + private fun printReceivedEventsToLogcat(activity: CaptureEventActivity) { + val getNextEvent = BatchedEventSplitter { activity.getInputEvent() } + var receivedEvent: InputEvent? = getNextEvent() + while (receivedEvent != null) { + Log.d(TAG, + parser.encodeEvent(receivedEvent)?.toString() + ?: "(Failed to encode received event)" + ) + receivedEvent = getNextEvent() + } + } + + /** + * Plays back the evemu recording associated with the current test case by injecting it via + * the `uinput` shell command in interactive mode. The recording playback will begin + * immediately, and the shell command (and the associated input device) will remain alive + * until the returned [AutoCloseable] is closed. + */ + private fun injectUinputEvents(): AutoCloseable { + val fds = instrumentation.uiAutomation!!.executeShellCommandRw("uinput -") + // We do not need to use stdout in this test. + fds[0].close() + + return ParcelFileDescriptor.AutoCloseOutputStream(fds[1]).also { stdin -> + instrumentation.context.resources.openRawResource( + testData.uinputRecordingResource, + ).use { inputStream -> + stdin.write(inputStream.readBytes()) + + // TODO(b/367419268): Remove extra event injection when uinput parsing is fixed. + // Inject an extra sync event with an arbitrarily large timestamp, because the + // uinput command will not process the last event until either the next event is + // parsed, or fd is closed. Injecting this sync allows us complete injection of + // the evemu recording and extend the lifetime of the input device by keeping this + // fd open. + stdin.write("\nE: 9999.99 0 0 0\n".toByteArray()) + stdin.flush() + } + } + } + + private fun verifyEvents(verifier: EventVerifier) { + val uinputTestData = parser.getUinputTestData(testData.expectedEventsResource) + for (test in uinputTestData) { + for ((index, expectedEvent) in test.events.withIndex()) { + if (expectedEvent is MotionEvent) { + verifier.assertReceivedMotion( + allOf( + withMotionAction(expectedEvent.action), + withSource(expectedEvent.source), + withButtonState(expectedEvent.buttonState), + withRawCoords(PointF(expectedEvent.rawX, expectedEvent.rawY)), + withPressure(expectedEvent.pressure), + isResampled(false), + withHistorySize(0), + ), + "${test.name}: Expected event at index $index", + ) + } + } + } + } +} diff --git a/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt index 63782f1bf955..1842f0a64a83 100644 --- a/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt +++ b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt @@ -14,6 +14,9 @@ * limitations under the License. */ +// InputMonitor is deprecated, but we still need to test it. +@file:Suppress("DEPRECATION") + package com.android.test.input import android.app.Activity @@ -43,6 +46,7 @@ class UnresponsiveGestureMonitorActivity : Activity() { } private lateinit var mInputEventReceiver: InputEventReceiver private lateinit var mInputMonitor: InputMonitor + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val inputManager = checkNotNull(getSystemService(InputManager::class.java)) diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java index e60d8efdbfa4..a2c3572eca9b 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java @@ -37,14 +37,14 @@ import android.content.res.Configuration; import android.os.SystemClock; import android.platform.test.annotations.RootPermissionTest; import android.platform.test.rule.UnlockScreenRule; -import android.support.test.uiautomator.By; -import android.support.test.uiautomator.UiDevice; -import android.support.test.uiautomator.UiObject2; -import android.support.test.uiautomator.Until; import android.view.WindowManager; import android.widget.EditText; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; import org.junit.Rule; import org.junit.Test; diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java index 2ac25f2696d3..b994bfb00007 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java @@ -33,10 +33,10 @@ import static com.google.common.truth.Truth.assertWithMessage; import android.content.Intent; import android.platform.test.annotations.RootPermissionTest; import android.platform.test.rule.UnlockScreenRule; -import android.support.test.uiautomator.UiDevice; import android.widget.EditText; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; import org.junit.Rule; import org.junit.Test; diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java index 5368025ff898..2128cbf90542 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java @@ -48,12 +48,12 @@ import android.os.Build; import android.os.SystemClock; import android.platform.test.annotations.RootPermissionTest; import android.platform.test.rule.UnlockScreenRule; -import android.support.test.uiautomator.UiDevice; import android.util.Log; import android.view.WindowManager; import android.widget.EditText; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; import org.junit.Rule; import org.junit.Test; diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java index c7463218b646..1249a4564e8e 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java @@ -18,10 +18,10 @@ package com.android.inputmethod.stresstest; import android.app.Instrumentation; import android.os.RemoteException; -import android.support.test.uiautomator.UiDevice; import androidx.annotation.NonNull; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; import org.junit.rules.TestWatcher; import org.junit.runner.Description; diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp index bb4e1ec9b6f2..9f35c7b7fa33 100644 --- a/tests/Internal/Android.bp +++ b/tests/Internal/Android.bp @@ -24,6 +24,7 @@ android_test { "flickerlib-parsers", "perfetto_trace_java_protos", "flickerlib-trace_processor_shell", + "ravenwood-junit", ], java_resource_dirs: ["res"], certificate: "platform", @@ -31,6 +32,27 @@ android_test { test_suites: ["device-tests"], } +// Run just ApplicationSharedMemoryTest with ABI override for 32 bits. +// This is to test that on systems that support multi-ABI, +// ApplicationSharedMemory works in app processes launched with a different ABI +// than that of the system processes. +android_test { + name: "ApplicationSharedMemoryTest32", + team: "trendy_team_system_performance", + srcs: ["src/com/android/internal/os/ApplicationSharedMemoryTest.java"], + libs: ["android.test.runner.stubs.system"], + static_libs: [ + "junit", + "androidx.test.rules", + "platform-test-annotations", + ], + manifest: "ApplicationSharedMemoryTest32/AndroidManifest.xml", + test_config: "ApplicationSharedMemoryTest32/AndroidTest.xml", + certificate: "platform", + platform_apis: true, + test_suites: ["device-tests"], +} + android_ravenwood_test { name: "InternalTestsRavenwood", static_libs: [ @@ -39,7 +61,14 @@ android_ravenwood_test { "platform-test-annotations", ], srcs: [ + "src/com/android/internal/graphics/ColorUtilsTest.java", "src/com/android/internal/util/ParcellingTests.java", ], auto_gen_config: true, } + +java_test_helper_library { + name: "ApplicationSharedMemoryTestRule", + srcs: ["src/com/android/internal/os/ApplicationSharedMemoryTestRule.java"], + static_libs: ["junit"], +} diff --git a/tests/Internal/AndroidTest.xml b/tests/Internal/AndroidTest.xml index 7b67e9ebcced..2d6c650eb2dc 100644 --- a/tests/Internal/AndroidTest.xml +++ b/tests/Internal/AndroidTest.xml @@ -26,4 +26,12 @@ <option name="package" value="com.android.internal.tests" /> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> </test> + + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="pull-pattern-keys" value="perfetto_file_path"/> + <option name="directory-keys" + value="/data/user/0/com.android.internal.tests/files"/> + <option name="collect-on-run-ended-only" value="true"/> + <option name="clean-up" value="true"/> + </metrics_collector> </configuration>
\ No newline at end of file diff --git a/tests/Internal/ApplicationSharedMemoryTest32/AndroidManifest.xml b/tests/Internal/ApplicationSharedMemoryTest32/AndroidManifest.xml new file mode 100644 index 000000000000..4e1058ead734 --- /dev/null +++ b/tests/Internal/ApplicationSharedMemoryTest32/AndroidManifest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2017 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.internal.tests"> + <application + android:use32bitAbi="true" + android:multiArch="true"> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.internal.tests" + android:label="Internal Tests"/> +</manifest> diff --git a/tests/Internal/ApplicationSharedMemoryTest32/AndroidTest.xml b/tests/Internal/ApplicationSharedMemoryTest32/AndroidTest.xml new file mode 100644 index 000000000000..9bde8b7522e3 --- /dev/null +++ b/tests/Internal/ApplicationSharedMemoryTest32/AndroidTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2017 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 tests for internal classes/utilities."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="ApplicationSharedMemoryTest32.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="InternalTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.internal.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + </test> + + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="pull-pattern-keys" value="perfetto_file_path"/> + <option name="directory-keys" + value="/data/user/0/com.android.internal.tests/files"/> + <option name="collect-on-run-ended-only" value="true"/> + <option name="clean-up" value="true"/> + </metrics_collector> +</configuration>
\ No newline at end of file diff --git a/tests/Internal/ApplicationSharedMemoryTest32/OWNERS b/tests/Internal/ApplicationSharedMemoryTest32/OWNERS new file mode 100644 index 000000000000..1ff3fac8ae6f --- /dev/null +++ b/tests/Internal/ApplicationSharedMemoryTest32/OWNERS @@ -0,0 +1 @@ +include platform/frameworks/base:/PERFORMANCE_OWNERS
\ No newline at end of file diff --git a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java index d0bb8e3745bc..38a22f2fc2f3 100644 --- a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java +++ b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java @@ -19,14 +19,19 @@ package com.android.internal.graphics; import static org.junit.Assert.assertTrue; import android.graphics.Color; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; +import org.junit.Rule; import org.junit.Test; @SmallTest public class ColorUtilsTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + @Test public void calculateMinimumBackgroundAlpha_satisfiestContrast() { diff --git a/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java new file mode 100644 index 000000000000..d03ad5cb2877 --- /dev/null +++ b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.os; + +import java.io.IOException; + +import static org.junit.Assert.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeTrue; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.junit.Before; + +import java.io.FileDescriptor; + +/** Tests for {@link TimeoutRecord}. */ +@SmallTest +@Presubmit +@RunWith(JUnit4.class) +public class ApplicationSharedMemoryTest { + + @Before + public void setUp() { + // Skip tests if the feature under test is disabled. + assumeTrue(Flags.applicationSharedMemoryEnabled()); + } + + /** + * Every application process, including ours, should have had an instance installed at this + * point. + */ + @Test + public void hasInstance() { + // This shouldn't throw and shouldn't return null. + assertNotNull(ApplicationSharedMemory.getInstance()); + } + + /** Any app process should be able to read shared memory values. */ + @Test + public void canRead() { + ApplicationSharedMemory instance = ApplicationSharedMemory.getInstance(); + try { + instance.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(); + // Don't actually care about the value of the above. + } catch (java.time.DateTimeException e) { + // This exception is okay during testing. It means there was no time source, which + // could be because of network problems or a feature being flagged off. + } + } + + /** Application processes should not have mutable access. */ + @Test + public void appInstanceNotMutable() { + ApplicationSharedMemory instance = ApplicationSharedMemory.getInstance(); + try { + instance.setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(17); + fail("Attempted mutation in an app process should throw"); + } catch (Exception expected) { + } + } + + /** Instances share memory if they share the underlying memory region. */ + @Test + public void instancesShareMemory() throws IOException { + ApplicationSharedMemory instance1 = ApplicationSharedMemory.create(); + ApplicationSharedMemory instance2 = + ApplicationSharedMemory.fromFileDescriptor( + instance1.getFileDescriptor(), /* mutable= */ true); + ApplicationSharedMemory instance3 = + ApplicationSharedMemory.fromFileDescriptor( + instance2.getReadOnlyFileDescriptor(), /* mutable= */ false); + + instance1.setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(17); + assertEquals( + 17, instance1.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis()); + assertEquals( + 17, instance2.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis()); + assertEquals( + 17, instance3.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis()); + + instance2.setLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis(24); + assertEquals( + 24, instance1.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis()); + assertEquals( + 24, instance2.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis()); + assertEquals( + 24, instance3.getLatestNetworkTimeUnixEpochMillisAtZeroElapsedRealtimeMillis()); + } + + /** Can't map read-only memory as mutable. */ + @Test + public void readOnlyCantBeMutable() throws IOException { + ApplicationSharedMemory readWriteInstance = ApplicationSharedMemory.create(); + FileDescriptor readOnlyFileDescriptor = readWriteInstance.getReadOnlyFileDescriptor(); + + try { + ApplicationSharedMemory.fromFileDescriptor(readOnlyFileDescriptor, /* mutable= */ true); + fail("Shouldn't be able to map read-only memory as mutable"); + } catch (Exception expected) { + } + } +} diff --git a/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTestRule.java b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTestRule.java new file mode 100644 index 000000000000..ff2a4611cdf0 --- /dev/null +++ b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTestRule.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.os; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import com.android.internal.os.ApplicationSharedMemory; + +/** Test rule that sets up and tears down ApplicationSharedMemory for test. */ +public class ApplicationSharedMemoryTestRule implements TestRule { + + private ApplicationSharedMemory mSavedInstance; + + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + setup(); + try { + base.evaluate(); // Run the test + } finally { + teardown(); + } + } + }; + } + + private void setup() { + mSavedInstance = ApplicationSharedMemory.sInstance; + ApplicationSharedMemory.sInstance = ApplicationSharedMemory.create(); + } + + private void teardown() { + ApplicationSharedMemory.sInstance.close(); + ApplicationSharedMemory.sInstance = mSavedInstance; + mSavedInstance = null; + } +} diff --git a/tests/Internal/src/com/android/internal/util/ParcellingTests.java b/tests/Internal/src/com/android/internal/util/ParcellingTests.java index 65a3436a4c5e..fb63422cdf9f 100644 --- a/tests/Internal/src/com/android/internal/util/ParcellingTests.java +++ b/tests/Internal/src/com/android/internal/util/ParcellingTests.java @@ -18,6 +18,7 @@ package com.android.internal.util; import android.os.Parcel; import android.platform.test.annotations.Presubmit; +import android.platform.test.ravenwood.RavenwoodRule; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -26,6 +27,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.util.Parcelling.BuiltIn.ForInstant; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -38,6 +40,9 @@ import java.time.Instant; @RunWith(JUnit4.class) public class ParcellingTests { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + private Parcel mParcel = Parcel.obtain(); @Test diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java index c0e90f9232d6..8d143b69d124 100644 --- a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java +++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java @@ -727,7 +727,17 @@ public class CrashRecoveryTest { when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW, ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL)); when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager); - + try { + when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> { + final PackageInfo res = new PackageInfo(); + res.packageName = inv.getArgument(0); + res.setApexPackageName(res.packageName); + res.setLongVersionCode(VERSION_CODE); + return res; + }); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } watchdog.registerHealthObserver(rollbackObserver); return rollbackObserver; } @@ -787,8 +797,10 @@ public class CrashRecoveryTest { // Verify controller by default is started when packages are ready assertThat(controller.mIsEnabled).isTrue(); - verify(mConnectivityModuleConnector).registerHealthListener( - mConnectivityModuleCallbackCaptor.capture()); + if (!Flags.refactorCrashrecovery()) { + verify(mConnectivityModuleConnector).registerHealthListener( + mConnectivityModuleCallbackCaptor.capture()); + } } mAllocatedWatchdogs.add(watchdog); return watchdog; diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index a8b383cd4274..0364781ab064 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -23,9 +23,12 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -33,6 +36,7 @@ import static org.mockito.Mockito.when; import android.Manifest; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; @@ -42,6 +46,9 @@ import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener; import android.os.Handler; import android.os.SystemProperties; import android.os.test.TestLooper; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.util.AtomicFile; @@ -107,6 +114,9 @@ public class PackageWatchdogTest { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule + public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private final TestClock mTestClock = new TestClock(); private TestLooper mTestLooper; private Context mSpyContext; @@ -116,6 +126,7 @@ public class PackageWatchdogTest { private ConnectivityModuleConnector mConnectivityModuleConnector; @Mock private PackageManager mMockPackageManager; + @Mock Intent mMockIntent; // Mock only sysprop apis private PackageWatchdog.BootThreshold mSpyBootThreshold; @Captor @@ -961,6 +972,7 @@ public class PackageWatchdogTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_CRASHRECOVERY) public void testNetworkStackFailure() { mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); final PackageWatchdog wd = createWatchdog(); @@ -981,6 +993,7 @@ public class PackageWatchdogTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_CRASHRECOVERY) public void testNetworkStackFailureRecoverabilityDetection() { final PackageWatchdog wd = createWatchdog(); @@ -1669,6 +1682,19 @@ public class PackageWatchdogTest { PackageWatchdog.DEFAULT_TRIGGER_FAILURE_DURATION_MS); } + /** + * Tests device config changes are propagated correctly. + */ + @Test + public void testRegisterShutdownBroadcastReceiver() { + PackageWatchdog watchdog = createWatchdog(); + doReturn(mMockIntent).when(mSpyContext) + .registerReceiverForAllUsers(any(), any(), any(), any()); + + watchdog.registerShutdownBroadcastReceiver(); + verify(mSpyContext).registerReceiverForAllUsers(any(), any(), eq(null), eq(null)); + } + private void adoptShellPermissions(String... permissions) { InstrumentationRegistry .getInstrumentation() @@ -1740,8 +1766,10 @@ public class PackageWatchdogTest { // Verify controller by default is started when packages are ready assertThat(controller.mIsEnabled).isTrue(); - verify(mConnectivityModuleConnector).registerHealthListener( - mConnectivityModuleCallbackCaptor.capture()); + if (!Flags.refactorCrashrecovery()) { + verify(mConnectivityModuleConnector).registerHealthListener( + mConnectivityModuleCallbackCaptor.capture()); + } } mAllocatedWatchdogs.add(watchdog); return watchdog; @@ -1849,7 +1877,7 @@ public class PackageWatchdogTest { return true; } - public String getName() { + public String getUniqueIdentifier() { return mName; } diff --git a/tests/RollbackTest/lib/src/com/android/tests/rollback/host/WatchdogEventLogger.java b/tests/RollbackTest/lib/src/com/android/tests/rollback/host/WatchdogEventLogger.java index 8c16079dca85..01f8bc148fce 100644 --- a/tests/RollbackTest/lib/src/com/android/tests/rollback/host/WatchdogEventLogger.java +++ b/tests/RollbackTest/lib/src/com/android/tests/rollback/host/WatchdogEventLogger.java @@ -16,33 +16,26 @@ package com.android.tests.rollback.host; +import static com.google.common.truth.Truth.assertThat; + import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.log.LogUtil.CLog; + import com.google.common.truth.FailureMetadata; import com.google.common.truth.Truth; -import static com.google.common.truth.Truth.assertThat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class WatchdogEventLogger { - private static final String[] ROLLBACK_EVENT_TYPES = { - "ROLLBACK_INITIATE", "ROLLBACK_BOOT_TRIGGERED", "ROLLBACK_SUCCESS"}; - private static final String[] ROLLBACK_EVENT_ATTRS = { - "logPackage", "rollbackReason", "failedPackageName"}; - private static final String PROP_PREFIX = "persist.sys.rollbacktest."; private ITestDevice mDevice; - private void resetProperties(boolean enabled) throws Exception { + private void updateTestSysProp(boolean enabled) throws Exception { try { mDevice.enableAdbRoot(); assertThat(mDevice.setProperty( - PROP_PREFIX + "enabled", String.valueOf(enabled))).isTrue(); - for (String type : ROLLBACK_EVENT_TYPES) { - String key = PROP_PREFIX + type; - assertThat(mDevice.setProperty(key, "")).isTrue(); - for (String attr : ROLLBACK_EVENT_ATTRS) { - assertThat(mDevice.setProperty(key + "." + attr, "")).isTrue(); - } - } + "persist.sys.rollbacktest.enabled", String.valueOf(enabled))).isTrue(); } finally { mDevice.disableAdbRoot(); } @@ -50,19 +43,17 @@ public class WatchdogEventLogger { public void start(ITestDevice device) throws Exception { mDevice = device; - resetProperties(true); + updateTestSysProp(true); } public void stop() throws Exception { if (mDevice != null) { - resetProperties(false); + updateTestSysProp(false); } } - private boolean matchProperty(String type, String attr, String expectedVal) throws Exception { - String key = PROP_PREFIX + type + "." + attr; - String val = mDevice.getProperty(key); - return expectedVal == null || expectedVal.equals(val); + private boolean verifyEventContainsVal(String watchdogEvent, String expectedVal) { + return expectedVal == null || watchdogEvent.contains(expectedVal); } /** @@ -72,11 +63,33 @@ public class WatchdogEventLogger { * occurred, and return {@code true} if an event exists which matches all criteria. */ public boolean watchdogEventOccurred(String type, String logPackage, - String rollbackReason, String failedPackageName) throws Exception { - return mDevice.getBooleanProperty(PROP_PREFIX + type, false) - && matchProperty(type, "logPackage", logPackage) - && matchProperty(type, "rollbackReason", rollbackReason) - && matchProperty(type, "failedPackageName", failedPackageName); + String rollbackReason, String failedPackageName) { + String watchdogEvent = getEventForRollbackType(type); + return (watchdogEvent != null) + && verifyEventContainsVal(watchdogEvent, logPackage) + && verifyEventContainsVal(watchdogEvent, rollbackReason) + && verifyEventContainsVal(watchdogEvent, failedPackageName); + } + + /** Returns last matched event for rollbackType **/ + private String getEventForRollbackType(String rollbackType) { + String lastMatchedEvent = null; + try { + String rollbackDump = mDevice.executeShellCommand("dumpsys rollback"); + String eventRegex = ".*%s%s(.*)\\n"; + String eventPrefix = "Watchdog event occurred with type: "; + + final Pattern pattern = Pattern.compile( + String.format(eventRegex, eventPrefix, rollbackType)); + final Matcher matcher = pattern.matcher(rollbackDump); + while (matcher.find()) { + lastMatchedEvent = matcher.group(1); + } + CLog.d("Found watchdogEvent: " + lastMatchedEvent + " for type: " + rollbackType); + } catch (Exception e) { + CLog.e("Unable to find event for type: " + rollbackType, e); + } + return lastMatchedEvent; } static class Subject extends com.google.common.truth.Subject { @@ -97,7 +110,7 @@ public class WatchdogEventLogger { } void eventOccurred(String type, String logPackage, String rollbackReason, - String failedPackageName) throws Exception { + String failedPackageName) { check("watchdogEventOccurred(type=%s, logPackage=%s, rollbackReason=%s, " + "failedPackageName=%s)", type, logPackage, rollbackReason, failedPackageName) .that(mActual.watchdogEventOccurred(type, logPackage, rollbackReason, diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java index 359eb35384c7..5012c235a2a5 100644 --- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java +++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostSyncTest.java @@ -84,6 +84,7 @@ public class SurfaceControlViewHostSyncTest extends Activity implements SurfaceH content.addView(enableSyncButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM)); + content.setFitsSystemWindows(true); setContentView(content); mSv.setZOrderOnTop(false); diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java index 73e01634709e..4119ea2c73c3 100644 --- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java +++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceControlViewHostTest.java @@ -37,6 +37,7 @@ public class SurfaceControlViewHostTest extends Activity implements SurfaceHolde protected void onCreate(Bundle savedInstanceState) { FrameLayout content = new FrameLayout(this); + content.setFitsSystemWindows(true); super.onCreate(savedInstanceState); mView = new SurfaceView(this); content.addView(mView, new FrameLayout.LayoutParams( diff --git a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java index ac7dc9e2f31f..528706860b31 100644 --- a/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java +++ b/tests/SurfaceControlViewHostTest/src/com/android/test/viewembed/SurfaceInputTestActivity.java @@ -88,6 +88,7 @@ public class SurfaceInputTestActivity extends Activity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); LinearLayout content = new LinearLayout(this); + content.setFitsSystemWindows(true); mLocalSurfaceView = new SurfaceView(this); content.addView(mLocalSurfaceView, new LinearLayout.LayoutParams( 500, 500, Gravity.CENTER_HORIZONTAL | Gravity.TOP)); diff --git a/tests/TouchLatency/app/src/main/res/values/styles.xml b/tests/TouchLatency/app/src/main/res/values/styles.xml index 5058331187e8..fa352cf1e832 100644 --- a/tests/TouchLatency/app/src/main/res/values/styles.xml +++ b/tests/TouchLatency/app/src/main/res/values/styles.xml @@ -18,7 +18,7 @@ <!-- Base application theme. --> <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar"> <!-- Customize your theme here. --> - <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> + <item name="android:windowLayoutInDisplayCutoutMode">default</item> </style> </resources> diff --git a/tests/Tracing/Android.bp b/tests/Tracing/Android.bp index f5c1ae57e8c2..90998e67ae31 100644 --- a/tests/Tracing/Android.bp +++ b/tests/Tracing/Android.bp @@ -15,7 +15,7 @@ android_test { }, // Include some source files directly to be able to access package members srcs: ["src/**/*.java"], - libs: ["android.test.runner.impl"], + libs: ["android.test.runner.stubs.system"], static_libs: [ "junit", "androidx.test.rules", diff --git a/tests/Tracing/TEST_MAPPING b/tests/Tracing/TEST_MAPPING index 7f58fceee24d..f6e5221b721b 100644 --- a/tests/Tracing/TEST_MAPPING +++ b/tests/Tracing/TEST_MAPPING @@ -1,5 +1,5 @@ { - "postsubmit": [ + "presubmit": [ { "name": "TracingTests" } diff --git a/tests/Tracing/src/android/tracing/perfetto/DataSourceTest.java b/tests/Tracing/src/android/tracing/perfetto/DataSourceTest.java new file mode 100644 index 000000000000..bbeb18dfbecd --- /dev/null +++ b/tests/Tracing/src/android/tracing/perfetto/DataSourceTest.java @@ -0,0 +1,709 @@ +/* + * 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.tracing.perfetto; + +import static android.internal.perfetto.protos.TestEventOuterClass.TestEvent.PAYLOAD; +import static android.internal.perfetto.protos.TestEventOuterClass.TestEvent.TestPayload.SINGLE_INT; +import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.FOR_TESTING; + +import static java.io.File.createTempFile; +import static java.nio.file.Files.createTempDirectory; + +import android.internal.perfetto.protos.DataSourceConfigOuterClass.DataSourceConfig; +import android.internal.perfetto.protos.TestConfigOuterClass.TestConfig; +import android.tools.ScenarioBuilder; +import android.tools.Tag; +import android.tools.io.TraceType; +import android.tools.traces.TraceConfig; +import android.tools.traces.TraceConfigs; +import android.tools.traces.io.ResultReader; +import android.tools.traces.io.ResultWriter; +import android.tools.traces.monitors.PerfettoTraceMonitor; +import android.tools.traces.monitors.TraceMonitor; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.common.truth.Truth; +import com.google.protobuf.InvalidProtocolBufferException; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import perfetto.protos.PerfettoConfig; +import perfetto.protos.TracePacketOuterClass; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +@RunWith(AndroidJUnit4.class) +public class DataSourceTest { + private final File mTracingDirectory = createTempDirectory("temp").toFile(); + + private final ResultWriter mWriter = new ResultWriter() + .forScenario(new ScenarioBuilder() + .forClass(createTempFile("temp", "").getName()).build()) + .withOutputDir(mTracingDirectory) + .setRunComplete(); + + private final TraceConfigs mTraceConfig = new TraceConfigs( + new TraceConfig(false, true, false), + new TraceConfig(false, true, false), + new TraceConfig(false, true, false), + new TraceConfig(false, true, false) + ); + + private static TestDataSource sTestDataSource; + + private static TestDataSource.DataSourceInstanceProvider sInstanceProvider; + private static TestDataSource.TlsStateProvider sTlsStateProvider; + private static TestDataSource.IncrementalStateProvider sIncrementalStateProvider; + + public DataSourceTest() throws IOException {} + + @BeforeClass + public static void beforeAll() { + Producer.init(InitArguments.DEFAULTS); + setupProviders(); + sTestDataSource = new TestDataSource( + (ds, idx, configStream) -> sInstanceProvider.provide(ds, idx, configStream), + args -> sTlsStateProvider.provide(args), + args -> sIncrementalStateProvider.provide(args)); + sTestDataSource.register(DataSourceParams.DEFAULTS); + } + + private static void setupProviders() { + sInstanceProvider = (ds, idx, configStream) -> + new TestDataSource.TestDataSourceInstance(ds, idx); + sTlsStateProvider = args -> new TestDataSource.TestTlsState(); + sIncrementalStateProvider = args -> new TestDataSource.TestIncrementalState(); + } + + @Before + public void setup() { + setupProviders(); + } + + @Test + public void canTraceData() throws InvalidProtocolBufferException { + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + + sTestDataSource.trace((ctx) -> { + final ProtoOutputStream protoOutputStream = ctx.newTracePacket(); + long forTestingToken = protoOutputStream.start(FOR_TESTING); + long payloadToken = protoOutputStream.start(PAYLOAD); + protoOutputStream.write(SINGLE_INT, 10); + protoOutputStream.end(payloadToken); + protoOutputStream.end(forTestingToken); + }); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL); + assert rawProtoFromFile != null; + final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace + .parseFrom(rawProtoFromFile); + + Truth.assertThat(trace.getPacketCount()).isGreaterThan(0); + final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList() + .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList(); + final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream() + .filter(it -> it.getForTesting().getPayload().getSingleInt() == 10).toList(); + Truth.assertThat(matchingPackets).hasSize(1); + } + + @Test + public void canUseTlsStateForCustomState() { + final int expectedStateTestValue = 10; + final AtomicInteger actualStateTestValue = new AtomicInteger(); + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + state.testStateValue = expectedStateTestValue; + }); + + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + actualStateTestValue.set(state.testStateValue); + }); + } finally { + traceMonitor.stop(mWriter); + } + + Truth.assertThat(actualStateTestValue.get()).isEqualTo(expectedStateTestValue); + } + + @Test + public void eachInstanceHasOwnTlsState() { + final int[] expectedStateTestValues = new int[] { 1, 2 }; + final int[] actualStateTestValues = new int[] { 0, 0 }; + + final TraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + final TraceMonitor traceMonitor2 = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor1.start(); + try { + traceMonitor2.start(); + + AtomicInteger index = new AtomicInteger(0); + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + state.testStateValue = expectedStateTestValues[index.getAndIncrement()]; + }); + + index.set(0); + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + actualStateTestValues[index.getAndIncrement()] = state.testStateValue; + }); + } finally { + traceMonitor1.stop(mWriter); + } + } finally { + traceMonitor2.stop(mWriter); + } + + Truth.assertThat(actualStateTestValues[0]).isEqualTo(expectedStateTestValues[0]); + Truth.assertThat(actualStateTestValues[1]).isEqualTo(expectedStateTestValues[1]); + } + + @Test + public void eachThreadHasOwnTlsState() throws InterruptedException { + final int thread1ExpectedStateValue = 1; + final int thread2ExpectedStateValue = 2; + + final AtomicInteger thread1ActualStateValue = new AtomicInteger(); + final AtomicInteger thread2ActualStateValue = new AtomicInteger(); + + final CountDownLatch setUpLatch = new CountDownLatch(2); + final CountDownLatch setStateLatch = new CountDownLatch(2); + final CountDownLatch setOutStateLatch = new CountDownLatch(2); + + final RunnableCreator createTask = (stateValue, stateOut) -> () -> { + Producer.init(InitArguments.DEFAULTS); + + setUpLatch.countDown(); + + try { + setUpLatch.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + state.testStateValue = stateValue; + setStateLatch.countDown(); + }); + + try { + setStateLatch.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + sTestDataSource.trace((ctx) -> { + stateOut.set(ctx.getCustomTlsState().testStateValue); + setOutStateLatch.countDown(); + }); + }; + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + + new Thread( + createTask.create(thread1ExpectedStateValue, thread1ActualStateValue)).start(); + new Thread( + createTask.create(thread2ExpectedStateValue, thread2ActualStateValue)).start(); + + setOutStateLatch.await(3, TimeUnit.SECONDS); + + } finally { + traceMonitor.stop(mWriter); + } + + Truth.assertThat(thread1ActualStateValue.get()).isEqualTo(thread1ExpectedStateValue); + Truth.assertThat(thread2ActualStateValue.get()).isEqualTo(thread2ExpectedStateValue); + } + + @Test + public void incrementalStateIsReset() throws InterruptedException { + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()) + .setIncrementalTimeout(10) + .build(); + + final AtomicInteger testStateValue = new AtomicInteger(); + try { + traceMonitor.start(); + + sTestDataSource.trace(ctx -> ctx.getIncrementalState().testStateValue = 1); + + // Timeout to make sure the incremental state is cleared. + Thread.sleep(1000); + + sTestDataSource.trace(ctx -> + testStateValue.set(ctx.getIncrementalState().testStateValue)); + } finally { + traceMonitor.stop(mWriter); + } + + Truth.assertThat(testStateValue.get()).isNotEqualTo(1); + } + + @Test + public void getInstanceConfigOnCreateInstance() throws IOException { + final int expectedDummyIntValue = 10; + AtomicReference<ProtoInputStream> configStream = new AtomicReference<>(); + sInstanceProvider = (ds, idx, config) -> { + configStream.set(config); + return new TestDataSource.TestDataSourceInstance(ds, idx); + }; + + final TraceMonitor monitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name) + .setForTesting(PerfettoConfig.TestConfig.newBuilder().setDummyFields( + PerfettoConfig.TestConfig.DummyFields.newBuilder() + .setFieldInt32(expectedDummyIntValue) + .build()) + .build()) + .build()) + .build(); + + try { + monitor.start(); + } finally { + monitor.stop(mWriter); + } + + int configDummyIntValue = 0; + while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (configStream.get().getFieldNumber() + == (int) DataSourceConfig.FOR_TESTING) { + final long forTestingToken = configStream.get() + .start(DataSourceConfig.FOR_TESTING); + while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (configStream.get().getFieldNumber() + == (int) TestConfig.DUMMY_FIELDS) { + final long dummyFieldsToken = configStream.get() + .start(TestConfig.DUMMY_FIELDS); + while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (configStream.get().getFieldNumber() + == (int) TestConfig.DummyFields.FIELD_INT32) { + int val = configStream.get().readInt( + TestConfig.DummyFields.FIELD_INT32); + if (val != 0) { + configDummyIntValue = val; + break; + } + } + } + configStream.get().end(dummyFieldsToken); + break; + } + } + configStream.get().end(forTestingToken); + break; + } + } + + Truth.assertThat(configDummyIntValue).isEqualTo(expectedDummyIntValue); + } + + @Test + public void multipleTraceInstances() throws IOException, InterruptedException { + final int instanceCount = 3; + + final List<TraceMonitor> monitors = new ArrayList<>(); + final List<ResultWriter> writers = new ArrayList<>(); + + for (int i = 0; i < instanceCount; i++) { + final ResultWriter writer = new ResultWriter() + .forScenario(new ScenarioBuilder() + .forClass(createTempFile("temp", "").getName()).build()) + .withOutputDir(mTracingDirectory) + .setRunComplete(); + writers.add(writer); + } + + // Start at 1 because 0 is considered null value so payload will be ignored in that case + TestDataSource.TestTlsState.lastIndex = 1; + + final AtomicInteger traceCallCount = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(instanceCount); + + try { + // Start instances + for (int i = 0; i < instanceCount; i++) { + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + monitors.add(traceMonitor); + traceMonitor.start(); + } + + // Trace the stateIndex of the tracing instance. + sTestDataSource.trace(ctx -> { + final int testIntValue = ctx.getCustomTlsState().stateIndex; + traceCallCount.incrementAndGet(); + + final ProtoOutputStream os = ctx.newTracePacket(); + long forTestingToken = os.start(FOR_TESTING); + long payloadToken = os.start(PAYLOAD); + os.write(SINGLE_INT, testIntValue); + os.end(payloadToken); + os.end(forTestingToken); + + latch.countDown(); + }); + } finally { + // Stop instances + for (int i = 0; i < instanceCount; i++) { + final TraceMonitor monitor = monitors.get(i); + final ResultWriter writer = writers.get(i); + monitor.stop(writer); + } + } + + latch.await(3, TimeUnit.SECONDS); + Truth.assertThat(traceCallCount.get()).isEqualTo(instanceCount); + + for (int i = 0; i < instanceCount; i++) { + final int expectedTracedValue = i + 1; + final ResultWriter writer = writers.get(i); + final ResultReader reader = new ResultReader(writer.write(), mTraceConfig); + final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL); + assert rawProtoFromFile != null; + final perfetto.protos.TraceOuterClass.Trace trace = + perfetto.protos.TraceOuterClass.Trace.parseFrom(rawProtoFromFile); + + Truth.assertThat(trace.getPacketCount()).isGreaterThan(0); + final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList() + .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList(); + Truth.assertWithMessage("One packet has for testing data") + .that(tracePackets).hasSize(1); + + final List<TracePacketOuterClass.TracePacket> matchingPackets = + tracePackets.stream() + .filter(it -> it.getForTesting().getPayload() + .getSingleInt() == expectedTracedValue).toList(); + Truth.assertWithMessage( + "One packet has testing data with a payload with the expected value") + .that(matchingPackets).hasSize(1); + } + } + + @Test + public void onStartCallbackTriggered() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + + final AtomicBoolean callbackCalled = new AtomicBoolean(false); + sInstanceProvider = (ds, idx, config) -> new TestDataSource.TestDataSourceInstance( + ds, + idx, + (args) -> { + callbackCalled.set(true); + latch.countDown(); + }, + (args) -> {}, + (args) -> {} + ); + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + Truth.assertThat(callbackCalled.get()).isFalse(); + try { + traceMonitor.start(); + latch.await(3, TimeUnit.SECONDS); + Truth.assertThat(callbackCalled.get()).isTrue(); + } finally { + traceMonitor.stop(mWriter); + } + } + + @Test + public void onFlushCallbackTriggered() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean callbackCalled = new AtomicBoolean(false); + sInstanceProvider = (ds, idx, config) -> + new TestDataSource.TestDataSourceInstance( + ds, + idx, + (args) -> {}, + (args) -> { + callbackCalled.set(true); + latch.countDown(); + }, + (args) -> {} + ); + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + Truth.assertThat(callbackCalled.get()).isFalse(); + sTestDataSource.trace((ctx) -> { + final ProtoOutputStream protoOutputStream = ctx.newTracePacket(); + long forTestingToken = protoOutputStream.start(FOR_TESTING); + long payloadToken = protoOutputStream.start(PAYLOAD); + protoOutputStream.write(SINGLE_INT, 10); + protoOutputStream.end(payloadToken); + protoOutputStream.end(forTestingToken); + }); + } finally { + traceMonitor.stop(mWriter); + } + + latch.await(3, TimeUnit.SECONDS); + Truth.assertThat(callbackCalled.get()).isTrue(); + } + + @Test + public void onStopCallbackTriggered() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean callbackCalled = new AtomicBoolean(false); + sInstanceProvider = (ds, idx, config) -> + new TestDataSource.TestDataSourceInstance( + ds, + idx, + (args) -> {}, + (args) -> {}, + (args) -> { + callbackCalled.set(true); + latch.countDown(); + } + ); + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + Truth.assertThat(callbackCalled.get()).isFalse(); + } finally { + traceMonitor.stop(mWriter); + } + + latch.await(3, TimeUnit.SECONDS); + Truth.assertThat(callbackCalled.get()).isTrue(); + } + + @Test + public void canUseDataSourceInstanceToCreateTlsState() throws InvalidProtocolBufferException { + final Object testObject = new Object(); + + sInstanceProvider = (ds, idx, configStream) -> { + final TestDataSource.TestDataSourceInstance dsInstance = + new TestDataSource.TestDataSourceInstance(ds, idx); + dsInstance.testObject = testObject; + return dsInstance; + }; + + sTlsStateProvider = args -> { + final TestDataSource.TestTlsState tlsState = new TestDataSource.TestTlsState(); + + try (TestDataSource.TestDataSourceInstance dataSourceInstance = + args.getDataSourceInstanceLocked()) { + if (dataSourceInstance != null) { + tlsState.testStateValue = dataSourceInstance.testObject.hashCode(); + } + } + + return tlsState; + }; + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + sTestDataSource.trace((ctx) -> { + final ProtoOutputStream protoOutputStream = ctx.newTracePacket(); + long forTestingToken = protoOutputStream.start(FOR_TESTING); + long payloadToken = protoOutputStream.start(PAYLOAD); + protoOutputStream.write(SINGLE_INT, ctx.getCustomTlsState().testStateValue); + protoOutputStream.end(payloadToken); + protoOutputStream.end(forTestingToken); + }); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL); + assert rawProtoFromFile != null; + final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace + .parseFrom(rawProtoFromFile); + + Truth.assertThat(trace.getPacketCount()).isGreaterThan(0); + final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList() + .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList(); + final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream() + .filter(it -> it.getForTesting().getPayload().getSingleInt() + == testObject.hashCode()).toList(); + Truth.assertThat(matchingPackets).hasSize(1); + } + + @Test + public void canUseDataSourceInstanceToCreateIncrementalState() + throws InvalidProtocolBufferException { + final Object testObject = new Object(); + + sInstanceProvider = (ds, idx, configStream) -> { + final TestDataSource.TestDataSourceInstance dsInstance = + new TestDataSource.TestDataSourceInstance(ds, idx); + dsInstance.testObject = testObject; + return dsInstance; + }; + + sIncrementalStateProvider = args -> { + final TestDataSource.TestIncrementalState incrementalState = + new TestDataSource.TestIncrementalState(); + + try (TestDataSource.TestDataSourceInstance dataSourceInstance = + args.getDataSourceInstanceLocked()) { + if (dataSourceInstance != null) { + incrementalState.testStateValue = dataSourceInstance.testObject.hashCode(); + } + } + + return incrementalState; + }; + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + sTestDataSource.trace((ctx) -> { + final ProtoOutputStream protoOutputStream = ctx.newTracePacket(); + long forTestingToken = protoOutputStream.start(FOR_TESTING); + long payloadToken = protoOutputStream.start(PAYLOAD); + protoOutputStream.write(SINGLE_INT, ctx.getIncrementalState().testStateValue); + protoOutputStream.end(payloadToken); + protoOutputStream.end(forTestingToken); + }); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL); + assert rawProtoFromFile != null; + final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace + .parseFrom(rawProtoFromFile); + + Truth.assertThat(trace.getPacketCount()).isGreaterThan(0); + final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList() + .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList(); + final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream() + .filter(it -> it.getForTesting().getPayload().getSingleInt() + == testObject.hashCode()).toList(); + Truth.assertThat(matchingPackets).hasSize(1); + } + + @Test + public void canTraceOnFlush() throws InvalidProtocolBufferException, InterruptedException { + final int singleIntValue = 101; + sInstanceProvider = (ds, idx, config) -> + new TestDataSource.TestDataSourceInstance( + ds, + idx, + (args) -> {}, + (args) -> sTestDataSource.trace(ctx -> { + final ProtoOutputStream protoOutputStream = ctx.newTracePacket(); + long forTestingToken = protoOutputStream.start(FOR_TESTING); + long payloadToken = protoOutputStream.start(PAYLOAD); + protoOutputStream.write(SINGLE_INT, singleIntValue); + protoOutputStream.end(payloadToken); + protoOutputStream.end(forTestingToken); + }), + (args) -> {} + ); + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL); + assert rawProtoFromFile != null; + final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace + .parseFrom(rawProtoFromFile); + + Truth.assertThat(trace.getPacketCount()).isGreaterThan(0); + final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList() + .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList(); + final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream() + .filter(it -> it.getForTesting().getPayload().getSingleInt() + == singleIntValue).toList(); + Truth.assertThat(matchingPackets).hasSize(1); + } + + interface RunnableCreator { + Runnable create(int state, AtomicInteger stateOut); + } +} diff --git a/tests/Tracing/src/android/tracing/perfetto/TestDataSource.java b/tests/Tracing/src/android/tracing/perfetto/TestDataSource.java new file mode 100644 index 000000000000..d78f78b1cb0e --- /dev/null +++ b/tests/Tracing/src/android/tracing/perfetto/TestDataSource.java @@ -0,0 +1,122 @@ +/* + * 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.tracing.perfetto; + +import android.util.proto.ProtoInputStream; + +import java.util.UUID; +import java.util.function.Consumer; + +public class TestDataSource extends DataSource<TestDataSource.TestDataSourceInstance, + TestDataSource.TestTlsState, TestDataSource.TestIncrementalState> { + private final DataSourceInstanceProvider mDataSourceInstanceProvider; + private final TlsStateProvider mTlsStateProvider; + private final IncrementalStateProvider mIncrementalStateProvider; + + interface DataSourceInstanceProvider { + TestDataSourceInstance provide( + TestDataSource dataSource, int instanceIndex, ProtoInputStream configStream); + } + + interface TlsStateProvider { + TestTlsState provide(CreateTlsStateArgs<TestDataSourceInstance> args); + } + + interface IncrementalStateProvider { + TestIncrementalState provide(CreateIncrementalStateArgs<TestDataSourceInstance> args); + } + + public TestDataSource() { + this((ds, idx, config) -> new TestDataSourceInstance(ds, idx), + args -> new TestTlsState(), args -> new TestIncrementalState()); + } + + public TestDataSource( + DataSourceInstanceProvider dataSourceInstanceProvider, + TlsStateProvider tlsStateProvider, + IncrementalStateProvider incrementalStateProvider + ) { + super("android.tracing.perfetto.TestDataSource#" + UUID.randomUUID().toString()); + this.mDataSourceInstanceProvider = dataSourceInstanceProvider; + this.mTlsStateProvider = tlsStateProvider; + this.mIncrementalStateProvider = incrementalStateProvider; + } + + @Override + public TestDataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) { + return mDataSourceInstanceProvider.provide(this, instanceIndex, configStream); + } + + @Override + public TestTlsState createTlsState(CreateTlsStateArgs args) { + return mTlsStateProvider.provide(args); + } + + @Override + public TestIncrementalState createIncrementalState(CreateIncrementalStateArgs args) { + return mIncrementalStateProvider.provide(args); + } + + public static class TestTlsState { + public int testStateValue; + public int stateIndex = lastIndex++; + + public static int lastIndex = 0; + } + + public static class TestIncrementalState { + public int testStateValue; + } + + public static class TestDataSourceInstance extends DataSourceInstance { + public Object testObject; + Consumer<StartCallbackArguments> mStartCallback; + Consumer<FlushCallbackArguments> mFlushCallback; + Consumer<StopCallbackArguments> mStopCallback; + + public TestDataSourceInstance(DataSource dataSource, int instanceIndex) { + this(dataSource, instanceIndex, args -> {}, args -> {}, args -> {}); + } + + public TestDataSourceInstance( + DataSource dataSource, + int instanceIndex, + Consumer<StartCallbackArguments> startCallback, + Consumer<FlushCallbackArguments> flushCallback, + Consumer<StopCallbackArguments> stopCallback) { + super(dataSource, instanceIndex); + this.mStartCallback = startCallback; + this.mFlushCallback = flushCallback; + this.mStopCallback = stopCallback; + } + + @Override + public void onStart(StartCallbackArguments args) { + this.mStartCallback.accept(args); + } + + @Override + public void onFlush(FlushCallbackArguments args) { + this.mFlushCallback.accept(args); + } + + @Override + public void onStop(StopCallbackArguments args) { + this.mStopCallback.accept(args); + } + } +} diff --git a/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogImplTest.java index e9ff691151fe..8913e8c1996e 100644 --- a/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogImplTest.java @@ -59,7 +59,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.LinkedList; -import java.util.TreeMap; /** * Test class for {@link ProtoLogImpl}. @@ -90,7 +89,7 @@ public class LegacyProtoLogImplTest { //noinspection ResultOfMethodCallIgnored mFile.delete(); mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename, - 1024 * 1024, mReader, 1024, new TreeMap<>(), () -> {}); + 1024 * 1024, mReader, 1024, () -> {}); } @After @@ -142,7 +141,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{true, 10000, 30000, "test", 0.000003}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -159,7 +158,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{true, 10000, 0.0001, 0.00002, "test"}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -176,7 +175,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -191,7 +190,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -207,13 +206,20 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy, never()).passToLogcat(any(), any(), any()); verify(mReader, never()).getViewerString(anyLong()); } + @Test + public void loadViewerConfigOnLogcatGroupRegistration() { + TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); + mProtoLog.registerGroups(TestProtoLogGroup.TEST_GROUP); + verify(mReader).loadViewerConfig(any(), any()); + } + private static class ProtoLogData { Long mMessageHash = null; Long mElapsedTime = null; @@ -276,7 +282,7 @@ public class LegacyProtoLogImplTest { long before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, - 0b1110101001010100, null, + 0b1110101001010100, new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true}); long after = SystemClock.elapsedRealtimeNanos(); mProtoLog.stopProtoLog(mock(PrintWriter.class), true); @@ -301,7 +307,7 @@ public class LegacyProtoLogImplTest { long before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, - 0b01100100, null, + 0b01100100, new Object[]{"test", 1, 0.1, true}); long after = SystemClock.elapsedRealtimeNanos(); mProtoLog.stopProtoLog(mock(PrintWriter.class), true); @@ -325,7 +331,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); mProtoLog.startProtoLog(mock(PrintWriter.class)); mProtoLog.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, - 0b11, null, new Object[]{true}); + 0b11, new Object[]{true}); mProtoLog.stopProtoLog(mock(PrintWriter.class), true); try (InputStream is = new FileInputStream(mFile)) { ProtoInputStream ip = new ProtoInputStream(is); diff --git a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java index 1d7b6b348e10..2692e12c57ed 100644 --- a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java @@ -16,7 +16,7 @@ package com.android.internal.protolog; -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static android.tools.traces.Utils.busyWaitForDataSourceRegistration; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -29,9 +29,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static java.io.File.createTempFile; -import static java.nio.file.Files.createTempDirectory; -import android.content.Context; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import android.tools.ScenarioBuilder; @@ -42,10 +40,10 @@ import android.tools.traces.io.ResultWriter; import android.tools.traces.monitors.PerfettoTraceMonitor; import android.tools.traces.protolog.ProtoLogTrace; import android.tracing.perfetto.DataSource; -import android.util.proto.ProtoInputStream; -import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer; import com.android.internal.protolog.common.IProtoLogGroup; import com.android.internal.protolog.common.LogDataType; import com.android.internal.protolog.common.LogLevel; @@ -54,31 +52,32 @@ import com.google.common.truth.Truth; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; + +import perfetto.protos.Protolog; +import perfetto.protos.ProtologCommon; import java.io.File; import java.io.IOException; import java.util.List; import java.util.Random; -import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; -import perfetto.protos.Protolog; -import perfetto.protos.ProtologCommon; - /** * Test class for {@link ProtoLogImpl}. */ @SuppressWarnings("ConstantConditions") -@SmallTest @Presubmit @RunWith(JUnit4.class) -public class PerfettoProtoLogImplTest { - private final File mTracingDirectory = createTempDirectory("temp").toFile(); +public class ProcessedPerfettoProtoLogImplTest { + private static final String TEST_PROTOLOG_DATASOURCE_NAME = "test.android.protolog"; + private static final String MOCK_VIEWER_CONFIG_FILE = "my/mock/viewer/config/file.pb"; + private final File mTracingDirectory = InstrumentationRegistry.getInstrumentation() + .getTargetContext().getFilesDir(); private final ResultWriter mWriter = new ResultWriter() .forScenario(new ScenarioBuilder() @@ -93,25 +92,19 @@ public class PerfettoProtoLogImplTest { new TraceConfig(false, true, false) ); - private PerfettoProtoLogImpl mProtoLog; - private Protolog.ProtoLogViewerConfig.Builder mViewerConfigBuilder; - private File mFile; - private Runnable mCacheUpdater; + private static ProtoLogConfigurationService sProtoLogConfigurationService; + private static PerfettoProtoLogImpl sProtoLog; + private static Protolog.ProtoLogViewerConfig.Builder sViewerConfigBuilder; + private static Runnable sCacheUpdater; - private ProtoLogViewerConfigReader mReader; + private static ProtoLogViewerConfigReader sReader; - public PerfettoProtoLogImplTest() throws IOException { + public ProcessedPerfettoProtoLogImplTest() throws IOException { } - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - final Context testContext = getInstrumentation().getContext(); - mFile = testContext.getFileStreamPath("tracing_test.dat"); - //noinspection ResultOfMethodCallIgnored - mFile.delete(); - - mViewerConfigBuilder = Protolog.ProtoLogViewerConfig.newBuilder() + @BeforeClass + public static void setUp() throws Exception { + sViewerConfigBuilder = Protolog.ProtoLogViewerConfig.newBuilder() .addGroups( Protolog.ProtoLogViewerConfig.Group.newBuilder() .setId(1) @@ -123,65 +116,95 @@ public class PerfettoProtoLogImplTest { .setMessage("My Test Debug Log Message %b") .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG) .setGroupId(1) + .setLocation("com/test/MyTestClass.java:123") ).addMessages( Protolog.ProtoLogViewerConfig.MessageData.newBuilder() .setMessageId(2) .setMessage("My Test Verbose Log Message %b") .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_VERBOSE) .setGroupId(1) + .setLocation("com/test/MyTestClass.java:342") ).addMessages( Protolog.ProtoLogViewerConfig.MessageData.newBuilder() .setMessageId(3) .setMessage("My Test Warn Log Message %b") .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WARN) .setGroupId(1) + .setLocation("com/test/MyTestClass.java:563") ).addMessages( Protolog.ProtoLogViewerConfig.MessageData.newBuilder() .setMessageId(4) .setMessage("My Test Error Log Message %b") .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_ERROR) .setGroupId(1) + .setLocation("com/test/MyTestClass.java:156") ).addMessages( Protolog.ProtoLogViewerConfig.MessageData.newBuilder() .setMessageId(5) .setMessage("My Test WTF Log Message %b") .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WTF) .setGroupId(1) + .setLocation("com/test/MyTestClass.java:192") ); ViewerConfigInputStreamProvider viewerConfigInputStreamProvider = Mockito.mock( ViewerConfigInputStreamProvider.class); Mockito.when(viewerConfigInputStreamProvider.getInputStream()) - .thenAnswer(it -> new ProtoInputStream(mViewerConfigBuilder.build().toByteArray())); + .thenAnswer(it -> new AutoClosableProtoInputStream( + sViewerConfigBuilder.build().toByteArray())); + + sCacheUpdater = () -> {}; + sReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider)); + + final ProtoLogDataSourceBuilder dataSourceBuilder = + (onStart, onFlush, onStop) -> new ProtoLogDataSource( + onStart, onFlush, onStop, TEST_PROTOLOG_DATASOURCE_NAME); + final ViewerConfigFileTracer tracer = (dataSource, viewerConfigFilePath) -> { + Utils.dumpViewerConfig(dataSource, () -> { + if (!viewerConfigFilePath.equals(MOCK_VIEWER_CONFIG_FILE)) { + throw new RuntimeException( + "Unexpected viewer config file path provided"); + } + return new AutoClosableProtoInputStream(sViewerConfigBuilder.build().toByteArray()); + }); + }; + sProtoLogConfigurationService = + new ProtoLogConfigurationServiceImpl(dataSourceBuilder, tracer); + + sProtoLog = new ProcessedPerfettoProtoLogImpl( + MOCK_VIEWER_CONFIG_FILE, viewerConfigInputStreamProvider, sReader, + () -> sCacheUpdater.run(), TestProtoLogGroup.values(), dataSourceBuilder, + sProtoLogConfigurationService); + + busyWaitForDataSourceRegistration(TEST_PROTOLOG_DATASOURCE_NAME); + } + + @Before + public void before() { + Mockito.reset(sReader); - mCacheUpdater = () -> {}; - mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider)); - mProtoLog = new PerfettoProtoLogImpl( - viewerConfigInputStreamProvider, mReader, new TreeMap<>(), - () -> mCacheUpdater.run()); + TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false); + TestProtoLogGroup.TEST_GROUP.setLogToProto(false); } @After public void tearDown() { - if (mFile != null) { - //noinspection ResultOfMethodCallIgnored - mFile.delete(); - } ProtoLogImpl.setSingleInstance(null); } @Test public void isEnabled_returnsFalseByDefault() { - assertFalse(mProtoLog.isProtoEnabled()); + assertFalse(sProtoLog.isProtoEnabled()); } @Test public void isEnabled_returnsTrueAfterStart() { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog().build(); + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(TEST_PROTOLOG_DATASOURCE_NAME) + .build(); try { traceMonitor.start(); - assertTrue(mProtoLog.isProtoEnabled()); + assertTrue(sProtoLog.isProtoEnabled()); } finally { traceMonitor.stop(mWriter); } @@ -189,36 +212,38 @@ public class PerfettoProtoLogImplTest { @Test public void isEnabled_returnsFalseAfterStop() { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog().build(); + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(TEST_PROTOLOG_DATASOURCE_NAME) + .build(); try { traceMonitor.start(); - assertTrue(mProtoLog.isProtoEnabled()); + assertTrue(sProtoLog.isProtoEnabled()); } finally { traceMonitor.stop(mWriter); } - assertFalse(mProtoLog.isProtoEnabled()); + assertFalse(sProtoLog.isProtoEnabled()); } @Test public void defaultMode() throws IOException { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog(false).build(); + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(false, List.of(), TEST_PROTOLOG_DATASOURCE_NAME) + .build(); try { traceMonitor.start(); // Shouldn't be logging anything except WTF unless explicitly requested in the group // override. - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, null, new Object[]{true}); + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, + LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -232,23 +257,25 @@ public class PerfettoProtoLogImplTest { @Test public void respectsOverrideConfigs_defaultMode() throws IOException { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog(true, + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog( + true, List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( - TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, true))) - .build(); + TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, true)), + TEST_PROTOLOG_DATASOURCE_NAME + ).build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, null, new Object[]{true}); + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, + LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -267,22 +294,24 @@ public class PerfettoProtoLogImplTest { @Test public void respectsOverrideConfigs_allEnabledMode() throws IOException { PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog(true, + PerfettoTraceMonitor.newBuilder().enableProtoLog( + true, List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( - TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.WARN, false))) - .build(); + TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.WARN, false)), + TEST_PROTOLOG_DATASOURCE_NAME + ).build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, null, new Object[]{true}); + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, + LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -298,21 +327,21 @@ public class PerfettoProtoLogImplTest { @Test public void respectsAllEnabledMode() throws IOException { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog(true, List.of()) - .build(); + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(true, List.of(), TEST_PROTOLOG_DATASOURCE_NAME) + .build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, null, new Object[]{true}); - mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, null, new Object[]{true}); + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, + LogDataType.BOOLEAN, new Object[]{true}); + sProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, + LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -329,83 +358,66 @@ public class PerfettoProtoLogImplTest { } @Test - public void log_logcatEnabledExternalMessage() { - when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f"); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); + public void log_logcatEnabled() { + when(sReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f"); + PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{true, 10000, 30000, "test", 0.000003}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( LogLevel.INFO), eq("test true 10000 % 0x7530 test 3.0E-6")); - verify(mReader).getViewerString(eq(1234L)); + verify(sReader).getViewerString(eq(1234L)); } @Test public void log_logcatEnabledInvalidMessage() { - when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% %x %s %f"); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); + when(sReader.getViewerString(anyLong())).thenReturn("test %b %d %% %x %s %f"); + PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{true, 10000, 0.0001, 0.00002, "test"}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( LogLevel.INFO), - eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test")); - verify(mReader).getViewerString(eq(1234L)); + eq("FORMAT_ERROR \"test %b %d %% %x %s %f\", " + + "args=(true, 10000, 1.0E-4, 2.0E-5, test)")); + verify(sReader).getViewerString(eq(1234L)); } @Test - public void log_logcatEnabledInlineMessage() { - when(mReader.getViewerString(anyLong())).thenReturn("test %d"); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); + public void log_logcatEnabledNoMessageThrows() { + when(sReader.getViewerString(anyLong())).thenReturn(null); + PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); - implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", - new Object[]{5}); - - verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( - LogLevel.INFO), eq("test 5")); - verify(mReader, never()).getViewerString(anyLong()); - } - - @Test - public void log_logcatEnabledNoMessage() { - when(mReader.getViewerString(anyLong())).thenReturn(null); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); - TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); - TestProtoLogGroup.TEST_GROUP.setLogToProto(false); - - implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, - new Object[]{5}); - - verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( - LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5")); - verify(mReader).getViewerString(eq(1234L)); + var assertion = assertThrows(RuntimeException.class, () -> + implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + new Object[]{5})); + Truth.assertThat(assertion).hasMessageThat() + .contains("Failed to decode message for logcat"); } @Test public void log_logcatDisabled() { - when(mReader.getViewerString(anyLong())).thenReturn("test %d"); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); + when(sReader.getViewerString(anyLong())).thenReturn("test %d"); + PerfettoProtoLogImpl implSpy = Mockito.spy(sProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy, never()).passToLogcat(any(), any(), any()); - verify(mReader, never()).getViewerString(anyLong()); + verify(sReader, never()).getViewerString(anyLong()); } @Test @@ -414,18 +426,20 @@ public class PerfettoProtoLogImplTest { ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_INFO, "My test message :: %s, %d, %o, %x, %f, %e, %g, %b"); - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog().build(); + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(TEST_PROTOLOG_DATASOURCE_NAME) + .build(); long before; long after; try { + assertFalse(sProtoLog.isProtoEnabled()); traceMonitor.start(); - assertTrue(mProtoLog.isProtoEnabled()); + assertTrue(sProtoLog.isProtoEnabled()); before = SystemClock.elapsedRealtimeNanos(); - mProtoLog.log( + sProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, - 0b1110101001010100, null, + 0b1110101001010100, new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true}); after = SystemClock.elapsedRealtimeNanos(); } finally { @@ -441,12 +455,67 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos()) .isAtMost(after); Truth.assertThat(protolog.messages.getFirst().getMessage()) - .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, 5.000000e-01, 0.6, true"); + .isEqualTo( + "My test message :: test, 1, 2, 3, 0.400000, 5.000000e-01, 0.6, true"); + } + + @Test + public void log_noProcessing() throws IOException { + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(TEST_PROTOLOG_DATASOURCE_NAME) + .build(); + long before; + long after; + try { + traceMonitor.start(); + assertTrue(sProtoLog.isProtoEnabled()); + + before = SystemClock.elapsedRealtimeNanos(); + sProtoLog.log( + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, + "My test message :: %s, %d, %x, %f, %b", + "test", 1, 3, 0.4, true); + after = SystemClock.elapsedRealtimeNanos(); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protolog = reader.readProtoLogTrace(); + + Truth.assertThat(protolog.messages).hasSize(1); + Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos()) + .isAtLeast(before); + Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos()) + .isAtMost(after); + Truth.assertThat(protolog.messages.getFirst().getMessage()) + .isEqualTo("My test message :: test, 1, 3, 0.400000, true"); + } + + @Test + public void supportsLocationInformation() throws IOException { + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(true, List.of(), TEST_PROTOLOG_DATASOURCE_NAME) + .build(); + try { + traceMonitor.start(); + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + LogDataType.BOOLEAN, new Object[]{true}); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protolog = reader.readProtoLogTrace(); + + Truth.assertThat(protolog.messages).hasSize(1); + Truth.assertThat(protolog.messages.get(0).getLocation()) + .isEqualTo("com/test/MyTestClass.java:123"); } private long addMessageToConfig(ProtologCommon.ProtoLogLevel logLevel, String message) { final long messageId = new Random().nextLong(); - mViewerConfigBuilder.addMessages(Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + sViewerConfigBuilder.addMessages(Protolog.ProtoLogViewerConfig.MessageData.newBuilder() .setMessageId(messageId) .setMessage(message) .setLevel(logLevel) @@ -461,18 +530,15 @@ public class PerfettoProtoLogImplTest { final long messageHash = addMessageToConfig( ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_INFO, "My test message :: %s, %d, %f, %b"); - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog().build(); - long before; - long after; + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(TEST_PROTOLOG_DATASOURCE_NAME) + .build(); try { traceMonitor.start(); - before = SystemClock.elapsedRealtimeNanos(); - mProtoLog.log( + sProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, - 0b01100100, null, + 0b01100100, new Object[]{"test", 1, 0.1, true}); - after = SystemClock.elapsedRealtimeNanos(); } finally { traceMonitor.stop(mWriter); } @@ -483,12 +549,13 @@ public class PerfettoProtoLogImplTest { @Test public void log_protoDisabled() throws Exception { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog(false).build(); + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(false, List.of(), TEST_PROTOLOG_DATASOURCE_NAME) + .build(); try { traceMonitor.start(); - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - 0b11, null, new Object[]{true}); + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + 0b11, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -501,18 +568,20 @@ public class PerfettoProtoLogImplTest { @Test public void stackTraceTrimmed() throws IOException { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog(true, + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog( + true, List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, - true))) - .build(); + true)), + TEST_PROTOLOG_DATASOURCE_NAME + ).build(); try { traceMonitor.start(); - ProtoLogImpl.setSingleInstance(mProtoLog); + ProtoLogImpl.setSingleInstance(sProtoLog); ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1, - 0b11, null, true); + 0b11, true); } finally { traceMonitor.stop(mWriter); } @@ -527,27 +596,28 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(stacktrace).doesNotContain(DataSource.class.getSimpleName() + ".java"); Truth.assertThat(stacktrace) .doesNotContain(ProtoLogImpl.class.getSimpleName() + ".java"); - Truth.assertThat(stacktrace).contains(PerfettoProtoLogImplTest.class.getSimpleName()); + Truth.assertThat(stacktrace) + .contains(ProcessedPerfettoProtoLogImplTest.class.getSimpleName()); Truth.assertThat(stacktrace).contains("stackTraceTrimmed"); } @Test public void cacheIsUpdatedWhenTracesStartAndStop() { final AtomicInteger cacheUpdateCallCount = new AtomicInteger(0); - mCacheUpdater = cacheUpdateCallCount::incrementAndGet; + sCacheUpdater = cacheUpdateCallCount::incrementAndGet; - PerfettoTraceMonitor traceMonitor1 = - PerfettoTraceMonitor.newBuilder().enableProtoLog(true, - List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( - TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.WARN, - false))) - .build(); + PerfettoTraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(true, + List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( + TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.WARN, + false)), TEST_PROTOLOG_DATASOURCE_NAME + ).build(); PerfettoTraceMonitor traceMonitor2 = PerfettoTraceMonitor.newBuilder().enableProtoLog(true, List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, - false))) + false)), TEST_PROTOLOG_DATASOURCE_NAME) .build(); Truth.assertThat(cacheUpdateCallCount.get()).isEqualTo(0); @@ -576,95 +646,216 @@ public class PerfettoProtoLogImplTest { @Test public void isEnabledUpdatesBasedOnRunningTraces() { - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isTrue(); + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isFalse(); PerfettoTraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder().enableProtoLog(true, List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.WARN, - false))) + false)), TEST_PROTOLOG_DATASOURCE_NAME) .build(); PerfettoTraceMonitor traceMonitor2 = PerfettoTraceMonitor.newBuilder().enableProtoLog(true, List.of(new PerfettoTraceMonitor.Builder.ProtoLogGroupOverride( TestProtoLogGroup.TEST_GROUP.toString(), LogLevel.DEBUG, - false))) + false)), TEST_PROTOLOG_DATASOURCE_NAME) .build(); try { traceMonitor1.start(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) .isTrue(); try { traceMonitor2.start(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)).isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) .isTrue(); } finally { traceMonitor2.stop(mWriter); } - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isTrue(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) .isTrue(); } finally { traceMonitor1.stop(mWriter); } - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.DEBUG)) + .isFalse(); + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.VERBOSE)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.INFO)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WARN)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) + Truth.assertThat(sProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) - .isTrue(); + } + + @Test + public void supportsNullString() throws IOException { + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(true, List.of(), TEST_PROTOLOG_DATASOURCE_NAME) + .build(); + + try { + traceMonitor.start(); + + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, + "My test null string: %s", (Object) null); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protolog = reader.readProtoLogTrace(); + + Truth.assertThat(protolog.messages).hasSize(1); + Truth.assertThat(protolog.messages.get(0).getMessage()) + .isEqualTo("My test null string: null"); + } + + @Test + public void supportNullParams() throws IOException { + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(true, List.of(), TEST_PROTOLOG_DATASOURCE_NAME) + .build(); + + try { + traceMonitor.start(); + + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, + "My null args: %d, %f, %b", null, null, null); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protolog = reader.readProtoLogTrace(); + + Truth.assertThat(protolog.messages).hasSize(1); + Truth.assertThat(protolog.messages.get(0).getMessage()) + .isEqualTo("My null args: 0, 0.000000, false"); + } + + @Test + public void handlesConcurrentTracingSessions() throws IOException { + PerfettoTraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(true, List.of(), TEST_PROTOLOG_DATASOURCE_NAME) + .build(); + + PerfettoTraceMonitor traceMonitor2 = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(true, List.of(), TEST_PROTOLOG_DATASOURCE_NAME) + .build(); + + final ResultWriter writer2 = new ResultWriter() + .forScenario(new ScenarioBuilder() + .forClass(createTempFile("temp", "").getName()).build()) + .withOutputDir(mTracingDirectory) + .setRunComplete(); + + try { + traceMonitor1.start(); + traceMonitor2.start(); + + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, + LogDataType.BOOLEAN, new Object[]{true}); + } finally { + traceMonitor1.stop(mWriter); + traceMonitor2.stop(writer2); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protologFromMonitor1 = reader.readProtoLogTrace(); + + final ResultReader reader2 = new ResultReader(writer2.write(), mTraceConfig); + final ProtoLogTrace protologFromMonitor2 = reader2.readProtoLogTrace(); + + Truth.assertThat(protologFromMonitor1.messages).hasSize(1); + Truth.assertThat(protologFromMonitor1.messages.get(0).getMessage()) + .isEqualTo("My Test Debug Log Message true"); + + Truth.assertThat(protologFromMonitor2.messages).hasSize(1); + Truth.assertThat(protologFromMonitor2.messages.get(0).getMessage()) + .isEqualTo("My Test Debug Log Message true"); + } + + @Test + public void usesDefaultLogFromLevel() throws IOException { + PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableProtoLog(LogLevel.WARN, List.of(), TEST_PROTOLOG_DATASOURCE_NAME) + .build(); + try { + traceMonitor.start(); + sProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, + "This message should not be logged"); + sProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, + "This message should be logged %d", 123); + sProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, + "This message should also be logged %d", 567); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protolog = reader.readProtoLogTrace(); + + Truth.assertThat(protolog.messages).hasSize(2); + + Truth.assertThat(protolog.messages.get(0).getLevel()) + .isEqualTo(LogLevel.WARN); + Truth.assertThat(protolog.messages.get(0).getMessage()) + .isEqualTo("This message should be logged 123"); + + Truth.assertThat(protolog.messages.get(1).getLevel()) + .isEqualTo(LogLevel.ERROR); + Truth.assertThat(protolog.messages.get(1).getMessage()) + .isEqualTo("This message should also be logged 567"); } private enum TestProtoLogGroup implements IProtoLogGroup { diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java new file mode 100644 index 000000000000..be0c7daebb57 --- /dev/null +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.protolog; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.endsWith; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.times; + +import android.os.Binder; +import android.platform.test.annotations.Presubmit; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Test class for {@link ProtoLogImpl}. + */ +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class ProtoLogCommandHandlerTest { + + @Mock + ProtoLogConfigurationService mProtoLogConfigurationService; + @Mock + PrintWriter mPrintWriter; + @Mock + Binder mMockBinder; + + @Test + public void printsHelpForAllAvailableCommands() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.onHelp(); + validateOnHelpPrinted(); + } + + @Test + public void printsHelpIfCommandIsNull() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.onCommand(null); + validateOnHelpPrinted(); + } + + @Test + public void handlesGroupListCommand() { + Mockito.when(mProtoLogConfigurationService.getGroups()) + .thenReturn(new String[] {"MY_TEST_GROUP", "MY_OTHER_GROUP"}); + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups", "list" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("MY_TEST_GROUP")); + Mockito.verify(mPrintWriter, times(1)) + .println(contains("MY_OTHER_GROUP")); + } + + @Test + public void handlesIncompleteGroupsCommand() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("Incomplete command")); + } + + @Test + public void handlesGroupStatusCommand() { + Mockito.when(mProtoLogConfigurationService.getGroups()) + .thenReturn(new String[] {"MY_GROUP"}); + Mockito.when(mProtoLogConfigurationService.isLoggingToLogcat("MY_GROUP")).thenReturn(true); + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups", "status", "MY_GROUP" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("MY_GROUP")); + Mockito.verify(mPrintWriter, times(1)) + .println(contains("LOG_TO_LOGCAT = true")); + } + + @Test + public void handlesGroupStatusCommandOfUnregisteredGroups() { + Mockito.when(mProtoLogConfigurationService.getGroups()).thenReturn(new String[] {}); + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups", "status", "MY_GROUP" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("MY_GROUP")); + Mockito.verify(mPrintWriter, times(1)) + .println(contains("UNREGISTERED")); + } + + @Test + public void handlesGroupStatusCommandWithNoGroups() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "groups", "status" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("Incomplete command")); + } + + @Test + public void handlesIncompleteLogcatCommand() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat" }); + + Mockito.verify(mPrintWriter, times(1)) + .println(contains("Incomplete command")); + } + + @Test + public void handlesLogcatEnableCommand() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP" }); + Mockito.verify(mProtoLogConfigurationService).enableProtoLogToLogcat("MY_GROUP"); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, + new String[] { "logcat", "enable", "MY_GROUP", "MY_OTHER_GROUP" }); + Mockito.verify(mProtoLogConfigurationService) + .enableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); + } + + @Test + public void handlesLogcatDisableCommand() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP" }); + Mockito.verify(mProtoLogConfigurationService).disableProtoLogToLogcat("MY_GROUP"); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, + new String[] { "logcat", "disable", "MY_GROUP", "MY_OTHER_GROUP" }); + Mockito.verify(mProtoLogConfigurationService) + .disableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP"); + } + + @Test + public void handlesLogcatEnableCommandWithNoGroups() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "enable" }); + Mockito.verify(mPrintWriter).println(contains("Incomplete command")); + } + + @Test + public void handlesLogcatDisableCommandWithNoGroups() { + final ProtoLogCommandHandler cmdHandler = + new ProtoLogCommandHandler(mProtoLogConfigurationService, mPrintWriter); + + cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, + FileDescriptor.err, new String[] { "logcat", "disable" }); + Mockito.verify(mPrintWriter).println(contains("Incomplete command")); + } + + private void validateOnHelpPrinted() { + Mockito.verify(mPrintWriter, times(1)).println(endsWith("help")); + Mockito.verify(mPrintWriter, times(1)) + .println(endsWith("groups (list | status)")); + Mockito.verify(mPrintWriter, times(1)) + .println(endsWith("logcat (enable | disable) <group>")); + Mockito.verify(mPrintWriter, atLeast(0)).println(anyString()); + } +} diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java new file mode 100644 index 000000000000..a3d03a8278ed --- /dev/null +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.protolog; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; + +import static java.io.File.createTempFile; +import static java.nio.file.Files.createTempDirectory; + +import android.os.IBinder; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; +import android.tools.ScenarioBuilder; +import android.tools.Tag; +import android.tools.io.ResultArtifactDescriptor; +import android.tools.io.TraceType; +import android.tools.traces.TraceConfig; +import android.tools.traces.TraceConfigs; +import android.tools.traces.io.ResultReader; +import android.tools.traces.io.ResultWriter; +import android.tools.traces.monitors.PerfettoTraceMonitor; + +import com.google.common.truth.Truth; +import com.google.protobuf.InvalidProtocolBufferException; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import perfetto.protos.Protolog.ProtoLogViewerConfig; +import perfetto.protos.ProtologCommon; +import perfetto.protos.TraceOuterClass.Trace; +import perfetto.protos.TracePacketOuterClass.TracePacket; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; + +/** + * Test class for {@link ProtoLogImpl}. + */ +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class ProtoLogConfigurationServiceTest { + + private static final String TEST_GROUP = "MY_TEST_GROUP"; + private static final String OTHER_TEST_GROUP = "MY_OTHER_TEST_GROUP"; + + private static final ProtoLogViewerConfig VIEWER_CONFIG = + ProtoLogViewerConfig.newBuilder() + .addGroups( + ProtoLogViewerConfig.Group.newBuilder() + .setId(1) + .setName(TEST_GROUP) + .setTag(TEST_GROUP) + ).addMessages( + ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(1) + .setMessage("My Test Debug Log Message %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG) + .setGroupId(1) + ).addMessages( + ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(2) + .setMessage("My Test Verbose Log Message %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_VERBOSE) + .setGroupId(1) + ).build(); + + @Mock + IProtoLogClient mMockClient; + + @Mock + IProtoLogClient mSecondMockClient; + + @Mock + IBinder mMockClientBinder; + + @Mock + IBinder mSecondMockClientBinder; + + private final File mTracingDirectory = createTempDirectory("temp").toFile(); + + private final ResultWriter mWriter = new ResultWriter() + .forScenario(new ScenarioBuilder() + .forClass(createTempFile("temp", "").getName()).build()) + .withOutputDir(mTracingDirectory) + .setRunComplete(); + + private final TraceConfigs mTraceConfig = new TraceConfigs( + new TraceConfig(false, true, false), + new TraceConfig(false, true, false), + new TraceConfig(false, true, false), + new TraceConfig(false, true, false) + ); + + @Captor + ArgumentCaptor<IBinder.DeathRecipient> mDeathRecipientArgumentCaptor; + + @Captor + ArgumentCaptor<IBinder.DeathRecipient> mSecondDeathRecipientArgumentCaptor; + + private File mViewerConfigFile; + + public ProtoLogConfigurationServiceTest() throws IOException { + } + + @Before + public void setUp() { + Mockito.when(mMockClient.asBinder()).thenReturn(mMockClientBinder); + Mockito.when(mSecondMockClient.asBinder()).thenReturn(mSecondMockClientBinder); + + try { + mViewerConfigFile = File.createTempFile("viewer-config", ".pb"); + try (var fos = new FileOutputStream(mViewerConfigFile); + BufferedOutputStream bos = new BufferedOutputStream(fos)) { + + bos.write(VIEWER_CONFIG.toByteArray()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void canRegisterClientWithGroupsOnly() throws RemoteException { + final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); + + final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = + new ProtoLogConfigurationServiceImpl.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs + .GroupConfig(TEST_GROUP, true)); + service.registerClient(mMockClient, args); + + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); + Truth.assertThat(service.getGroups()).asList().containsExactly(TEST_GROUP); + } + + @Test + public void willDumpViewerConfigOnlyOnceOnTraceStop() + throws RemoteException, InvalidProtocolBufferException { + final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); + + final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = + new ProtoLogConfigurationServiceImpl.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs + .GroupConfig(TEST_GROUP, true)) + .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); + service.registerClient(mMockClient, args); + service.registerClient(mSecondMockClient, args); + + PerfettoTraceMonitor traceMonitor = + PerfettoTraceMonitor.newBuilder().enableProtoLog().build(); + + traceMonitor.start(); + traceMonitor.stop(mWriter); + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final byte[] traceData = reader.getArtifact() + .readBytes(new ResultArtifactDescriptor(TraceType.PERFETTO, Tag.ALL)); + + final Trace trace = Trace.parseFrom(traceData); + + final List<TracePacket> configPackets = trace.getPacketList().stream() + .filter(it -> it.hasProtologViewerConfig()) + // Exclude viewer configs from regular system tracing + .filter(it -> + it.getProtologViewerConfig().getGroups(0).getName().equals(TEST_GROUP)) + .toList(); + Truth.assertThat(configPackets).hasSize(1); + Truth.assertThat(configPackets.get(0).getProtologViewerConfig().toString()) + .isEqualTo(VIEWER_CONFIG.toString()); + } + + @Test + public void willDumpViewerConfigOnLastClientDisconnected() + throws RemoteException, FileNotFoundException { + final ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer tracer = + Mockito.mock(ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer.class); + final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(tracer); + + final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = + new ProtoLogConfigurationServiceImpl.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs + .GroupConfig(TEST_GROUP, true)) + .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); + service.registerClient(mMockClient, args); + service.registerClient(mSecondMockClient, args); + + Mockito.verify(mMockClientBinder) + .linkToDeath(mDeathRecipientArgumentCaptor.capture(), anyInt()); + Mockito.verify(mSecondMockClientBinder) + .linkToDeath(mSecondDeathRecipientArgumentCaptor.capture(), anyInt()); + + mDeathRecipientArgumentCaptor.getValue().binderDied(); + Mockito.verify(tracer, never()).trace(any(), any()); + mSecondDeathRecipientArgumentCaptor.getValue().binderDied(); + Mockito.verify(tracer).trace(any(), eq(mViewerConfigFile.getAbsolutePath())); + } + + @Test + public void sendEnableLoggingToLogcatToClient() throws RemoteException { + final var service = new ProtoLogConfigurationServiceImpl(); + + final var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs + .GroupConfig(TEST_GROUP, false)); + service.registerClient(mMockClient, args); + + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); + service.enableProtoLogToLogcat(TEST_GROUP); + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); + + Mockito.verify(mMockClient).toggleLogcat(eq(true), + Mockito.argThat(it -> it.length == 1 && it[0].equals(TEST_GROUP))); + } + + @Test + public void sendDisableLoggingToLogcatToClient() throws RemoteException { + final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); + + final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = + new ProtoLogConfigurationServiceImpl.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs + .GroupConfig(TEST_GROUP, true)); + service.registerClient(mMockClient, args); + + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); + service.disableProtoLogToLogcat(TEST_GROUP); + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); + + Mockito.verify(mMockClient).toggleLogcat(eq(false), + Mockito.argThat(it -> it.length == 1 && it[0].equals(TEST_GROUP))); + } + + @Test + public void doNotSendLoggingToLogcatToClientWithoutRegisteredGroup() throws RemoteException { + final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); + + final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = + new ProtoLogConfigurationServiceImpl.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs + .GroupConfig(TEST_GROUP, false)); + service.registerClient(mMockClient, args); + + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); + service.enableProtoLogToLogcat(OTHER_TEST_GROUP); + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); + + Mockito.verify(mMockClient, never()).toggleLogcat(anyBoolean(), any()); + } + + @Test + public void handlesToggleToLogcatBeforeClientIsRegistered() throws RemoteException { + final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); + + Truth.assertThat(service.getGroups()).asList().doesNotContain(TEST_GROUP); + service.enableProtoLogToLogcat(TEST_GROUP); + Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); + + final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = + new ProtoLogConfigurationServiceImpl.RegisterClientArgs() + .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs + .GroupConfig(TEST_GROUP, false)); + service.registerClient(mMockClient, args); + + Mockito.verify(mMockClient).toggleLogcat(eq(true), + Mockito.argThat(it -> it.length == 1 && it[0].equals(TEST_GROUP))); + } +} diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogImplTest.java index 60456f9ea10f..0496240f01e4 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogImplTest.java @@ -58,51 +58,50 @@ public class ProtoLogImplTest { public void d_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); + ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.DEBUG), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test public void v_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); + ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.VERBOSE), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test public void i_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); + ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.INFO), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test public void w_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234, - 4321, "test %d"); + ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.WARN), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test public void e_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); + ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test @@ -110,10 +109,10 @@ public class ProtoLogImplTest { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); ProtoLogImpl.wtf(TestProtoLogGroup.TEST_GROUP, - 1234, 4321, "test %d"); + 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.WTF), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } private enum TestProtoLogGroup implements IProtoLogGroup { diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java new file mode 100644 index 000000000000..3d1e208189b0 --- /dev/null +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.protolog; + +import static org.junit.Assert.assertThrows; + +import android.platform.test.annotations.Presubmit; + +import com.android.internal.protolog.common.IProtoLogGroup; + +import com.google.common.truth.Truth; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test class for {@link ProtoLog}. */ +@SuppressWarnings("ConstantConditions") +@Presubmit +@RunWith(JUnit4.class) +public class ProtoLogTest { + + @Test + public void canRunProtoLogInitMultipleTimes() { + ProtoLog.init(TEST_GROUP_1); + ProtoLog.init(TEST_GROUP_1); + ProtoLog.init(TEST_GROUP_2); + ProtoLog.init(TEST_GROUP_1, TEST_GROUP_2); + + final var instance = ProtoLog.getSingleInstance(); + Truth.assertThat(instance.getRegisteredGroups()) + .containsExactly(TEST_GROUP_1, TEST_GROUP_2); + } + + @Test + public void deduplicatesRegisteringDuplicateGroup() { + ProtoLog.init(TEST_GROUP_1, TEST_GROUP_1, TEST_GROUP_2); + + final var instance = ProtoLog.getSingleInstance(); + Truth.assertThat(instance.getRegisteredGroups()) + .containsExactly(TEST_GROUP_1, TEST_GROUP_2); + } + + @Test + public void throwOnRegisteringGroupsWithIdCollisions() { + final var assertion = assertThrows(RuntimeException.class, + () -> ProtoLog.init(TEST_GROUP_1, TEST_GROUP_WITH_COLLISION, TEST_GROUP_2)); + + Truth.assertThat(assertion).hasMessageThat() + .contains("" + TEST_GROUP_WITH_COLLISION.getId()); + Truth.assertThat(assertion).hasMessageThat().contains("collision"); + } + + private static final IProtoLogGroup TEST_GROUP_1 = new ProtoLogGroup("TEST_TAG_1", 1); + private static final IProtoLogGroup TEST_GROUP_2 = new ProtoLogGroup("TEST_TAG_2", 2); + private static final IProtoLogGroup TEST_GROUP_WITH_COLLISION = + new ProtoLogGroup("TEST_TAG_WITH_COLLISION", 1); + + private static class ProtoLogGroup implements IProtoLogGroup { + private final boolean mEnabled; + private volatile boolean mLogToProto; + private volatile boolean mLogToLogcat; + private final String mTag; + private final int mId; + + ProtoLogGroup(String tag, int id) { + this(true, true, false, tag, id); + } + + ProtoLogGroup( + boolean enabled, boolean logToProto, boolean logToLogcat, String tag, int id) { + this.mEnabled = enabled; + this.mLogToProto = logToProto; + this.mLogToLogcat = logToLogcat; + this.mTag = tag; + this.mId = id; + } + + @Override + public String name() { + return mTag; + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public boolean isLogToProto() { + return mLogToProto; + } + + @Override + public boolean isLogToLogcat() { + return mLogToLogcat; + } + + @Override + public boolean isLogToAny() { + return mLogToLogcat || mLogToProto; + } + + @Override + public String getTag() { + return mTag; + } + + @Override + public void setLogToProto(boolean logToProto) { + this.mLogToProto = logToProto; + } + + @Override + public void setLogToLogcat(boolean logToLogcat) { + this.mLogToLogcat = logToLogcat; + } + + @Override + public int getId() { + return mId; + } + } +} diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java new file mode 100644 index 000000000000..9e029a8d5e57 --- /dev/null +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java @@ -0,0 +1,162 @@ +/* + * 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.internal.protolog; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import android.os.Build; +import android.platform.test.annotations.Presubmit; + +import com.google.common.truth.Truth; + +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import perfetto.protos.ProtologCommon; + +import java.io.File; + +@Presubmit +@RunWith(JUnit4.class) +public class ProtoLogViewerConfigReaderTest { + private static final String TEST_GROUP_NAME = "MY_TEST_GROUP"; + private static final String TEST_GROUP_TAG = "TEST"; + + private static final String OTHER_TEST_GROUP_NAME = "MY_OTHER_TEST_GROUP"; + private static final String OTHER_TEST_GROUP_TAG = "OTHER_TEST"; + + private static final byte[] TEST_VIEWER_CONFIG = + perfetto.protos.Protolog.ProtoLogViewerConfig.newBuilder() + .addGroups( + perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder() + .setId(1) + .setName(TEST_GROUP_NAME) + .setTag(TEST_GROUP_TAG) + ).addGroups( + perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder() + .setId(2) + .setName(OTHER_TEST_GROUP_NAME) + .setTag(OTHER_TEST_GROUP_TAG) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(1) + .setMessage("My Test Log Message 1 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG) + .setGroupId(1) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(2) + .setMessage("My Test Log Message 2 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_VERBOSE) + .setGroupId(1) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(3) + .setMessage("My Test Log Message 3 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WARN) + .setGroupId(1) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(4) + .setMessage("My Test Log Message 4 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_ERROR) + .setGroupId(2) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(5) + .setMessage("My Test Log Message 5 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WTF) + .setGroupId(2) + ).build().toByteArray(); + + private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider = + () -> new AutoClosableProtoInputStream(TEST_VIEWER_CONFIG); + + private ProtoLogViewerConfigReader mConfig; + + @Before + public void before() { + mConfig = new ProtoLogViewerConfigReader(mViewerConfigInputStreamProvider); + } + + @Test + public void getViewerString_notLoaded() { + assertNull(mConfig.getViewerString(1)); + } + + @Test + public void loadViewerConfig() { + mConfig.loadViewerConfig(new String[] { TEST_GROUP_NAME }); + assertEquals("My Test Log Message 1 %b", mConfig.getViewerString(1)); + assertEquals("My Test Log Message 2 %b", mConfig.getViewerString(2)); + assertEquals("My Test Log Message 3 %b", mConfig.getViewerString(3)); + assertNull(mConfig.getViewerString(4)); + assertNull(mConfig.getViewerString(5)); + } + + @Test + public void unloadViewerConfig() { + mConfig.loadViewerConfig(new String[] { TEST_GROUP_NAME, OTHER_TEST_GROUP_NAME }); + mConfig.unloadViewerConfig(new String[] { TEST_GROUP_NAME }); + assertNull(mConfig.getViewerString(1)); + assertNull(mConfig.getViewerString(2)); + assertNull(mConfig.getViewerString(3)); + assertEquals("My Test Log Message 4 %b", mConfig.getViewerString(4)); + assertEquals("My Test Log Message 5 %b", mConfig.getViewerString(5)); + + mConfig.unloadViewerConfig(new String[] { OTHER_TEST_GROUP_NAME }); + assertNull(mConfig.getViewerString(4)); + assertNull(mConfig.getViewerString(5)); + } + + @Test + public void viewerConfigIsOnDevice() { + Assume.assumeFalse(Build.FINGERPRINT.contains("robolectric")); + + final String[] viewerConfigPaths; + if (android.tracing.Flags.perfettoProtologTracing()) { + viewerConfigPaths = new String[] { + "/system_ext/etc/wmshell.protolog.pb", + "/system/etc/core.protolog.pb", + }; + } else { + viewerConfigPaths = new String[] { + "/system_ext/etc/wmshell.protolog.json.gz", + "/system/etc/protolog.conf.json.gz", + }; + } + + for (final var viewerConfigPath : viewerConfigPaths) { + File f = new File(viewerConfigPath); + + Truth.assertWithMessage(f.getAbsolutePath() + " exists").that(f.exists()).isTrue(); + } + + } + + @Test + public void loadUnloadAndReloadViewerConfig() { + loadViewerConfig(); + unloadViewerConfig(); + loadViewerConfig(); + unloadViewerConfig(); + } +} diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java index be9fb1b309f6..ce519b7a1576 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java @@ -67,7 +67,8 @@ public class ProtologDataSourceTest { @Test public void allEnabledTraceMode() { - final ProtoLogDataSource ds = new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {}); + final ProtoLogDataSource ds = + new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {}); final ProtoLogDataSource.TlsState tlsState = createTlsState( DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig( @@ -154,7 +155,7 @@ public class ProtologDataSourceTest { private ProtoLogDataSource.TlsState createTlsState( DataSourceConfigOuterClass.DataSourceConfig config) { final ProtoLogDataSource ds = - Mockito.spy(new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {})); + Mockito.spy(new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {})); ProtoInputStream configStream = new ProtoInputStream(config.toByteArray()); final ProtoLogDataSource.Instance dsInstance = Mockito.spy( diff --git a/tests/UiBench/src/com/android/test/uibench/BitmapUploadActivity.java b/tests/UiBench/src/com/android/test/uibench/BitmapUploadActivity.java index 09236ffebdf4..459db8a0a1ac 100644 --- a/tests/UiBench/src/com/android/test/uibench/BitmapUploadActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/BitmapUploadActivity.java @@ -74,6 +74,9 @@ public class BitmapUploadActivity extends AppCompatActivity { } } + private ObjectAnimator mColorValueAnimator; + private ObjectAnimator mYAnimator; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -81,16 +84,28 @@ public class BitmapUploadActivity extends AppCompatActivity { // animate color to force bitmap uploads UploadView uploadView = findViewById(R.id.upload_view); - ObjectAnimator colorValueAnimator = ObjectAnimator.ofInt(uploadView, "colorValue", 0, 255); - colorValueAnimator.setRepeatMode(ValueAnimator.REVERSE); - colorValueAnimator.setRepeatCount(ValueAnimator.INFINITE); - colorValueAnimator.start(); + mColorValueAnimator = ObjectAnimator.ofInt(uploadView, "colorValue", 0, 255); + mColorValueAnimator.setRepeatMode(ValueAnimator.REVERSE); + mColorValueAnimator.setRepeatCount(ValueAnimator.INFINITE); + mColorValueAnimator.start(); // animate scene root to guarantee there's a minimum amount of GPU rendering work View uploadRoot = findViewById(R.id.upload_root); - ObjectAnimator yAnimator = ObjectAnimator.ofFloat(uploadRoot, "translationY", 0, 100); - yAnimator.setRepeatMode(ValueAnimator.REVERSE); - yAnimator.setRepeatCount(ValueAnimator.INFINITE); - yAnimator.start(); + mYAnimator = ObjectAnimator.ofFloat(uploadRoot, "translationY", 0, 100); + mYAnimator.setRepeatMode(ValueAnimator.REVERSE); + mYAnimator.setRepeatCount(ValueAnimator.INFINITE); + mYAnimator.start(); + } + + @Override + protected void onPause() { + super.onPause(); + if (mColorValueAnimator != null) { + mColorValueAnimator.cancel(); + } + + if (mYAnimator != null) { + mYAnimator.cancel(); + } } } diff --git a/tests/UiBench/src/com/android/test/uibench/FullscreenOverdrawActivity.java b/tests/UiBench/src/com/android/test/uibench/FullscreenOverdrawActivity.java index 882163bd6b0e..9d10f76198c3 100644 --- a/tests/UiBench/src/com/android/test/uibench/FullscreenOverdrawActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/FullscreenOverdrawActivity.java @@ -66,18 +66,29 @@ public class FullscreenOverdrawActivity extends AppCompatActivity { return PixelFormat.OPAQUE; } } + + private ObjectAnimator mObjectAnimator; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); OverdrawDrawable overdraw = new OverdrawDrawable(); getWindow().setBackgroundDrawable(overdraw); - setContentView(new View(this)); - ObjectAnimator objectAnimator = ObjectAnimator.ofInt(overdraw, "colorValue", 0, 255); - objectAnimator.setRepeatMode(ValueAnimator.REVERSE); - objectAnimator.setRepeatCount(ValueAnimator.INFINITE); - objectAnimator.start(); + mObjectAnimator = ObjectAnimator.ofInt(overdraw, "colorValue", 0, 255); + mObjectAnimator.setRepeatMode(ValueAnimator.REVERSE); + mObjectAnimator.setRepeatCount(ValueAnimator.INFINITE); + + mObjectAnimator.start(); + } + + @Override + protected void onPause() { + super.onPause(); + if (mObjectAnimator != null) { + mObjectAnimator.cancel(); + } } } diff --git a/tests/UiBench/src/com/android/test/uibench/GlTextureViewActivity.java b/tests/UiBench/src/com/android/test/uibench/GlTextureViewActivity.java index b26a660981da..1b28dc29d6aa 100644 --- a/tests/UiBench/src/com/android/test/uibench/GlTextureViewActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/GlTextureViewActivity.java @@ -33,6 +33,7 @@ import com.android.test.uibench.opengl.ImageFlipRenderThread; public class GlTextureViewActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener { private ImageFlipRenderThread mRenderThread; private TextureView mTextureView; + private ObjectAnimator mAnimator; @Override protected void onCreate(Bundle savedInstanceState) { @@ -54,17 +55,17 @@ public class GlTextureViewActivity extends AppCompatActivity implements TextureV int distance = Math.max(mTextureView.getWidth(), mTextureView.getHeight()); mTextureView.setCameraDistance(distance * metrics.density); - ObjectAnimator animator = ObjectAnimator.ofFloat(mTextureView, "rotationY", 0.0f, 360.0f); - animator.setRepeatMode(ObjectAnimator.REVERSE); - animator.setRepeatCount(ObjectAnimator.INFINITE); - animator.setDuration(4000); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + mAnimator = ObjectAnimator.ofFloat(mTextureView, "rotationY", 0.0f, 360.0f); + mAnimator.setRepeatMode(ObjectAnimator.REVERSE); + mAnimator.setRepeatCount(ObjectAnimator.INFINITE); + mAnimator.setDuration(4000); + mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mTextureView.invalidate(); } }); - animator.start(); + mAnimator.start(); } @Override @@ -86,4 +87,11 @@ public class GlTextureViewActivity extends AppCompatActivity implements TextureV public void onSurfaceTextureUpdated(SurfaceTexture surface) { } + @Override + protected void onPause() { + super.onPause(); + if (mAnimator != null) { + mAnimator.cancel(); + } + } }
\ No newline at end of file diff --git a/tests/UiBench/src/com/android/test/uibench/InvalidateActivity.java b/tests/UiBench/src/com/android/test/uibench/InvalidateActivity.java index 76ed1ae4e445..f1e96c80c85a 100644 --- a/tests/UiBench/src/com/android/test/uibench/InvalidateActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/InvalidateActivity.java @@ -51,6 +51,7 @@ public class InvalidateActivity extends AppCompatActivity { } private ColorView[][] mColorViews; + private ObjectAnimator mAnimator; @SuppressWarnings("unused") public void setColorValue(int colorValue) { @@ -80,9 +81,17 @@ public class InvalidateActivity extends AppCompatActivity { } } - ObjectAnimator animator = ObjectAnimator.ofInt(this, "colorValue", 0, 255); - animator.setRepeatMode(ValueAnimator.REVERSE); - animator.setRepeatCount(ValueAnimator.INFINITE); - animator.start(); + mAnimator = ObjectAnimator.ofInt(this, "colorValue", 0, 255); + mAnimator.setRepeatMode(ValueAnimator.REVERSE); + mAnimator.setRepeatCount(ValueAnimator.INFINITE); + mAnimator.start(); + } + + @Override + protected void onPause() { + super.onPause(); + if (mAnimator != null) { + mAnimator.cancel(); + } } } diff --git a/tests/UiBench/src/com/android/test/uibench/InvalidateTreeActivity.java b/tests/UiBench/src/com/android/test/uibench/InvalidateTreeActivity.java index 804ced14d522..95635720d4f9 100644 --- a/tests/UiBench/src/com/android/test/uibench/InvalidateTreeActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/InvalidateTreeActivity.java @@ -33,6 +33,7 @@ public class InvalidateTreeActivity extends AppCompatActivity { private final ArrayList<LinearLayout> mLayouts = new ArrayList<>(); private int mColorToggle = 0; + private ObjectAnimator mAnimator; private void createQuadTree(LinearLayout parent, int remainingDepth) { mLayouts.add(parent); @@ -71,9 +72,17 @@ public class InvalidateTreeActivity extends AppCompatActivity { createQuadTree(root, 8); setContentView(root); - ObjectAnimator animator = ObjectAnimator.ofInt(this, "ignoredValue", 0, 1000); - animator.setRepeatMode(ValueAnimator.REVERSE); - animator.setRepeatCount(ValueAnimator.INFINITE); - animator.start(); + mAnimator = ObjectAnimator.ofInt(this, "ignoredValue", 0, 1000); + mAnimator.setRepeatMode(ValueAnimator.REVERSE); + mAnimator.setRepeatCount(ValueAnimator.INFINITE); + mAnimator.start(); + } + + @Override + protected void onPause() { + super.onPause(); + if (mAnimator != null) { + mAnimator.cancel(); + } } } diff --git a/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java b/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java index 80d495df142c..cb26edcf336c 100644 --- a/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/ResizeHWLayerActivity.java @@ -30,6 +30,8 @@ import android.widget.FrameLayout; */ public class ResizeHWLayerActivity extends AppCompatActivity { + private ValueAnimator mAnimator; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -43,10 +45,10 @@ public class ResizeHWLayerActivity extends AppCompatActivity { PropertyValuesHolder pvhWidth = PropertyValuesHolder.ofInt("width", width, 1); PropertyValuesHolder pvhHeight = PropertyValuesHolder.ofInt("height", height, 1); final LayoutParams params = child.getLayoutParams(); - ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder(pvhWidth, pvhHeight); - animator.setRepeatMode(ValueAnimator.REVERSE); - animator.setRepeatCount(ValueAnimator.INFINITE); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + mAnimator = ValueAnimator.ofPropertyValuesHolder(pvhWidth, pvhHeight); + mAnimator.setRepeatMode(ValueAnimator.REVERSE); + mAnimator.setRepeatCount(ValueAnimator.INFINITE); + mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { params.width = (Integer)valueAnimator.getAnimatedValue("width"); @@ -54,7 +56,15 @@ public class ResizeHWLayerActivity extends AppCompatActivity { child.requestLayout(); } }); - animator.start(); + mAnimator.start(); setContentView(child); } + + @Override + protected void onPause() { + super.onPause(); + if (mAnimator != null) { + mAnimator.cancel(); + } + } } diff --git a/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java b/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java index 56845aeb6a2c..51d57f0a0de9 100644 --- a/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java +++ b/tests/UsbTests/src/com/android/server/usb/UsbServiceTest.java @@ -18,6 +18,10 @@ package com.android.server.usb; import static android.hardware.usb.UsbOperationInternal.USB_OPERATION_ERROR_INTERNAL; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -31,12 +35,15 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.hardware.usb.IUsbOperationInternal; import android.hardware.usb.flags.Flags; +import android.hardware.usb.UsbPort; import android.os.RemoteException; import android.os.UserManager; import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.runner.AndroidJUnit4; +import com.android.server.LocalServices; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -71,26 +78,38 @@ public class UsbServiceTest { private static final int TEST_SECOND_CALLER_ID = 2000; + private static final int TEST_INTERNAL_REQUESTER_REASON_1 = 100; + + private static final int TEST_INTERNAL_REQUESTER_REASON_2 = 200; + private UsbService mUsbService; + private UsbManagerInternal mUsbManagerInternal; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_USB_DATA_SIGNAL_STAKING_INTERNAL); + LocalServices.removeAllServicesForTest(); MockitoAnnotations.initMocks(this); - when(mUsbPortManager.enableUsbData(eq(TEST_PORT_ID), anyBoolean(), eq(TEST_TRANSACTION_ID), - eq(mCallback), any())).thenReturn(true); + when(mUsbPortManager.enableUsbData(eq(TEST_PORT_ID), anyBoolean(), + eq(TEST_TRANSACTION_ID), eq(mCallback), any())).thenReturn(true); mUsbService = new UsbService(mContext, mUsbPortManager, mUsbAlsaManager, mUserManager, mUsbSettingsManager); + mUsbManagerInternal = LocalServices.getService(UsbManagerInternal.class); + assertWithMessage("LocalServices.getService(UsbManagerInternal.class)") + .that(mUsbManagerInternal).isNotNull(); } - private void assertToggleUsbSuccessfully(int uid, boolean enable) { + private void assertToggleUsbSuccessfully(int requester, boolean enable, + boolean isInternalRequest) { assertTrue(mUsbService.enableUsbDataInternal(TEST_PORT_ID, enable, - TEST_TRANSACTION_ID, mCallback, uid)); + TEST_TRANSACTION_ID, mCallback, requester, isInternalRequest)); verify(mUsbPortManager).enableUsbData(TEST_PORT_ID, enable, TEST_TRANSACTION_ID, mCallback, null); @@ -100,9 +119,10 @@ public class UsbServiceTest { clearInvocations(mCallback); } - private void assertToggleUsbFailed(int uid, boolean enable) throws Exception { + private void assertToggleUsbFailed(int requester, boolean enable, + boolean isInternalRequest) throws Exception { assertFalse(mUsbService.enableUsbDataInternal(TEST_PORT_ID, enable, - TEST_TRANSACTION_ID, mCallback, uid)); + TEST_TRANSACTION_ID, mCallback, requester, isInternalRequest)); verifyZeroInteractions(mUsbPortManager); verify(mCallback).onOperationComplete(USB_OPERATION_ERROR_INTERNAL); @@ -116,15 +136,16 @@ public class UsbServiceTest { */ @Test public void disableUsb_successfullyDisable() { - assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, false); + assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, false, false); } /** - * Verify enableUsbData successfully enables USB port without error given no other stakers + * Verify enableUsbData successfully enables USB port without error given + * no other stakers */ @Test public void enableUsbWhenNoOtherStakers_successfullyEnable() { - assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, true); + assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, true, false); } /** @@ -132,47 +153,132 @@ public class UsbServiceTest { */ @Test public void enableUsbPortWithOtherStakers_failsToEnable() throws Exception { - assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, false); + assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, false, false); - assertToggleUsbFailed(TEST_SECOND_CALLER_ID, true); + assertToggleUsbFailed(TEST_SECOND_CALLER_ID, true, false); } /** - * Verify enableUsbData successfully enables USB port when the last staker is removed + * Verify enableUsbData successfully enables USB port when the last staker + * is removed */ @Test public void enableUsbByTheOnlyStaker_successfullyEnable() { - assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, false); + assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, false, false); - assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, true); + assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, true, false); } /** - * Verify enableUsbDataWhileDockedInternal does not enable USB port if other stakers are present + * Verify enableUsbDataWhileDockedInternal does not enable USB port if other + * stakers are present */ @Test public void enableUsbWhileDockedWhenThereAreOtherStakers_failsToEnable() throws RemoteException { - assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, false); + assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, false, false); mUsbService.enableUsbDataWhileDockedInternal(TEST_PORT_ID, TEST_TRANSACTION_ID, - mCallback, TEST_SECOND_CALLER_ID); + mCallback, TEST_SECOND_CALLER_ID, false); verifyZeroInteractions(mUsbPortManager); verify(mCallback).onOperationComplete(USB_OPERATION_ERROR_INTERNAL); } /** - * Verify enableUsbDataWhileDockedInternal does enable USB port if other stakers are - * not present + * Verify enableUsbDataWhileDockedInternal does enable USB port if other + * stakers are not present */ @Test public void enableUsbWhileDockedWhenThereAreNoStakers_SuccessfullyEnable() { mUsbService.enableUsbDataWhileDockedInternal(TEST_PORT_ID, TEST_TRANSACTION_ID, - mCallback, TEST_SECOND_CALLER_ID); + mCallback, TEST_SECOND_CALLER_ID, false); verify(mUsbPortManager).enableUsbDataWhileDocked(TEST_PORT_ID, TEST_TRANSACTION_ID, mCallback, null); verifyZeroInteractions(mCallback); } + + /** + * Verify enableUsbData successfully enables USB port without error given no + * other stakers for internal requests + */ + @Test + public void enableUsbWhenNoOtherStakers_forInternalRequest_successfullyEnable() { + assertToggleUsbSuccessfully(TEST_INTERNAL_REQUESTER_REASON_1, true, true); + } + + /** + * Verify enableUsbData does not enable USB port if other internal stakers + * are present for internal requests + */ + @Test + public void enableUsbPortWithOtherInternalStakers_forInternalRequest_failsToEnable() + throws Exception { + assertToggleUsbSuccessfully(TEST_INTERNAL_REQUESTER_REASON_1, false, true); + + assertToggleUsbFailed(TEST_INTERNAL_REQUESTER_REASON_2, true, true); + } + + /** + * Verify enableUsbData does not enable USB port if other external stakers + * are present for internal requests + */ + @Test + public void enableUsbPortWithOtherExternalStakers_forInternalRequest_failsToEnable() + throws Exception { + assertToggleUsbSuccessfully(TEST_FIRST_CALLER_ID, false, false); + + assertToggleUsbFailed(TEST_INTERNAL_REQUESTER_REASON_2, true, true); + } + + /** + * Verify enableUsbData does not enable USB port if other internal stakers + * are present for external requests + */ + @Test + public void enableUsbPortWithOtherInternalStakers_forExternalRequest_failsToEnable() + throws Exception { + assertToggleUsbSuccessfully(TEST_INTERNAL_REQUESTER_REASON_1, false, true); + + assertToggleUsbFailed(TEST_FIRST_CALLER_ID, true, false); + } + + /** + * Verify enableUsbData successfully enables USB port when the last staker + * is removed for internal requests + */ + @Test + public void enableUsbByTheOnlyStaker_forInternalRequest_successfullyEnable() { + assertToggleUsbSuccessfully(TEST_INTERNAL_REQUESTER_REASON_1, false, false); + + assertToggleUsbSuccessfully(TEST_INTERNAL_REQUESTER_REASON_1, true, false); + } + + /** + * Verify USB Manager internal calls mPortManager to get UsbPorts + */ + @Test + public void usbManagerInternal_getPorts_callsPortManager() { + when(mUsbPortManager.getPorts()).thenReturn(new UsbPort[] {}); + + UsbPort[] ports = mUsbManagerInternal.getPorts(); + + verify(mUsbPortManager).getPorts(); + assertEquals(ports.length, 0); + } + + @Test + public void usbManagerInternal_enableUsbData_successfullyEnable() { + boolean desiredEnableState = true; + + assertTrue(mUsbManagerInternal.enableUsbData(TEST_PORT_ID, desiredEnableState, + TEST_TRANSACTION_ID, mCallback, TEST_INTERNAL_REQUESTER_REASON_1)); + + verify(mUsbPortManager).enableUsbData(TEST_PORT_ID, + desiredEnableState, TEST_TRANSACTION_ID, mCallback, null); + verifyZeroInteractions(mCallback); + clearInvocations(mUsbPortManager); + clearInvocations(mCallback); + } } diff --git a/tests/broadcasts/OWNERS b/tests/broadcasts/OWNERS new file mode 100644 index 000000000000..d2e1f815e8dc --- /dev/null +++ b/tests/broadcasts/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 316181 +include platform/frameworks/base:/BROADCASTS_OWNERS diff --git a/tests/broadcasts/unit/Android.bp b/tests/broadcasts/unit/Android.bp new file mode 100644 index 000000000000..47166a713580 --- /dev/null +++ b/tests/broadcasts/unit/Android.bp @@ -0,0 +1,45 @@ +// +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + // 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"], + default_team: "trendy_team_framework_backstage_power", +} + +android_test { + name: "BroadcastUnitTests", + srcs: ["src/**/*.java"], + defaults: [ + "modules-utils-extended-mockito-rule-defaults", + ], + static_libs: [ + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "mockito-target-extended-minus-junit4", + "truth", + "flag-junit", + "android.app.flags-aconfig-java", + ], + certificate: "platform", + platform_apis: true, + test_suites: ["device-tests"], +} diff --git a/tests/broadcasts/unit/AndroidManifest.xml b/tests/broadcasts/unit/AndroidManifest.xml new file mode 100644 index 000000000000..e9c5248e4d98 --- /dev/null +++ b/tests/broadcasts/unit/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.broadcasts.unit" > + + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.broadcasts.unit" + android:label="Broadcasts Unit Tests"/> +</manifest>
\ No newline at end of file diff --git a/tests/broadcasts/unit/AndroidTest.xml b/tests/broadcasts/unit/AndroidTest.xml new file mode 100644 index 000000000000..b91e4783b69e --- /dev/null +++ b/tests/broadcasts/unit/AndroidTest.xml @@ -0,0 +1,29 @@ +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<configuration description="Runs Broadcasts tests"> + <option name="test-suite-tag" value="apct" /> + <option name="test-tag" value="BroadcastUnitTests" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="BroadcastUnitTests.apk" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.broadcasts.unit" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration>
\ No newline at end of file diff --git a/tests/broadcasts/unit/TEST_MAPPING b/tests/broadcasts/unit/TEST_MAPPING new file mode 100644 index 000000000000..8919fdcd7a3f --- /dev/null +++ b/tests/broadcasts/unit/TEST_MAPPING @@ -0,0 +1,15 @@ +{ + "presubmit": [ + { + "name": "BroadcastUnitTests", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + }, + { + "exclude-annotation": "org.junit.Ignore" + } + ] + } + ] +} diff --git a/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java new file mode 100644 index 000000000000..b7c412dea999 --- /dev/null +++ b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.app; + +import static android.content.Intent.ACTION_BATTERY_CHANGED; +import static android.content.Intent.ACTION_DEVICE_STORAGE_LOW; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; + +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.os.Bundle; +import android.os.SystemProperties; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.util.ArrayMap; + +import androidx.annotation.GuardedBy; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.modules.utils.testing.ExtendedMockitoRule; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +@EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE) +@RunWith(AndroidJUnit4.class) +@SmallTest +public class BroadcastStickyCacheTest { + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + + @Rule + public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) + .mockStatic(SystemProperties.class) + .build(); + + private static final String PROP_KEY_BATTERY_CHANGED = BroadcastStickyCache.getKey( + ACTION_BATTERY_CHANGED); + + private final TestSystemProps mTestSystemProps = new TestSystemProps(); + + @Before + public void setUp() { + doAnswer(invocation -> { + final String name = invocation.getArgument(0); + final long value = Long.parseLong(invocation.getArgument(1)); + mTestSystemProps.add(name, value); + return null; + }).when(() -> SystemProperties.set(anyString(), anyString())); + doAnswer(invocation -> { + final String name = invocation.getArgument(0); + final TestSystemProps.Handle testHandle = mTestSystemProps.query(name); + if (testHandle == null) { + return null; + } + final SystemProperties.Handle handle = Mockito.mock(SystemProperties.Handle.class); + doAnswer(handleInvocation -> testHandle.getLong(-1)).when(handle).getLong(anyLong()); + return handle; + }).when(() -> SystemProperties.find(anyString())); + } + + @After + public void tearDown() { + mTestSystemProps.clear(); + BroadcastStickyCache.clearForTest(); + } + + @Test + public void testUseCache_nullFilter() { + assertThat(BroadcastStickyCache.useCache(null)).isEqualTo(false); + } + + @Test + public void testUseCache_noActions() { + final IntentFilter filter = new IntentFilter(); + assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false); + } + + @Test + public void testUseCache_multipleActions() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_DEVICE_STORAGE_LOW); + filter.addAction(ACTION_BATTERY_CHANGED); + assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false); + } + + @Test + public void testUseCache_valueNotSet() { + final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED); + assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false); + } + + @Test + public void testUseCache() { + final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED); + final Intent intent = new Intent(ACTION_BATTERY_CHANGED) + .putExtra(BatteryManager.EXTRA_LEVEL, 90); + BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + BroadcastStickyCache.add(filter, intent); + assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(true); + } + + @Test + public void testUseCache_versionMismatch() { + final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED); + final Intent intent = new Intent(ACTION_BATTERY_CHANGED) + .putExtra(BatteryManager.EXTRA_LEVEL, 90); + BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + BroadcastStickyCache.add(filter, intent); + BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + + assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false); + } + + @Test + public void testAdd() { + final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED); + Intent intent = new Intent(ACTION_BATTERY_CHANGED) + .putExtra(BatteryManager.EXTRA_LEVEL, 90); + BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + BroadcastStickyCache.add(filter, intent); + assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(true); + Intent actualIntent = BroadcastStickyCache.getIntentUnchecked(filter); + assertThat(actualIntent).isNotNull(); + assertEquals(actualIntent, intent); + + intent = new Intent(ACTION_BATTERY_CHANGED) + .putExtra(BatteryManager.EXTRA_LEVEL, 99); + BroadcastStickyCache.add(filter, intent); + actualIntent = BroadcastStickyCache.getIntentUnchecked(filter); + assertThat(actualIntent).isNotNull(); + assertEquals(actualIntent, intent); + } + + @Test + public void testIncrementVersion_propExists() { + SystemProperties.set(PROP_KEY_BATTERY_CHANGED, String.valueOf(100)); + + BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(101); + BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(102); + } + + @Test + public void testIncrementVersion_propNotExists() { + // Verify that the property doesn't exist + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1); + + BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(1); + BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(2); + } + + @Test + public void testIncrementVersionIfExists_propExists() { + BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + + BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED); + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(2); + BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED); + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(3); + } + + @Test + public void testIncrementVersionIfExists_propNotExists() { + // Verify that the property doesn't exist + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1); + + BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED); + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1); + // Verify that property is not added as part of the querying. + BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED); + assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1); + } + + private void assertEquals(Intent actualIntent, Intent expectedIntent) { + assertThat(actualIntent.getAction()).isEqualTo(expectedIntent.getAction()); + assertEquals(actualIntent.getExtras(), expectedIntent.getExtras()); + } + + private void assertEquals(Bundle actualExtras, Bundle expectedExtras) { + assertWithMessage("Extras expected=%s, actual=%s", expectedExtras, actualExtras) + .that(actualExtras.kindofEquals(expectedExtras)).isTrue(); + } + + private static final class TestSystemProps { + @GuardedBy("mSysProps") + private final ArrayMap<String, Long> mSysProps = new ArrayMap<>(); + + public void add(String name, long value) { + synchronized (mSysProps) { + mSysProps.put(name, value); + } + } + + public long get(String name, long defaultValue) { + synchronized (mSysProps) { + final int idx = mSysProps.indexOfKey(name); + return idx >= 0 ? mSysProps.valueAt(idx) : defaultValue; + } + } + + public Handle query(String name) { + synchronized (mSysProps) { + return mSysProps.containsKey(name) ? new Handle(name) : null; + } + } + + public void clear() { + synchronized (mSysProps) { + mSysProps.clear(); + } + } + + public class Handle { + private final String mName; + + Handle(String name) { + mName = name; + } + + public long getLong(long defaultValue) { + return get(mName, defaultValue); + } + } + } +} diff --git a/tests/graphics/HwAccelerationTest/AndroidManifest.xml b/tests/graphics/HwAccelerationTest/AndroidManifest.xml index db3a992b9c7b..05b2f4c53b15 100644 --- a/tests/graphics/HwAccelerationTest/AndroidManifest.xml +++ b/tests/graphics/HwAccelerationTest/AndroidManifest.xml @@ -24,7 +24,7 @@ <uses-feature android:name="android.hardware.camera"/> <uses-feature android:name="android.hardware.camera.autofocus"/> - <uses-sdk android:minSdkVersion="21"/> + <uses-sdk android:minSdkVersion="21" /> <application android:label="HwUi" android:theme="@android:style/Theme.Material.Light"> @@ -409,6 +409,24 @@ </intent-filter> </activity> + <activity android:name="ScrollingZAboveSurfaceView" + android:label="SurfaceView/Z-Above scrolling" + 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="ScrollingZAboveScaledSurfaceView" + android:label="SurfaceView/Z-Above scrolling, scaled surface" + 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="StretchySurfaceViewActivity" android:label="SurfaceView/Stretchy Movement" android:exported="true"> diff --git a/tests/graphics/HwAccelerationTest/res/layout/scrolling_zabove_surfaceview.xml b/tests/graphics/HwAccelerationTest/res/layout/scrolling_zabove_surfaceview.xml new file mode 100644 index 000000000000..31e5774dd1ad --- /dev/null +++ b/tests/graphics/HwAccelerationTest/res/layout/scrolling_zabove_surfaceview.xml @@ -0,0 +1,131 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:context=".MainActivity"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Above the ScrollView" + android:textColor="#FFFFFFFF" + android:background="#FF444444" + android:padding="32dp" /> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Header" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <SurfaceView + android:layout_width="match_parent" + android:layout_height="500dp" + android:id="@+id/surfaceview" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + </LinearLayout> + + </ScrollView> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Below the ScrollView" + android:textColor="#FFFFFFFF" + android:background="#FF444444" + android:padding="32dp" /> + +</LinearLayout>
\ No newline at end of file diff --git a/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveScaledSurfaceView.kt b/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveScaledSurfaceView.kt new file mode 100644 index 000000000000..59ae885664db --- /dev/null +++ b/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveScaledSurfaceView.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.hwui + +import android.app.Activity +import android.graphics.Color +import android.graphics.Paint +import android.os.Bundle +import android.view.SurfaceHolder +import android.view.SurfaceView + +class ScrollingZAboveScaledSurfaceView : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.scrolling_zabove_surfaceview) + + findViewById<SurfaceView>(R.id.surfaceview).apply { + setZOrderOnTop(true) + holder.setFixedSize(1000, 2000) + holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(p0: SurfaceHolder) { + + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + holder.unlockCanvasAndPost(holder.lockCanvas().apply { + drawColor(Color.BLUE) + val paint = Paint() + paint.textSize = 16 * resources.displayMetrics.density + paint.textAlign = Paint.Align.CENTER + paint.color = Color.WHITE + drawText("I'm a setZOrderOnTop(true) SurfaceView!", + (width / 2).toFloat(), (height / 2).toFloat(), paint) + }) + } + + override fun surfaceDestroyed(p0: SurfaceHolder) { + + } + + }) + } + } +}
\ No newline at end of file diff --git a/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveSurfaceView.kt b/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveSurfaceView.kt new file mode 100644 index 000000000000..ccb71ec0ff2a --- /dev/null +++ b/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveSurfaceView.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.hwui + +import android.app.Activity +import android.graphics.Color +import android.graphics.Paint +import android.os.Bundle +import android.view.SurfaceHolder +import android.view.SurfaceView + +class ScrollingZAboveSurfaceView : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.scrolling_zabove_surfaceview) + + findViewById<SurfaceView>(R.id.surfaceview).apply { + setZOrderOnTop(true) + holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(p0: SurfaceHolder) { + + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + holder.unlockCanvasAndPost(holder.lockCanvas().apply { + drawColor(Color.BLUE) + val paint = Paint() + paint.textSize = 16 * resources.displayMetrics.density + paint.textAlign = Paint.Align.CENTER + paint.color = Color.WHITE + drawText("I'm a setZOrderOnTop(true) SurfaceView!", + (width / 2).toFloat(), (height / 2).toFloat(), paint) + }) + } + + override fun surfaceDestroyed(p0: SurfaceHolder) { + + } + + }) + } + } +}
\ No newline at end of file diff --git a/tests/graphics/SilkFX/res/layout/view_blur_behind.xml b/tests/graphics/SilkFX/res/layout/view_blur_behind.xml new file mode 100644 index 000000000000..83b1fa4b73cb --- /dev/null +++ b/tests/graphics/SilkFX/res/layout/view_blur_behind.xml @@ -0,0 +1,148 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="wowwowwowwowwowwowwowwowwowwowwowwowwowwowwow" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="I'm a little teapot" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="Something. Something." /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="/\\/\\/\\/\\/\\/\\/\\/\\/\\/" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="wowwowwowwowwowwowwowwowwowwowwowwowwowwowwow" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="I'm a little teapot" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="Something. Something." /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="/\\/\\/\\/\\/\\/\\/\\/\\/\\/" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:textSize="24dp" + android:text="^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^" /> + + </LinearLayout> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <View + android:layout_width="match_parent" + android:layout_height="300dp" /> + + <com.android.test.silkfx.materials.BlurBehindContainer + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="#33AAAAAA" + android:padding="32dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="48dp" + android:text="Blur!" /> + + </com.android.test.silkfx.materials.BlurBehindContainer> + + <View + android:layout_width="match_parent" + android:layout_height="1024dp" /> + + </LinearLayout> + + </ScrollView> + +</FrameLayout>
\ No newline at end of file diff --git a/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt b/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt index 59a6078376cf..6b6d3b8d3d12 100644 --- a/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt +++ b/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt @@ -61,7 +61,8 @@ private val AllDemos = listOf( )), DemoGroup("Materials", listOf( Demo("Glass", GlassActivity::class), - Demo("Background Blur", BackgroundBlurActivity::class) + Demo("Background Blur", BackgroundBlurActivity::class), + Demo("View blur behind", R.layout.view_blur_behind, commonControls = false) )) ) diff --git a/tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.kt b/tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.kt new file mode 100644 index 000000000000..ce6348e32969 --- /dev/null +++ b/tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.test.silkfx.materials + +import android.content.Context +import android.graphics.RenderEffect +import android.graphics.Shader +import android.util.AttributeSet +import android.widget.FrameLayout + +class BlurBehindContainer(context: Context, attributeSet: AttributeSet) : FrameLayout(context, attributeSet) { + override fun onFinishInflate() { + super.onFinishInflate() + setBackdropRenderEffect( + RenderEffect.createBlurEffect(16.0f, 16.0f, Shader.TileMode.CLAMP)) + } +}
\ No newline at end of file diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp index b68937f268e2..44aa4028c916 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp @@ -23,6 +23,7 @@ android_test { resource_dirs: ["res"], libs: ["android.test.runner.stubs"], static_libs: [ + "androidx.core_core", "androidx.test.ext.junit", "androidx.test.rules", "compatibility-device-util-axt", diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java index fff1dd1a7cb1..5f9a710c5f78 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java @@ -16,25 +16,40 @@ package com.android.server.inputmethod.multisessiontest; +import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; + import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.compatibility.common.util.concurrentuser.ConcurrentUserActivityUtils.getResponderUserId; import static com.android.compatibility.common.util.concurrentuser.ConcurrentUserActivityUtils.launchActivityAsUserSync; import static com.android.compatibility.common.util.concurrentuser.ConcurrentUserActivityUtils.sendBundleAndWaitForReply; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_DISPLAY_ID; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_EDITTEXT_CENTER; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_IME_SHOWN; import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_REQUEST_CODE; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_RESULT_CODE; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REPLY_IME_HIDDEN; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_DISPLAY_ID; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_EDITTEXT_POSITION; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_HIDE_IME; import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_IME_STATUS; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_SHOW_IME; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assume.assumeTrue; + +import android.app.UiAutomation; import android.content.ComponentName; +import android.content.Context; import android.os.Bundle; +import android.os.UserHandle; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; import androidx.test.core.app.ActivityScenario; import com.android.bedstead.harrier.BedsteadJUnit4; import com.android.bedstead.harrier.DeviceState; +import com.android.compatibility.common.util.SystemUtil; import org.junit.After; import org.junit.Before; @@ -44,8 +59,10 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; +import java.util.List; + @RunWith(BedsteadJUnit4.class) -@Ignore("b/345557347") public final class ConcurrentMultiUserTest { @ClassRule @@ -55,6 +72,10 @@ public final class ConcurrentMultiUserTest { private static final ComponentName TEST_ACTIVITY = new ComponentName( getInstrumentation().getTargetContext().getPackageName(), MainActivity.class.getName()); + private final Context mContext = getInstrumentation().getTargetContext(); + private final InputMethodManager mInputMethodManager = + mContext.getSystemService(InputMethodManager.class); + private final UiAutomation mUiAutomation = getInstrumentation().getUiAutomation(); private ActivityScenario<MainActivity> mActivityScenario; private MainActivity mActivity; @@ -69,17 +90,19 @@ public final class ConcurrentMultiUserTest { // Launch driver activity. mActivityScenario = ActivityScenario.launch(MainActivity.class); mActivityScenario.onActivity(activity -> mActivity = activity); + mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL); } @After public void tearDown() { + mUiAutomation.dropShellPermissionIdentity(); if (mActivityScenario != null) { mActivityScenario.close(); } } @Test - public void driverShowImeNotAffectPassenger() { + public void driverShowImeNotAffectPassenger() throws Exception { assertDriverImeHidden(); assertPassengerImeHidden(); @@ -87,6 +110,91 @@ public final class ConcurrentMultiUserTest { assertPassengerImeHidden(); } + @Test + @Ignore("b/352823913") + public void passengerShowImeNotAffectDriver() throws Exception { + assertDriverImeHidden(); + assertPassengerImeHidden(); + + showPassengerImeAndAssert(); + assertDriverImeHidden(); + } + + @Test + public void driverHideImeNotAffectPassenger() throws Exception { + showDriverImeAndAssert(); + showPassengerImeAndAssert(); + + hideDriverImeAndAssert(); + assertPassengerImeShown(); + } + + @Test + public void passengerHideImeNotAffectDriver() throws Exception { + showDriverImeAndAssert(); + showPassengerImeAndAssert(); + + hidePassengerImeAndAssert(); + assertDriverImeShown(); + } + + @Test + public void imeListNotEmpty() { + List<InputMethodInfo> driverImeList = mInputMethodManager.getInputMethodList(); + assertWithMessage("Driver IME list shouldn't be empty") + .that(driverImeList.isEmpty()).isFalse(); + + List<InputMethodInfo> passengerImeList = + mInputMethodManager.getInputMethodListAsUser(mPeerUserId); + assertWithMessage("Passenger IME list shouldn't be empty") + .that(passengerImeList.isEmpty()).isFalse(); + } + + @Test + public void enabledImeListNotEmpty() { + List<InputMethodInfo> driverEnabledImeList = + mInputMethodManager.getEnabledInputMethodList(); + assertWithMessage("Driver enabled IME list shouldn't be empty") + .that(driverEnabledImeList.isEmpty()).isFalse(); + + List<InputMethodInfo> passengerEnabledImeList = + mInputMethodManager.getEnabledInputMethodListAsUser(UserHandle.of(mPeerUserId)); + assertWithMessage("Passenger enabled IME list shouldn't be empty") + .that(passengerEnabledImeList.isEmpty()).isFalse(); + } + + @Test + public void currentImeNotNull() { + InputMethodInfo driverIme = mInputMethodManager.getCurrentInputMethodInfo(); + assertWithMessage("Driver IME shouldn't be null").that(driverIme).isNotNull(); + + InputMethodInfo passengerIme = + mInputMethodManager.getCurrentInputMethodInfoAsUser(UserHandle.of(mPeerUserId)); + assertWithMessage("Passenger IME shouldn't be null") + .that(passengerIme).isNotNull(); + } + + @Test + public void enableDisableImePerUser() throws IOException { + UserHandle driver = UserHandle.of(mContext.getUserId()); + UserHandle passenger = UserHandle.of(mPeerUserId); + enableDisableImeForUser(driver, passenger); + enableDisableImeForUser(passenger, driver); + } + + @Test + public void setImePerUser() throws IOException { + UserHandle driver = UserHandle.of(mContext.getUserId()); + UserHandle passenger = UserHandle.of(mPeerUserId); + setImeForUser(driver, passenger); + setImeForUser(passenger, driver); + } + + private void assertDriverImeShown() { + assertWithMessage("Driver IME should be shown") + .that(mActivity.isMyImeVisible()).isTrue(); + } + private void assertDriverImeHidden() { assertWithMessage("Driver IME should be hidden") .that(mActivity.isMyImeVisible()).isFalse(); @@ -98,10 +206,157 @@ public final class ConcurrentMultiUserTest { Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), mPeerUserId, bundleToSend); assertWithMessage("Passenger IME should be hidden") - .that(receivedBundle.getInt(KEY_RESULT_CODE)).isEqualTo(REPLY_IME_HIDDEN); + .that(receivedBundle.getBoolean(KEY_IME_SHOWN, /* defaultValue= */ true)).isFalse(); } - private void showDriverImeAndAssert() { + private void assertPassengerImeShown() { + final Bundle bundleToSend = new Bundle(); + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_IME_STATUS); + Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + assertWithMessage("Passenger IME should be shown") + .that(receivedBundle.getBoolean(KEY_IME_SHOWN)).isTrue(); + } + + private void showDriverImeAndAssert() throws Exception { + // WindowManagerInternal only allows the top focused display to show IME, so this method + // taps the driver display in case it is not the top focused display. + moveDriverDisplayToTop(); + mActivity.showMyImeAndWait(); } + + private void hideDriverImeAndAssert() { + mActivity.hideMyImeAndWait(); + } + + private void showPassengerImeAndAssert() throws Exception { + // WindowManagerInternal only allows the top focused display to show IME, so this method + // taps the passenger display in case it is not the top focused display. + movePassengerDisplayToTop(); + + Bundle bundleToSend = new Bundle(); + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_SHOW_IME); + Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + + assertWithMessage("Passenger IME should be shown") + .that(receivedBundle.getBoolean(KEY_IME_SHOWN)).isTrue(); + } + + private void hidePassengerImeAndAssert() { + Bundle bundleToSend = new Bundle(); + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_HIDE_IME); + Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + + assertWithMessage("Passenger IME should be hidden") + .that(receivedBundle.getBoolean(KEY_IME_SHOWN, /* defaultValue= */ true)).isFalse(); + } + + private void moveDriverDisplayToTop() throws Exception { + float[] driverEditTextCenter = mActivity.getEditTextCenter(); + SystemUtil.runShellCommand(mUiAutomation, String.format("input tap %f %f", + driverEditTextCenter[0], driverEditTextCenter[1])); + } + + private void movePassengerDisplayToTop() throws Exception { + final Bundle bundleToSend = new Bundle(); + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_EDITTEXT_POSITION); + Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + final float[] passengerEditTextCenter = receivedBundle.getFloatArray(KEY_EDITTEXT_CENTER); + + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_DISPLAY_ID); + receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + final int passengerDisplayId = receivedBundle.getInt(KEY_DISPLAY_ID); + SystemUtil.runShellCommand(mUiAutomation, String.format("input -d %d tap %f %f", + passengerDisplayId, passengerEditTextCenter[0], passengerEditTextCenter[1])); + } + + /** + * Disables/enables IME for {@code user1}, then verifies that the IME settings for {@code user1} + * has changed as expected and {@code user2} stays the same. + */ + private void enableDisableImeForUser(UserHandle user1, UserHandle user2) throws IOException { + List<InputMethodInfo> user1EnabledImeList = + mInputMethodManager.getEnabledInputMethodListAsUser(user1); + List<InputMethodInfo> user2EnabledImeList = + mInputMethodManager.getEnabledInputMethodListAsUser(user2); + + // Disable an IME for user1. + InputMethodInfo imeToDisable = user1EnabledImeList.get(0); + SystemUtil.runShellCommand(mUiAutomation, + "ime disable --user " + user1.getIdentifier() + " " + imeToDisable.getId()); + List<InputMethodInfo> user1EnabledImeList2 = + mInputMethodManager.getEnabledInputMethodListAsUser(user1); + List<InputMethodInfo> user2EnabledImeList2 = + mInputMethodManager.getEnabledInputMethodListAsUser(user2); + assertWithMessage("User " + user1.getIdentifier() + " IME " + imeToDisable.getId() + + " should be disabled") + .that(user1EnabledImeList2.contains(imeToDisable)).isFalse(); + assertWithMessage("Disabling user " + user1.getIdentifier() + + " IME shouldn't affect user " + user2.getIdentifier()) + .that(user2EnabledImeList2.containsAll(user2EnabledImeList) + && user2EnabledImeList.containsAll(user2EnabledImeList2)) + .isTrue(); + + // Enable the IME. + SystemUtil.runShellCommand(mUiAutomation, + "ime enable --user " + user1.getIdentifier() + " " + imeToDisable.getId()); + List<InputMethodInfo> user1EnabledImeList3 = + mInputMethodManager.getEnabledInputMethodListAsUser(user1); + List<InputMethodInfo> user2EnabledImeList3 = + mInputMethodManager.getEnabledInputMethodListAsUser(user2); + assertWithMessage("User " + user1.getIdentifier() + " IME " + imeToDisable.getId() + + " should be enabled").that(user1EnabledImeList3.contains(imeToDisable)).isTrue(); + assertWithMessage("Enabling user " + user1.getIdentifier() + + " IME shouldn't affect user " + user2.getIdentifier()) + .that(user2EnabledImeList2.containsAll(user2EnabledImeList3) + && user2EnabledImeList3.containsAll(user2EnabledImeList2)) + .isTrue(); + } + + /** + * Sets/resets IME for {@code user1}, then verifies that the IME settings for {@code user1} + * has changed as expected and {@code user2} stays the same. + */ + private void setImeForUser(UserHandle user1, UserHandle user2) throws IOException { + // Reset IME for user1. + SystemUtil.runShellCommand(mUiAutomation, + "ime reset --user " + user1.getIdentifier()); + + List<InputMethodInfo> user1EnabledImeList = + mInputMethodManager.getEnabledInputMethodListAsUser(user1); + assumeTrue("There must be at least two IME to test", user1EnabledImeList.size() >= 2); + InputMethodInfo user1Ime = mInputMethodManager.getCurrentInputMethodInfoAsUser(user1); + InputMethodInfo user2Ime = mInputMethodManager.getCurrentInputMethodInfoAsUser(user2); + + // Set to another IME for user1. + InputMethodInfo anotherIme = null; + for (InputMethodInfo info : user1EnabledImeList) { + if (!info.equals(user1Ime)) { + anotherIme = info; + } + } + SystemUtil.runShellCommand(mUiAutomation, + "ime set --user " + user1.getIdentifier() + " " + anotherIme.getId()); + InputMethodInfo user1Ime2 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user1); + InputMethodInfo user2Ime2 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user2); + assertWithMessage("The current IME for user " + user1.getIdentifier() + " is wrong") + .that(user1Ime2).isEqualTo(anotherIme); + assertWithMessage("The current IME for user " + user2.getIdentifier() + " shouldn't change") + .that(user2Ime2).isEqualTo(user2Ime); + + // Reset IME for user1. + SystemUtil.runShellCommand(mUiAutomation, + "ime reset --user " + user1.getIdentifier()); + InputMethodInfo user1Ime3 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user1); + InputMethodInfo user2Ime3 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user2); + assertWithMessage("The current IME for user " + user1.getIdentifier() + " is wrong") + .that(user1Ime3).isEqualTo(user1Ime); + assertWithMessage("The current IME for user " + user2.getIdentifier() + " shouldn't change") + .that(user2Ime3).isEqualTo(user2Ime); + } } diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java index f1260008ca59..fa0aa19a8822 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java @@ -16,20 +16,25 @@ package com.android.server.inputmethod.multisessiontest; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_DISPLAY_ID; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_EDITTEXT_CENTER; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_IME_SHOWN; import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_REQUEST_CODE; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_RESULT_CODE; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REPLY_IME_HIDDEN; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REPLY_IME_SHOWN; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_DISPLAY_ID; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_EDITTEXT_POSITION; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_HIDE_IME; import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_IME_STATUS; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_SHOW_IME; import android.app.Activity; import android.os.Bundle; import android.os.Process; import android.util.Log; -import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import androidx.annotation.WorkerThread; import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; import com.android.compatibility.common.util.PollingCheck; @@ -43,7 +48,6 @@ public final class MainActivity extends ConcurrentUserActivityBase { private static final long WAIT_IME_TIMEOUT_MS = 3000; private EditText mEditor; - private InputMethodManager mImm; @Override protected void onCreate(Bundle savedInstanceState) { @@ -52,19 +56,56 @@ public final class MainActivity extends ConcurrentUserActivityBase { + Process.myUserHandle().getIdentifier() + " on display " + getDisplay().getDisplayId()); setContentView(R.layout.main_activity); - mImm = getSystemService(InputMethodManager.class); mEditor = requireViewById(R.id.edit_text); } @Override + protected void onResume() { + super.onResume(); + Log.v(TAG, "onResume"); + } + + @Override + protected void onPause() { + super.onPause(); + Log.v(TAG, "onPause"); + } + + @Override + protected void onStop() { + super.onStop(); + Log.v(TAG, "onResume"); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + Log.v(TAG, "onWindowFocusChanged " + hasFocus); + } + + @Override + @WorkerThread protected Bundle onBundleReceived(Bundle receivedBundle) { final int requestCode = receivedBundle.getInt(KEY_REQUEST_CODE); Log.v(TAG, "onBundleReceived() with request code:" + requestCode); final Bundle replyBundle = new Bundle(); switch (requestCode) { case REQUEST_IME_STATUS: - replyBundle.putInt(KEY_RESULT_CODE, - isMyImeVisible() ? REPLY_IME_SHOWN : REPLY_IME_HIDDEN); + replyBundle.putBoolean(KEY_IME_SHOWN, isMyImeVisible()); + break; + case REQUEST_SHOW_IME: + showMyImeAndWait(); + replyBundle.putBoolean(KEY_IME_SHOWN, isMyImeVisible()); + break; + case REQUEST_HIDE_IME: + hideMyImeAndWait(); + replyBundle.putBoolean(KEY_IME_SHOWN, isMyImeVisible()); + break; + case REQUEST_EDITTEXT_POSITION: + replyBundle.putFloatArray(KEY_EDITTEXT_CENTER, getEditTextCenter()); + break; + case REQUEST_DISPLAY_ID: + replyBundle.putInt(KEY_DISPLAY_ID, getDisplay().getDisplayId()); break; default: throw new RuntimeException("Received undefined request code:" + requestCode); @@ -77,21 +118,41 @@ public final class MainActivity extends ConcurrentUserActivityBase { return insets == null ? false : insets.isVisible(WindowInsetsCompat.Type.ime()); } + float[] getEditTextCenter() { + final float editTextCenterX = mEditor.getX() + 0.5f * mEditor.getWidth(); + final float editTextCenterY = mEditor.getY() + 0.5f * mEditor.getHeight(); + return new float[]{editTextCenterX, editTextCenterY}; + } + + @WorkerThread void showMyImeAndWait() { - Log.v(TAG, "showSoftInput"); runOnUiThread(() -> { - // requestFocus() must run on UI thread. + // View#requestFocus() and WindowInsetsControllerCompat#show() must run on UI thread. if (!mEditor.requestFocus()) { Log.e(TAG, "Failed to focus on mEditor"); return; } - if (!mImm.showSoftInput(mEditor, /* flags= */ 0)) { - Log.e(TAG, String.format("Failed to show my IME as user %d, " - + "mEditor:focused=%b,hasWindowFocus=%b", getUserId(), - mEditor.isFocused(), mEditor.hasWindowFocus())); - } + // Compared to mImm.showSoftInput(), the call below is the recommended way to show the + // keyboard because it is guaranteed to be scheduled after the window is focused. + Log.v(TAG, "showSoftInput"); + WindowCompat.getInsetsController(getWindow(), mEditor).show( + WindowInsetsCompat.Type.ime()); }); PollingCheck.waitFor(WAIT_IME_TIMEOUT_MS, () -> isMyImeVisible(), - String.format("My IME (user %d) didn't show up", getUserId())); + String.format("%s: My IME (user %d) didn't show up", TAG, + Process.myUserHandle().getIdentifier())); + } + + @WorkerThread + void hideMyImeAndWait() { + runOnUiThread(() -> { + Log.v(TAG, "hideSoftInput"); + // WindowInsetsControllerCompat#hide() must run on UI thread. + WindowCompat.getInsetsController(getWindow(), mEditor) + .hide(WindowInsetsCompat.Type.ime()); + }); + PollingCheck.waitFor(WAIT_IME_TIMEOUT_MS, () -> !isMyImeVisible(), + String.format("%s: My IME (user %d) is still shown", TAG, + Process.myUserHandle().getIdentifier())); } } diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/TestRequestConstants.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/TestRequestConstants.java index 1501bfb69c92..68c9d5403c0b 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/TestRequestConstants.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/TestRequestConstants.java @@ -21,9 +21,13 @@ final class TestRequestConstants { } public static final String KEY_REQUEST_CODE = "key_request_code"; - public static final String KEY_RESULT_CODE = "key_result_code"; + public static final String KEY_EDITTEXT_CENTER = "key_edittext_center"; + public static final String KEY_DISPLAY_ID = "key_display_id"; + public static final String KEY_IME_SHOWN = "key_ime_shown"; public static final int REQUEST_IME_STATUS = 1; - public static final int REPLY_IME_SHOWN = 2; - public static final int REPLY_IME_HIDDEN = 3; + public static final int REQUEST_SHOW_IME = 2; + public static final int REQUEST_HIDE_IME = 3; + public static final int REQUEST_EDITTEXT_POSITION = 4; + public static final int REQUEST_DISPLAY_ID = 5; } diff --git a/tests/testables/Android.bp b/tests/testables/Android.bp index 7596ee722d01..17cc0b2a5884 100644 --- a/tests/testables/Android.bp +++ b/tests/testables/Android.bp @@ -25,11 +25,18 @@ package { java_library { name: "testables", - srcs: ["src/**/*.java"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], libs: [ "android.test.runner.stubs.system", "android.test.mock.stubs.system", "androidx.test.rules", "mockito-target-inline-minus-junit4", ], + static_libs: [ + "PlatformMotionTesting", + "kotlinx_coroutines_test", + ], } diff --git a/tests/testables/src/android/animation/AnimatorTestRule.java b/tests/testables/src/android/animation/AnimatorTestRule.java new file mode 100644 index 000000000000..3b39e1fc6bc7 --- /dev/null +++ b/tests/testables/src/android/animation/AnimatorTestRule.java @@ -0,0 +1,378 @@ +/* + * 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.animation; + +import android.animation.AnimationHandler.AnimationFrameCallback; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Looper; +import android.os.SystemClock; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunnableWithException; +import android.util.AndroidRuntimeException; +import android.util.Singleton; +import android.view.Choreographer; +import android.view.animation.AnimationUtils; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.util.Preconditions; + +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * JUnit {@link TestRule} that can be used to run {@link Animator}s without actually waiting for the + * duration of the animation. This also helps the test to be written in a deterministic manner. + * + * Create an instance of {@code AnimatorTestRule} and specify it as a {@link org.junit.Rule} + * of the test class. Use {@link #advanceTimeBy(long)} to advance animators that have been started. + * Note that {@link #advanceTimeBy(long)} should be called from the same thread you have used to + * start the animator. + * + * <pre> + * {@literal @}SmallTest + * {@literal @}RunWith(AndroidJUnit4.class) + * public class SampleAnimatorTest { + * + * {@literal @}Rule + * public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + * + * {@literal @}UiThreadTest + * {@literal @}Test + * public void sample() { + * final ValueAnimator animator = ValueAnimator.ofInt(0, 1000); + * animator.setDuration(1000L); + * assertThat(animator.getAnimatedValue(), is(0)); + * animator.start(); + * mAnimatorTestRule.advanceTimeBy(500L); + * assertThat(animator.getAnimatedValue(), is(500)); + * } + * } + * </pre> + */ +public final class AnimatorTestRule implements TestRule { + + private final Object mLock = new Object(); + private final Singleton<TestHandler> mTestHandler = new Singleton<>() { + @Override + protected TestHandler create() { + return new TestHandler(); + } + }; + private final Object mTest; + private final long mStartTime; + private long mTotalTimeDelta = 0; + private volatile boolean mCanLockAnimationClock; + private Looper mLooperWithLockedAnimationClock; + + /** + * Construct an AnimatorTestRule with access to the test instance and a custom start time. + * @see #AnimatorTestRule(Object) + */ + public AnimatorTestRule(Object test, long startTime) { + mTest = test; + mStartTime = startTime; + } + + /** + * Construct an AnimatorTestRule for the given test instance with a start time of + * {@link SystemClock#uptimeMillis()}. Initializing the start time with this clock reduces the + * discrepancies with various internals of classes like ValueAnimator which can sometimes read + * that clock via {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}. + * + * @param test the test instance used to access the {@link TestableLooper} used by the class. + */ + public AnimatorTestRule(Object test) { + this(test, SystemClock.uptimeMillis()); + } + + @NonNull + @Override + public Statement apply(@NonNull final Statement base, @NonNull Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + final TestHandler testHandler = mTestHandler.get(); + final AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler); + final RunnableWithException lockClock = + wrapWithRunBlocking(new LockAnimationClockRunnable()); + final RunnableWithException unlockClock = + wrapWithRunBlocking(new UnlockAnimationClockRunnable()); + try { + lockClock.run(); + base.evaluate(); + } finally { + unlockClock.run(); + AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart); + if (testHandler != objAtEnd) { + // pass or fail, inner logic not restoring the handler needs to be reported. + // noinspection ThrowFromFinallyBlock + throw new IllegalStateException("Test handler was altered: expected=" + + testHandler + " actual=" + objAtEnd); + } + } + } + }; + } + + private RunnableWithException wrapWithRunBlocking(RunnableWithException runnable) { + RunnableWithException wrapped = TestableLooper.wrapWithRunBlocking(mTest, runnable); + if (wrapped != null) { + return wrapped; + } + return () -> runOnMainThrowing(runnable); + } + + private static void runOnMainThrowing(RunnableWithException runnable) throws Exception { + if (Looper.myLooper() == Looper.getMainLooper()) { + runnable.run(); + } else { + final Throwable[] throwableBox = new Throwable[1]; + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + try { + runnable.run(); + } catch (Throwable t) { + throwableBox[0] = t; + } + }); + if (throwableBox[0] == null) { + return; + } else if (throwableBox[0] instanceof RuntimeException ex) { + throw ex; + } else if (throwableBox[0] instanceof Error err) { + throw err; + } else { + throw new RuntimeException(throwableBox[0]); + } + } + } + + private class LockAnimationClockRunnable implements RunnableWithException { + @Override + public void run() { + mLooperWithLockedAnimationClock = Looper.myLooper(); + mCanLockAnimationClock = true; + lockAnimationClockToCurrentTime(); + } + } + + private class UnlockAnimationClockRunnable implements RunnableWithException { + @Override + public void run() { + mCanLockAnimationClock = false; + mLooperWithLockedAnimationClock = null; + AnimationUtils.unlockAnimationClock(); + } + } + + private void lockAnimationClockToCurrentTime() { + if (!mCanLockAnimationClock) { + throw new AssertionError("Unable to lock the animation clock; " + + "has the test started? already finished?"); + } + if (mLooperWithLockedAnimationClock != Looper.myLooper()) { + throw new AssertionError("Animation clock being locked on " + Looper.myLooper() + + " but should only be locked on " + mLooperWithLockedAnimationClock); + } + long desiredTime = getCurrentTime(); + AnimationUtils.lockAnimationClock(desiredTime); + if (!mCanLockAnimationClock) { + AnimationUtils.unlockAnimationClock(); + throw new AssertionError("Threading error when locking the animation clock"); + } + long outputTime = AnimationUtils.currentAnimationTimeMillis(); + if (outputTime != desiredTime) { + // Skip the test (rather than fail it) if there's a clock issue + throw new AssumptionViolatedException("currentAnimationTimeMillis() is " + outputTime + + " after locking to " + desiredTime); + } + } + + /** + * If any new {@link Animator}s have been registered since the last time the frame time was + * advanced, initialize them with the current frame time. Failing to do this will result in the + * animations beginning on the *next* advancement instead, so this is done automatically for + * test authors inside of {@link #advanceTimeBy}. However this is exposed in case authors want + * to validate operations performed by onStart listeners. + * <p> + * NOTE: This is only required of the platform ValueAnimator because its start() method calls + * {@link AnimationHandler#addAnimationFrameCallback} BEFORE it calls startAnimation(), so this + * rule can't synchronously trigger the callback at that time. + */ + public void initNewAnimators() { + requireLooper("AnimationTestRule#initNewAnimators()"); + long currentTime = getCurrentTime(); + final TestHandler testHandler = mTestHandler.get(); + List<AnimationFrameCallback> newCallbacks = new ArrayList<>(testHandler.mNewCallbacks); + testHandler.mNewCallbacks.clear(); + for (AnimationFrameCallback newCallback : newCallbacks) { + newCallback.doAnimationFrame(currentTime); + } + } + + /** + * Advances the animation clock by the given amount of delta in milliseconds. This call will + * produce an animation frame to all the ongoing animations. This method needs to be + * called on the same thread as {@link Animator#start()}. + * + * @param timeDelta the amount of milliseconds to advance + */ + public void advanceTimeBy(long timeDelta) { + advanceTimeBy(timeDelta, null); + } + + /** + * Advances the animation clock by the given amount of delta in milliseconds. This call will + * produce an animation frame to all the ongoing animations. This method needs to be + * called on the same thread as {@link Animator#start()}. + * <p> + * This method is not for test authors, but for rule authors to ensure that multiple animators + * can be advanced in sync. + * + * @param timeDelta the amount of milliseconds to advance + * @param preFrameAction a consumer to be passed the timeDelta following the time advancement + * but prior to the frame production. + */ + public void advanceTimeBy(long timeDelta, @Nullable Consumer<Long> preFrameAction) { + Preconditions.checkArgumentNonnegative(timeDelta, "timeDelta must not be negative"); + requireLooper("AnimationTestRule#advanceTimeBy(long)"); + final TestHandler testHandler = mTestHandler.get(); + if (timeDelta == 0) { + // If time is not being advanced, all animators will get a tick; don't double tick these + testHandler.mNewCallbacks.clear(); + } else { + // before advancing time, start new animators with the current time + initNewAnimators(); + } + synchronized (mLock) { + // advance time + mTotalTimeDelta += timeDelta; + } + lockAnimationClockToCurrentTime(); + if (preFrameAction != null) { + preFrameAction.accept(timeDelta); + // After letting other code run, clear any new callbacks to avoid double-ticking them + testHandler.mNewCallbacks.clear(); + } + // produce a frame + testHandler.doFrame(); + } + + /** + * Returns the current time in milliseconds tracked by AnimationHandler. Note that this is a + * different time than the time tracked by {@link SystemClock} This method needs to be called on + * the same thread as {@link Animator#start()}. + */ + public long getCurrentTime() { + requireLooper("AnimationTestRule#getCurrentTime()"); + synchronized (mLock) { + return mStartTime + mTotalTimeDelta; + } + } + + private static void requireLooper(String method) { + if (Looper.myLooper() == null) { + throw new AndroidRuntimeException(method + " may only be called on Looper threads"); + } + } + + private class TestHandler extends AnimationHandler { + public final TestProvider mTestProvider = new TestProvider(); + private final List<AnimationFrameCallback> mNewCallbacks = new ArrayList<>(); + + TestHandler() { + setProvider(mTestProvider); + } + + public void doFrame() { + mTestProvider.animateFrame(); + mTestProvider.commitFrame(); + } + + @Override + public void addAnimationFrameCallback(AnimationFrameCallback callback, long delay) { + // NOTE: using the delay is infeasible because the AnimationHandler uses + // SystemClock.uptimeMillis(); -- If we fix this to use an overridable method, then we + // could fix this for tests. + super.addAnimationFrameCallback(callback, 0); + if (delay <= 0) { + mNewCallbacks.add(callback); + } + } + + @Override + public void removeCallback(AnimationFrameCallback callback) { + super.removeCallback(callback); + mNewCallbacks.remove(callback); + } + } + + private class TestProvider implements AnimationHandler.AnimationFrameCallbackProvider { + private long mFrameDelay = 10; + private Choreographer.FrameCallback mFrameCallback = null; + private final List<Runnable> mCommitCallbacks = new ArrayList<>(); + + public void animateFrame() { + Choreographer.FrameCallback frameCallback = mFrameCallback; + mFrameCallback = null; + if (frameCallback != null) { + frameCallback.doFrame(getFrameTime()); + } + } + + public void commitFrame() { + List<Runnable> commitCallbacks = new ArrayList<>(mCommitCallbacks); + mCommitCallbacks.clear(); + for (Runnable commitCallback : commitCallbacks) { + commitCallback.run(); + } + } + + @Override + public void postFrameCallback(Choreographer.FrameCallback callback) { + assert mFrameCallback == null; + mFrameCallback = callback; + } + + @Override + public void postCommitCallback(Runnable runnable) { + mCommitCallbacks.add(runnable); + } + + @Override + public void setFrameDelay(long delay) { + mFrameDelay = delay; + } + + @Override + public long getFrameDelay() { + return mFrameDelay; + } + + @Override + public long getFrameTime() { + return getCurrentTime(); + } + } +} diff --git a/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt b/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt new file mode 100644 index 000000000000..ded467993eef --- /dev/null +++ b/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation + +import android.animation.AnimatorTestRuleToolkit.Companion.TAG +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.util.Log +import android.view.View +import androidx.core.graphics.drawable.toBitmap +import androidx.test.core.app.ActivityScenario +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import platform.test.motion.MotionTestRule +import platform.test.motion.RecordedMotion +import platform.test.motion.RecordedMotion.Companion.create +import platform.test.motion.golden.DataPoint +import platform.test.motion.golden.Feature +import platform.test.motion.golden.FrameId +import platform.test.motion.golden.TimeSeries +import platform.test.motion.golden.TimeSeriesCaptureScope +import platform.test.motion.golden.TimestampFrameId +import platform.test.screenshot.captureToBitmapAsync + +class AnimatorTestRuleToolkit( + internal val animatorTestRule: AnimatorTestRule, + internal val testScope: TestScope, + internal val currentActivityScenario: () -> ActivityScenario<*>, +) { + internal companion object { + const val TAG = "AnimatorRuleToolkit" + } +} + +/** Capture utility to extract a [Bitmap] from a [drawable]. */ +fun captureDrawable(drawable: Drawable): Bitmap { + val width = drawable.bounds.right - drawable.bounds.left + val height = drawable.bounds.bottom - drawable.bounds.top + + // If either dimension is 0 this will fail, so we set it to 1 pixel instead. + return drawable.toBitmap( + width = + if (width > 0) { + width + } else { + 1 + }, + height = + if (height > 0) { + height + } else { + 1 + }, + ) +} + +/** Capture utility to extract a [Bitmap] from a [view]. */ +fun captureView(view: View): Bitmap { + return view.captureToBitmapAsync().get(10, TimeUnit.SECONDS) +} + +/** + * Controls the timing of the motion recording. + * + * The time series is recorded while the [recording] function is running. + */ +class MotionControl(val recording: MotionControlFn) + +typealias MotionControlFn = suspend MotionControlScope.() -> Unit + +interface MotionControlScope { + /** Waits until [check] returns true. Invoked on each frame. */ + suspend fun awaitCondition(check: () -> Boolean) + + /** Waits for [count] frames to be processed. */ + suspend fun awaitFrames(count: Int = 1) +} + +/** Defines the sampling of features during a test run. */ +data class AnimatorRuleRecordingSpec<T>( + /** The root `observing` object, available in [timeSeriesCapture]'s [TimeSeriesCaptureScope]. */ + val captureRoot: T, + + /** The timing for the recording. */ + val motionControl: MotionControl, + + /** Time interval between frame captures, in milliseconds. */ + val frameDurationMs: Long = 16L, + + /** Whether a sequence of screenshots should also be recorded. */ + val visualCapture: ((captureRoot: T) -> Bitmap)? = null, + + /** Produces the time-series, invoked on each animation frame. */ + val timeSeriesCapture: TimeSeriesCaptureScope<T>.() -> Unit, +) + +/** Records the time-series of the features specified in [recordingSpec]. */ +fun <T> MotionTestRule<AnimatorTestRuleToolkit>.recordMotion( + recordingSpec: AnimatorRuleRecordingSpec<T> +): RecordedMotion { + with(toolkit.animatorTestRule) { + val activityScenario = toolkit.currentActivityScenario() + val frameIdCollector = mutableListOf<FrameId>() + val propertyCollector = mutableMapOf<String, MutableList<DataPoint<*>>>() + val screenshotCollector = + if (recordingSpec.visualCapture != null) { + mutableListOf<Bitmap>() + } else { + null + } + + fun recordFrame(frameId: FrameId) { + Log.i(TAG, "recordFrame($frameId)") + frameIdCollector.add(frameId) + activityScenario.onActivity { + recordingSpec.timeSeriesCapture.invoke( + TimeSeriesCaptureScope(recordingSpec.captureRoot, propertyCollector) + ) + } + + val bitmap = recordingSpec.visualCapture?.invoke(recordingSpec.captureRoot) + if (bitmap != null) screenshotCollector!!.add(bitmap) + } + + val motionControl = + MotionControlImpl( + toolkit.animatorTestRule, + toolkit.testScope, + recordingSpec.frameDurationMs, + recordingSpec.motionControl, + ) + + Log.i(TAG, "recordMotion() begin recording") + + var startFrameTime: Long? = null + toolkit.currentActivityScenario().onActivity { startFrameTime = currentTime } + while (!motionControl.recordingEnded) { + var time: Long? = null + toolkit.currentActivityScenario().onActivity { time = currentTime } + recordFrame(TimestampFrameId(time!! - startFrameTime!!)) + toolkit.currentActivityScenario().onActivity { motionControl.nextFrame() } + } + + Log.i(TAG, "recordMotion() end recording") + + val timeSeries = + TimeSeries( + frameIdCollector.toList(), + propertyCollector.entries.map { entry -> Feature(entry.key, entry.value) }, + ) + + return create(timeSeries, screenshotCollector) + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +private class MotionControlImpl( + val animatorTestRule: AnimatorTestRule, + val testScope: TestScope, + val frameMs: Long, + motionControl: MotionControl, +) : MotionControlScope { + private val recordingJob = motionControl.recording.launch() + + private val frameEmitter = MutableStateFlow<Long>(0) + private val onFrame = frameEmitter.asStateFlow() + + var recordingEnded: Boolean = false + + fun nextFrame() { + animatorTestRule.advanceTimeBy(frameMs) + + frameEmitter.tryEmit(animatorTestRule.currentTime) + testScope.runCurrent() + + if (recordingJob.isCompleted) { + recordingEnded = true + } + } + + override suspend fun awaitCondition(check: () -> Boolean) { + onFrame.takeWhile { !check() }.collect {} + } + + override suspend fun awaitFrames(count: Int) { + onFrame.take(count).collect {} + } + + private fun MotionControlFn.launch(): Job { + val function = this + return testScope.launch { function() } + } +} diff --git a/tests/testables/src/android/testing/TestWithLooperRule.java b/tests/testables/src/android/testing/TestWithLooperRule.java index 37b39c314e53..6a8e142e2314 100644 --- a/tests/testables/src/android/testing/TestWithLooperRule.java +++ b/tests/testables/src/android/testing/TestWithLooperRule.java @@ -34,13 +34,13 @@ import java.util.List; * Looper for the Statement. */ public class TestWithLooperRule implements MethodRule { - /* * This rule requires to be the inner most Rule, so the next statement is RunAfters * instead of another rule. You can set it by '@Rule(order = Integer.MAX_VALUE)' */ @Override public Statement apply(Statement base, FrameworkMethod method, Object target) { + // getting testRunner check, if AndroidTestingRunning then we skip this rule RunWith runWithAnnotation = target.getClass().getAnnotation(RunWith.class); if (runWithAnnotation != null) { @@ -97,6 +97,12 @@ public class TestWithLooperRule implements MethodRule { case "InvokeParameterizedMethod": this.wrapFieldMethodFor(next, "frameworkMethod", method, target); return; + case "ExpectException": + next = this.getNextStatement(next, "next"); + break; + case "UiThreadStatement": + next = this.getNextStatement(next, "base"); + break; default: throw new Exception( String.format("Unexpected Statement received: [%s]", diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index be5c84c0353c..ac96ef28f501 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -53,6 +53,7 @@ public class TestableLooper { private static final Field MESSAGE_QUEUE_MESSAGES_FIELD; private static final Field MESSAGE_NEXT_FIELD; private static final Field MESSAGE_WHEN_FIELD; + private static Field MESSAGE_QUEUE_USE_CONCURRENT_FIELD = null; private Looper mLooper; private MessageQueue mQueue; @@ -63,6 +64,14 @@ public class TestableLooper { static { try { + MESSAGE_QUEUE_USE_CONCURRENT_FIELD = + MessageQueue.class.getDeclaredField("mUseConcurrent"); + MESSAGE_QUEUE_USE_CONCURRENT_FIELD.setAccessible(true); + } catch (NoSuchFieldException ignored) { + // Ignore - maybe this is not CombinedMessageQueue? + } + + try { MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); @@ -146,6 +155,15 @@ public class TestableLooper { mLooper = l; mQueue = mLooper.getQueue(); mHandler = new Handler(mLooper); + + // If we are using CombinedMessageQueue, we need to disable concurrent mode for testing. + if (MESSAGE_QUEUE_USE_CONCURRENT_FIELD != null) { + try { + MESSAGE_QUEUE_USE_CONCURRENT_FIELD.set(mQueue, false); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } } /** diff --git a/tests/testables/tests/Android.bp b/tests/testables/tests/Android.bp index 2a3e4ae0c039..f0cda535b3aa 100644 --- a/tests/testables/tests/Android.bp +++ b/tests/testables/tests/Android.bp @@ -26,14 +26,24 @@ android_test { platform_apis: true, srcs: [ "src/**/*.java", + "src/**/*.kt", "src/**/I*.aidl", ], + asset_dirs: ["goldens"], resource_dirs: ["res"], static_libs: [ + "PlatformMotionTesting", + "androidx.core_core-animation", + "androidx.core_core-ktx", + "androidx.test.ext.junit", "androidx.test.rules", "hamcrest-library", + "kotlinx_coroutines_test", "mockito-target-inline-minus-junit4", + "platform-screenshot-diff-core", + "platform-test-annotations", "testables", + "truth", ], compile_multilib: "both", jni_libs: [ @@ -46,6 +56,7 @@ android_test { "android.test.mock.stubs.system", ], certificate: "platform", + test_config: "AndroidTest.xml", test_suites: [ "device-tests", "automotive-tests", diff --git a/tests/testables/tests/AndroidManifest.xml b/tests/testables/tests/AndroidManifest.xml index 2bfb04fdb765..6cba59872710 100644 --- a/tests/testables/tests/AndroidManifest.xml +++ b/tests/testables/tests/AndroidManifest.xml @@ -23,6 +23,10 @@ <application android:debuggable="true"> <uses-library android:name="android.test.runner" /> + <activity + android:name="platform.test.screenshot.ScreenshotActivity" + android:exported="true"> + </activity> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/tests/testables/tests/AndroidTest.xml b/tests/testables/tests/AndroidTest.xml new file mode 100644 index 000000000000..85f6e6257770 --- /dev/null +++ b/tests/testables/tests/AndroidTest.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. +--> +<configuration description="Runs Tests for Testables."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="TestablesTests.apk" /> + <option name="install-arg" value="-t" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"> + <option name="force-root" value="true" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <option name="screen-always-on" value="on" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="input keyevent KEYCODE_WAKEUP" /> + <option name="run-command" value="wm dismiss-keyguard" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="TestableTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.testables" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="test-filter-dir" value="/data/data/com.android.testables" /> + <option name="hidden-api-checks" value="false"/> + </test> + + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/data/user/0/com.android.testables/files"/> + <option name="collect-on-run-ended-only" value="true"/> + <option name="clean-up" value="true"/> + </metrics_collector> +</configuration> diff --git a/tests/testables/tests/goldens/recordFilmstrip_withAnimator.png b/tests/testables/tests/goldens/recordFilmstrip_withAnimator.png Binary files differnew file mode 100644 index 000000000000..9aed2e970239 --- /dev/null +++ b/tests/testables/tests/goldens/recordFilmstrip_withAnimator.png diff --git a/tests/testables/tests/goldens/recordFilmstrip_withSpring.png b/tests/testables/tests/goldens/recordFilmstrip_withSpring.png Binary files differnew file mode 100644 index 000000000000..1d0c0c3c3393 --- /dev/null +++ b/tests/testables/tests/goldens/recordFilmstrip_withSpring.png diff --git a/tests/testables/tests/goldens/recordTimeSeries_withAnimator.json b/tests/testables/tests/goldens/recordTimeSeries_withAnimator.json new file mode 100644 index 000000000000..73eb6c74fee6 --- /dev/null +++ b/tests/testables/tests/goldens/recordTimeSeries_withAnimator.json @@ -0,0 +1,64 @@ +{ + "frame_ids": [ + 0, + 20, + 40, + 60, + 80, + 100, + 120, + 140, + 160, + 180, + 200, + 220, + 240, + 260, + 280, + 300, + 320, + 340, + 360, + 380, + 400, + 420, + 440, + 460, + 480, + 500 + ], + "features": [ + { + "name": "alpha", + "type": "float", + "data_points": [ + 1, + 0.9960574, + 0.98429155, + 0.9648882, + 0.9381534, + 0.9045085, + 0.8644843, + 0.818712, + 0.76791346, + 0.7128896, + 0.65450853, + 0.5936906, + 0.5313952, + 0.46860474, + 0.40630943, + 0.34549147, + 0.2871104, + 0.23208654, + 0.181288, + 0.13551569, + 0.09549153, + 0.061846733, + 0.035111785, + 0.015708387, + 0.003942609, + 0 + ] + } + ] +} diff --git a/tests/testables/tests/goldens/recordTimeSeries_withSpring.json b/tests/testables/tests/goldens/recordTimeSeries_withSpring.json new file mode 100644 index 000000000000..2b97bad08e00 --- /dev/null +++ b/tests/testables/tests/goldens/recordTimeSeries_withSpring.json @@ -0,0 +1,48 @@ +{ + "frame_ids": [ + 0, + 16, + 32, + 48, + 64, + 80, + 96, + 112, + 128, + 144, + 160, + 176, + 192, + 208, + 224, + 240, + 256, + 272 + ], + "features": [ + { + "name": "alpha", + "type": "float", + "data_points": [ + 1, + 0.9488604, + 0.83574325, + 0.7016156, + 0.5691678, + 0.4497436, + 0.34789434, + 0.26431116, + 0.19766562, + 0.14572789, + 0.10601636, + 0.076149896, + 0.05401709, + 0.037837274, + 0.026161024, + 0.017839976, + 0.011983856, + 0.007914998 + ] + } + ] +} diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt new file mode 100644 index 000000000000..5abebee77d3d --- /dev/null +++ b/tests/testables/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt @@ -0,0 +1,89 @@ +/* + * 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.animation + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.core.animation.doOnEnd +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * This test class validates that two tests' animators are isolated from each other when using the + * same animator test rule. This is a test to prevent future instances of b/275602127. + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +@RunWithLooper +class AnimatorTestRuleIsolationTest { + + @get:Rule val animatorTestRule = AnimatorTestRule(this) + + @Test + fun testA() { + // GIVEN global state is reset at the start of the test + didTouchA = false + didTouchB = false + // WHEN starting 2 animations of different durations, and setting didTouch{A,B} at the end + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 100 + doOnEnd { didTouchA = true } + start() + } + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 150 + doOnEnd { didTouchB = true } + start() + } + // WHEN when you advance time so that only one of the animations has ended + animatorTestRule.advanceTimeBy(100) + // VERIFY we did indeed end the current animation + assertThat(didTouchA).isTrue() + // VERIFY advancing the animator did NOT cause testB's animator to end + assertThat(didTouchB).isFalse() + } + + @Test + fun testB() { + // GIVEN global state is reset at the start of the test + didTouchA = false + didTouchB = false + // WHEN starting 2 animations of different durations, and setting didTouch{A,B} at the end + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 100 + doOnEnd { didTouchB = true } + start() + } + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 150 + doOnEnd { didTouchA = true } + start() + } + animatorTestRule.advanceTimeBy(100) + // VERIFY advancing the animator did NOT cause testA's animator to end + assertThat(didTouchA).isFalse() + // VERIFY we did indeed end the current animation + assertThat(didTouchB).isTrue() + } + + companion object { + var didTouchA = false + var didTouchB = false + } +} diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt new file mode 100644 index 000000000000..9eeaad5cd272 --- /dev/null +++ b/tests/testables/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt @@ -0,0 +1,196 @@ +/* + * 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.animation + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.animation.LinearInterpolator +import androidx.core.animation.doOnEnd +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +@RunWithLooper +class AnimatorTestRulePrecisionTest { + + @get:Rule val animatorTestRule = AnimatorTestRule(this) + + var value1: Float = -1f + var value2: Float = -1f + + private inline fun animateThis( + propertyName: String, + duration: Long, + startDelay: Long = 0, + crossinline onEndAction: (animator: Animator) -> Unit, + ) { + ObjectAnimator.ofFloat(this, propertyName, 0f, 1f).also { + it.interpolator = LINEAR_INTERPOLATOR + it.duration = duration + it.startDelay = startDelay + it.doOnEnd(onEndAction) + it.start() + } + } + + @Test + fun testSingleAnimator() { + var ended = false + animateThis("value1", duration = 100) { ended = true } + + assertThat(value1).isEqualTo(0f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(50) + assertThat(value1).isEqualTo(0.5f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(49) + assertThat(value1).isEqualTo(0.99f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(ended).isTrue() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) + } + + @Test + fun testDelayedAnimator() { + var ended = false + animateThis("value1", duration = 100, startDelay = 50) { ended = true } + + assertThat(value1).isEqualTo(-1f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(49) + assertThat(value1).isEqualTo(-1f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(0f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(0.99f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(ended).isTrue() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) + } + + @Test + fun testTwoAnimators() { + var ended1 = false + var ended2 = false + animateThis("value1", duration = 100) { ended1 = true } + animateThis("value2", duration = 200) { ended2 = true } + assertThat(value1).isEqualTo(0f) + assertThat(value2).isEqualTo(0f) + assertThat(ended1).isFalse() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(0.99f) + assertThat(value2).isEqualTo(0.495f) + assertThat(ended1).isFalse() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(0.5f) + assertThat(ended1).isTrue() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(0.995f) + assertThat(ended1).isTrue() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(1f) + assertThat(ended1).isTrue() + assertThat(ended2).isTrue() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) + } + + @Test + fun testChainedAnimators() { + var ended1 = false + var ended2 = false + animateThis("value1", duration = 100) { + ended1 = true + animateThis("value2", duration = 100) { ended2 = true } + } + + assertThat(value1).isEqualTo(0f) + assertThat(value2).isEqualTo(-1f) + assertThat(ended1).isFalse() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(0.99f) + assertThat(value2).isEqualTo(-1f) + assertThat(ended1).isFalse() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(0f) + assertThat(ended1).isTrue() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(0.99f) + assertThat(ended1).isTrue() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(1f) + assertThat(ended1).isTrue() + assertThat(ended2).isTrue() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) + } + + private companion object { + private val LINEAR_INTERPOLATOR = LinearInterpolator() + } +} diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt new file mode 100644 index 000000000000..993c3fed9d59 --- /dev/null +++ b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation + +import android.graphics.Color +import android.platform.test.annotations.MotionTest +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.dynamicanimation.animation.DynamicAnimation +import com.android.internal.dynamicanimation.animation.SpringAnimation +import com.android.internal.dynamicanimation.animation.SpringForce +import kotlinx.coroutines.test.TestScope +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.motion.MotionTestRule +import platform.test.motion.RecordedMotion +import platform.test.motion.testing.createGoldenPathManager +import platform.test.motion.view.ViewFeatureCaptures +import platform.test.screenshot.DeviceEmulationRule +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.DisplaySpec +import platform.test.screenshot.ScreenshotActivity +import platform.test.screenshot.ScreenshotTestRule + +@SmallTest +@MotionTest +@RunWith(AndroidJUnit4::class) +class AnimatorTestRuleToolkitTest { + companion object { + private val GOLDEN_PATH_MANAGER = + createGoldenPathManager("frameworks/base/tests/testables/tests/goldens") + + private val EMULATION_SPEC = + DeviceEmulationSpec(DisplaySpec("phone", width = 320, height = 690, densityDpi = 160)) + } + + @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(EMULATION_SPEC) + @get:Rule(order = 1) val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java) + @get:Rule(order = 2) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 3) val screenshotRule = ScreenshotTestRule(GOLDEN_PATH_MANAGER) + @get:Rule(order = 4) + val motionRule = + MotionTestRule( + AnimatorTestRuleToolkit(animatorTestRule, TestScope()) { activityRule.scenario }, + GOLDEN_PATH_MANAGER, + bitmapDiffer = screenshotRule, + ) + + @Test + fun recordFilmstrip_withAnimator() { + val animatedBox = createScene() + createAnimator(animatedBox).apply { getInstrumentation().runOnMainSync { start() } } + + val recordedMotion = + record( + animatedBox, + MotionControl { awaitFrames(count = 26) }, + sampleIntervalMs = 20L, + recordScreenshots = true, + ) + + motionRule.assertThat(recordedMotion).filmstripMatchesGolden("recordFilmstrip_withAnimator") + } + + @Test + fun recordTimeSeries_withAnimator() { + val animatedBox = createScene() + createAnimator(animatedBox).apply { getInstrumentation().runOnMainSync { start() } } + + val recordedMotion = + record( + animatedBox, + MotionControl { awaitFrames(count = 26) }, + sampleIntervalMs = 20L, + recordScreenshots = false, + ) + + motionRule + .assertThat(recordedMotion) + .timeSeriesMatchesGolden("recordTimeSeries_withAnimator") + } + + @Test + fun recordFilmstrip_withSpring() { + val animatedBox = createScene() + var isDone = false + createSpring(animatedBox).apply { + addEndListener { _, _, _, _ -> isDone = true } + getInstrumentation().runOnMainSync { start() } + } + + val recordedMotion = + record( + animatedBox, + MotionControl { awaitCondition { isDone } }, + sampleIntervalMs = 16L, + recordScreenshots = true, + ) + + motionRule.assertThat(recordedMotion).filmstripMatchesGolden("recordFilmstrip_withSpring") + } + + @Test + fun recordTimeSeries_withSpring() { + val animatedBox = createScene() + var isDone = false + createSpring(animatedBox).apply { + addEndListener { _, _, _, _ -> isDone = true } + getInstrumentation().runOnMainSync { start() } + } + + val recordedMotion = + record( + animatedBox, + MotionControl { awaitCondition { isDone } }, + sampleIntervalMs = 16L, + recordScreenshots = false, + ) + + motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden("recordTimeSeries_withSpring") + } + + private fun createScene(): ViewGroup { + lateinit var sceneRoot: ViewGroup + activityRule.scenario.onActivity { activity -> + sceneRoot = FrameLayout(activity).apply { setBackgroundColor(Color.BLACK) } + activity.setContentView(sceneRoot) + } + getInstrumentation().waitForIdleSync() + return sceneRoot + } + + private fun createAnimator(animatedBox: ViewGroup): AnimatorSet { + return AnimatorSet().apply { + duration = 500 + play( + ValueAnimator.ofFloat(animatedBox.alpha, 0f).apply { + addUpdateListener { animatedBox.alpha = it.animatedValue as Float } + } + ) + } + } + + private fun createSpring(animatedBox: ViewGroup): SpringAnimation { + return SpringAnimation(animatedBox, DynamicAnimation.ALPHA).apply { + spring = + SpringForce(0f).apply { + stiffness = 500f + dampingRatio = 0.95f + } + + setStartValue(animatedBox.alpha) + setMinValue(0f) + setMaxValue(1f) + minimumVisibleChange = 0.01f + } + } + + private fun record( + container: ViewGroup, + motionControl: MotionControl, + sampleIntervalMs: Long, + recordScreenshots: Boolean, + ): RecordedMotion { + val visualCapture = + if (recordScreenshots) { + ::captureView + } else { + null + } + return motionRule.recordMotion( + AnimatorRuleRecordingSpec( + container, + motionControl, + sampleIntervalMs, + visualCapture, + ) { + feature(ViewFeatureCaptures.alpha, "alpha") + } + ) + } +} diff --git a/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java b/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java new file mode 100644 index 000000000000..b7d5e0e12942 --- /dev/null +++ b/tests/testables/tests/src/android/testing/TestableLooperJUnit4Test.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.testing; + +import android.testing.TestableLooper.RunWithLooper; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test that TestableLooper now handles expected exceptions in tests + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +@RunWithLooper +public class TestableLooperJUnit4Test { + @Rule + public final TestWithLooperRule mTestWithLooperRule = new TestWithLooperRule(); + + @Test(expected = Exception.class) + public void testException() throws Exception { + throw new Exception("this exception is expected"); + } +} + diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index a826646f69f3..1bcfaf60857d 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -93,13 +93,25 @@ public class TestLooper { try { mLooper = LOOPER_CONSTRUCTOR.newInstance(false); - ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD - .get(null); + ThreadLocal<Looper> threadLocalLooper = + (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD.get(null); threadLocalLooper.set(mLooper); } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { throw new RuntimeException("Reflection error constructing or accessing looper", e); } + // If we are using CombinedMessageQueue, we need to disable concurrent mode for testing. + try { + Field messageQueueUseConcurrentField = + MessageQueue.class.getDeclaredField("mUseConcurrent"); + messageQueueUseConcurrentField.setAccessible(true); + messageQueueUseConcurrentField.set(mLooper.getQueue(), false); + } catch (NoSuchFieldException e) { + // Ignore - maybe this is not CombinedMessageQueue? + } catch (IllegalAccessException e) { + throw new RuntimeException("Reflection error constructing or accessing looper", e); + } + mClock = clock; } |