diff options
Diffstat (limited to 'tests')
223 files changed, 9353 insertions, 3474 deletions
diff --git a/tests/AppJankTest/Android.bp b/tests/AppJankTest/Android.bp new file mode 100644 index 000000000000..c3cda6a41cbb --- /dev/null +++ b/tests/AppJankTest/Android.bp @@ -0,0 +1,38 @@ +// 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", + "androidx.test.uiautomator_uiautomator", + ], + 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..861a79c6f0ed --- /dev/null +++ b/tests/AppJankTest/AndroidManifest.xml @@ -0,0 +1,49 @@ +<?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 android:appCategory="news"> + <activity android:name=".JankTrackerActivity" + android:exported="true" + android:label="JankTrackerActivity" + android:launchMode="singleTop"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/> + </intent-filter> + </activity> + <activity android:name=".EmptyActivity" + android:exported="true" + android:label="EmptyActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/> + </intent-filter> + </activity> + <uses-library android:name="android.test.runner"/> + </application> + + <!-- self-instrumenting test package. --> + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="Core tests of App Jank Tracking" + android:targetPackage="android.app.jank.tests"> + </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/res/layout/jank_tracker_activity_layout.xml b/tests/AppJankTest/res/layout/jank_tracker_activity_layout.xml new file mode 100644 index 000000000000..65def7f2d5b6 --- /dev/null +++ b/tests/AppJankTest/res/layout/jank_tracker_activity_layout.xml @@ -0,0 +1,47 @@ +<?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. + --> + + +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + + <LinearLayout + android:id="@+id/linear_layout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <EditText + android:id="@+id/edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textEnableTextConversionSuggestions" + android:text="Edit Text"/> + <TextView android:id="@+id/text_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Text View" + /> + <android.app.jank.tests.TestWidget + android:id="@+id/jank_tracker_widget" + android:layout_width="match_parent" + android:layout_height="wrap_content" + /> + </LinearLayout> +</ScrollView>
\ No newline at end of file diff --git a/tests/AppJankTest/res/values/strings.xml b/tests/AppJankTest/res/values/strings.xml new file mode 100644 index 000000000000..ab2d18fa9d53 --- /dev/null +++ b/tests/AppJankTest/res/values/strings.xml @@ -0,0 +1,3 @@ +<resources> + <string name="continue_test">Continue Test</string> +</resources>
\ 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/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java new file mode 100644 index 000000000000..3498974b348e --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java @@ -0,0 +1,217 @@ +/* + * 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.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import android.app.Activity; +import android.app.Instrumentation; +import android.app.jank.AppJankStats; +import android.app.jank.Flags; +import android.app.jank.JankDataProcessor; +import android.app.jank.JankTracker; +import android.app.jank.StateTracker; +import android.content.Intent; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.widget.EditText; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.Until; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This file contains tests that verify the proper functionality of the Jank Tracking feature. + * All tests should obtain references to necessary objects through View type interfaces, rather + * than direct instantiation. When operating outside of a testing environment, the expected + * behavior is to retrieve the necessary objects using View type interfaces. This approach ensures + * that calls are correctly routed down to the activity level. Any modifications to the call + * routing should result in test failures, which might happen with direct instantiations. + */ +@RunWith(AndroidJUnit4.class) +public class IntegrationTests { + public static final int WAIT_FOR_TIMEOUT_MS = 5000; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private Activity mEmptyActivity; + + public UiDevice mDevice; + private Instrumentation mInstrumentation; + private ActivityTestRule<JankTrackerActivity> mJankTrackerActivityRule = + new ActivityTestRule<>( + JankTrackerActivity.class, + false, + false); + + private ActivityTestRule<EmptyActivity> mEmptyActivityRule = + new ActivityTestRule<>(EmptyActivity.class, false , true); + + + @Before + public void setUp() { + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + mDevice = UiDevice.getInstance(mInstrumentation); + } + + + /** + * Get a JankTracker object from a view and confirm it's not null. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void getJankTacker_confirmNotNull() { + Activity jankTrackerActivity = mJankTrackerActivityRule.launchActivity(null); + EditText editText = jankTrackerActivity.findViewById(R.id.edit_text); + + mDevice.wait(Until.findObject(By.text("Edit Text")), WAIT_FOR_TIMEOUT_MS); + + JankTracker jankTracker = editText.getJankTracker(); + assertTrue(jankTracker != null); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void reportJankStats_confirmPendingStatsIncreases() { + Activity jankTrackerActivity = mJankTrackerActivityRule.launchActivity(null); + EditText editText = jankTrackerActivity.findViewById(R.id.edit_text); + JankTracker jankTracker = editText.getJankTracker(); + + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + jankTracker.getPendingJankStats(); + assertEquals(0, pendingStats.size()); + + editText.reportAppJankStats(JankUtils.getAppJankStats()); + + // reportAppJankStats performs the work on a background thread, check periodically to see + // if the work is complete. + for (int i = 0; i < 10; i++) { + try { + Thread.sleep(100); + if (jankTracker.getPendingJankStats().size() > 0) { + break; + } + } catch (InterruptedException exception) { + //do nothing and continue + } + } + + pendingStats = jankTracker.getPendingJankStats(); + + assertEquals(1, pendingStats.size()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void simulateWidgetStateChanges_confirmStateChangesAreTracked() { + JankTrackerActivity jankTrackerActivity = + mJankTrackerActivityRule.launchActivity(null); + TestWidget testWidget = jankTrackerActivity.findViewById(R.id.jank_tracker_widget); + JankTracker jankTracker = testWidget.getJankTracker(); + jankTracker.forceListenerRegistration(); + + ArrayList<StateTracker.StateData> uiStates = new ArrayList<>(); + // Get the current UI states, at this point only the activity name should be in the UI + // states list. + jankTracker.getAllUiStates(uiStates); + + assertEquals(1, uiStates.size()); + + // This should add a UI state to be tracked. + testWidget.simulateAnimationStarting(); + uiStates.clear(); + jankTracker.getAllUiStates(uiStates); + + assertEquals(2, uiStates.size()); + + // Stop the animation + testWidget.simulateAnimationEnding(); + uiStates.clear(); + jankTracker.getAllUiStates(uiStates); + + assertEquals(2, uiStates.size()); + + // Confirm the Animation state has a VsyncIdEnd that is not default, indicating the end + // of that state. + for (int i = 0; i < uiStates.size(); i++) { + StateTracker.StateData stateData = uiStates.get(i); + if (stateData.mWidgetCategory.equals(AppJankStats.WIDGET_CATEGORY_ANIMATION)) { + assertNotEquals(Long.MAX_VALUE, stateData.mVsyncIdEnd); + } + } + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void jankTrackingPaused_whenActivityNoLongerVisible() { + JankTrackerActivity jankTrackerActivity = + mJankTrackerActivityRule.launchActivity(null); + TestWidget testWidget = jankTrackerActivity.findViewById(R.id.jank_tracker_widget); + JankTracker jankTracker = testWidget.getJankTracker(); + jankTracker.forceListenerRegistration(); + + assertTrue(jankTracker.shouldTrack()); + + // Send jankTrackerActivity to the background + mDevice.pressHome(); + mDevice.waitForIdle(WAIT_FOR_TIMEOUT_MS); + + assertFalse(jankTracker.shouldTrack()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void jankTrackingResumed_whenActivityBecomesVisibleAgain() { + mEmptyActivityRule.launchActivity(null); + mEmptyActivity = mEmptyActivityRule.getActivity(); + JankTrackerActivity jankTrackerActivity = + mJankTrackerActivityRule.launchActivity(null); + TestWidget testWidget = jankTrackerActivity.findViewById(R.id.jank_tracker_widget); + JankTracker jankTracker = testWidget.getJankTracker(); + jankTracker.forceListenerRegistration(); + + // Send jankTrackerActivity to the background + mDevice.pressHome(); + mDevice.waitForIdle(WAIT_FOR_TIMEOUT_MS); + + assertFalse(jankTracker.shouldTrack()); + + Intent resumeJankTracker = new Intent(mInstrumentation.getContext(), + JankTrackerActivity.class); + resumeJankTracker.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + mEmptyActivity.startActivity(resumeJankTracker); + mDevice.wait(Until.findObject(By.text(mEmptyActivity.getString(R.string.continue_test))), + WAIT_FOR_TIMEOUT_MS); + + assertTrue(jankTracker.shouldTrack()); + } +} 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..c90595782cd1 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java @@ -0,0 +1,348 @@ +/* + * 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.AppJankStats; +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.HashMap; +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); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void mergeAppJankStats_confirmStatAddedToPendingStats() { + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + mJankDataProcessor.getPendingJankStats(); + + assertEquals(pendingStats.size(), 0); + + AppJankStats jankStats = JankUtils.getAppJankStats(); + mJankDataProcessor.mergeJankStats(jankStats, sActivityName); + + pendingStats = mJankDataProcessor.getPendingJankStats(); + + assertEquals(pendingStats.size(), 1); + } + + /** + * This test confirms matching states are combined into one pending stat. When JankStats are + * merged from outside the platform they will contain widget category, widget id and widget + * state. If an incoming JankStats matches a pending stat on all those fields the incoming + * JankStat will be merged into the existing stat. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void mergeAppJankStats_confirmStatsWithMatchingStatesAreCombinedIntoOnePendingStat() { + AppJankStats jankStats = JankUtils.getAppJankStats(); + mJankDataProcessor.mergeJankStats(jankStats, sActivityName); + + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + mJankDataProcessor.getPendingJankStats(); + assertEquals(pendingStats.size(), 1); + + AppJankStats secondJankStat = JankUtils.getAppJankStats(); + mJankDataProcessor.mergeJankStats(secondJankStat, sActivityName); + + pendingStats = mJankDataProcessor.getPendingJankStats(); + + assertEquals(pendingStats.size(), 1); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void mergeAppJankStats_whenStatsWithMatchingStatesMerge_confirmFrameCountsAdded() { + AppJankStats jankStats = JankUtils.getAppJankStats(); + mJankDataProcessor.mergeJankStats(jankStats, sActivityName); + mJankDataProcessor.mergeJankStats(jankStats, sActivityName); + + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + mJankDataProcessor.getPendingJankStats(); + + String statKey = pendingStats.keySet().iterator().next(); + JankDataProcessor.PendingJankStat pendingStat = pendingStats.get(statKey); + + assertEquals(pendingStats.size(), 1); + // The same jankStats objects are merged twice, this should result in the frame counts being + // doubled. + assertEquals(jankStats.getJankyFrameCount() * 2, pendingStat.getJankyFrames()); + assertEquals(jankStats.getTotalFrameCount() * 2, pendingStat.getTotalFrames()); + + int[] originalHistogramBuckets = + jankStats.getRelativeFrameTimeHistogram().getBucketCounters(); + int[] frameOverrunBuckets = pendingStat.getFrameOverrunBuckets(); + + for (int i = 0; i < frameOverrunBuckets.length; i++) { + assertEquals(originalHistogramBuckets[i] * 2, frameOverrunBuckets[i]); + } + } + + // 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/JankTrackerActivity.java b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java new file mode 100644 index 000000000000..686758200853 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerActivity.java @@ -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 android.app.jank.tests; + +import android.app.Activity; +import android.os.Bundle; +import android.widget.EditText; + + +public class JankTrackerActivity extends Activity { + + private static final int CONTINUE_TEST_DELAY_MS = 4000; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.jank_tracker_activity_layout); + } + + /** + * In IntegrationTests#jankTrackingResumed_whenActivityBecomesVisibleAgain this activity is + * placed into the background and then resumed via an intent. The test waits until the + * `continue_test` string is visible on the screen before validating that Jank tracking has + * resumed. + * + * <p>The 4 second delay allows JankTracker to re-register its callbacks and start receiving + * JankData before the test proceeds. + */ + @Override + protected void onResume() { + super.onResume(); + getActivityThread().getHandler().postDelayed(new Runnable() { + @Override + public void run() { + EditText editTextView = findViewById(R.id.edit_text); + if (editTextView != null) { + editTextView.setText(R.string.continue_test); + } + } + }, CONTINUE_TEST_DELAY_MS); + } +} + + 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..1bdf019d6c42 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java @@ -0,0 +1,171 @@ +/* + * 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.assertNotNull; + +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 com.android.internal.policy.DecorView; + +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()); + } + + /** + * Test confirms a JankTracker object is retrieved from the activity. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_LOGGING_ENABLED) + public void jankTracker_NotNull_WhenRetrievedFromDecorView() { + DecorView decorView = (DecorView) sActivityDecorView; + JankTracker jankTracker = decorView.getJankTracker(); + + assertNotNull(jankTracker); + } +} diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java new file mode 100644 index 000000000000..9640a84eb9ca --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java @@ -0,0 +1,53 @@ +/* + * 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.jank.AppJankStats; +import android.app.jank.RelativeFrameTimeHistogram; + +public class JankUtils { + private static final int APP_ID = 25; + + /** + * Returns a mock AppJankStats object to be used in tests. + */ + public static AppJankStats getAppJankStats() { + AppJankStats jankStats = new AppJankStats( + /*App Uid*/APP_ID, + /*Widget Id*/"test widget id", + /*navigationComponent*/null, + /*Widget Category*/AppJankStats.WIDGET_CATEGORY_SCROLL, + /*Widget State*/AppJankStats.WIDGET_STATE_SCROLLING, + /*Total Frames*/100, + /*Janky Frames*/25, + getOverrunHistogram() + ); + return jankStats; + } + + /** + * Returns a mock histogram to be used with an AppJankStats object. + */ + public static RelativeFrameTimeHistogram getOverrunHistogram() { + RelativeFrameTimeHistogram overrunHistogram = new RelativeFrameTimeHistogram(); + overrunHistogram.addRelativeFrameTimeMillis(-2); + overrunHistogram.addRelativeFrameTimeMillis(1); + overrunHistogram.addRelativeFrameTimeMillis(5); + overrunHistogram.addRelativeFrameTimeMillis(25); + return overrunHistogram; + } +} 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/AppJankTest/src/android/app/jank/tests/TestWidget.java b/tests/AppJankTest/src/android/app/jank/tests/TestWidget.java new file mode 100644 index 000000000000..71796d64ddee --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/TestWidget.java @@ -0,0 +1,69 @@ +/* + * 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.jank.AppJankStats; +import android.app.jank.JankTracker; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +public class TestWidget extends View { + + private JankTracker mJankTracker; + + /** + * Create JankTrackerView + */ + public TestWidget(Context context) { + super(context); + } + + /** + * Create JankTrackerView, needed by system when inflating views defined in a layout file. + */ + public TestWidget(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + /** + * Mock starting an animation. + */ + public void simulateAnimationStarting() { + if (jankTrackerCreated()) { + mJankTracker.addUiState(AppJankStats.WIDGET_CATEGORY_ANIMATION, + Integer.toString(this.getId()), AppJankStats.WIDGET_STATE_ANIMATING); + } + } + + /** + * Mock ending an animation. + */ + public void simulateAnimationEnding() { + if (jankTrackerCreated()) { + mJankTracker.removeUiState(AppJankStats.WIDGET_CATEGORY_ANIMATION, + Integer.toString(this.getId()), AppJankStats.WIDGET_STATE_ANIMATING); + } + } + + private boolean jankTrackerCreated() { + if (mJankTracker == null) { + mJankTracker = getJankTracker(); + } + return mJankTracker != null; + } +} diff --git a/tests/AttestationVerificationTest/AndroidManifest.xml b/tests/AttestationVerificationTest/AndroidManifest.xml index 37321ad80b0f..758852bb1074 100644 --- a/tests/AttestationVerificationTest/AndroidManifest.xml +++ b/tests/AttestationVerificationTest/AndroidManifest.xml @@ -18,7 +18,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.security.attestationverification"> - <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" /> + <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="34" /> <uses-permission android:name="android.permission.USE_ATTESTATION_VERIFICATION_SERVICE" /> <application> diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json new file mode 100644 index 000000000000..2a3ba5ebde7d --- /dev/null +++ b/tests/AttestationVerificationTest/assets/test_revocation_list_no_test_certs.json @@ -0,0 +1,12 @@ +{ + "entries": { + "6681152659205225093" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "8350192447815228107" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + } + } +}
\ No newline at end of file diff --git a/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json new file mode 100644 index 000000000000..e22a834a92bf --- /dev/null +++ b/tests/AttestationVerificationTest/assets/test_revocation_list_with_test_certs.json @@ -0,0 +1,16 @@ +{ + "entries": { + "6681152659205225093" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "353017e73dc205a73a9c3de142230370" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + }, + "8350192447815228107" : { + "status": "REVOKED", + "reason": "KEY_COMPROMISE" + } + } +}
\ No newline at end of file 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 4712d6b51bd5..4d1a1a55af74 100644 --- a/tests/AttestationVerificationTest/src/com/android/server/security/AttestationVerificationPeerDeviceVerifierTest.kt +++ b/tests/AttestationVerificationTest/src/com/android/server/security/AttestationVerificationPeerDeviceVerifierTest.kt @@ -3,11 +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 @@ -72,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 @@ -88,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 @@ -108,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 @@ -126,7 +127,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TEST_OWNED_BY_SYSTEM_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_SUCCESS) + assertThat(result).isEqualTo(0) } @Test @@ -143,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 @@ -159,7 +160,7 @@ 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 @@ -176,7 +177,7 @@ class AttestationVerificationPeerDeviceVerifierTest { TYPE_CHALLENGE, challengeRequirements, TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_SUCCESS) + assertThat(result).isEqualTo(0) } @Test @@ -191,10 +192,28 @@ class AttestationVerificationPeerDeviceVerifierTest { val result = verifier.verifyAttestation( TYPE_CHALLENGE, challengeRequirements, - // The patch date of this file is early 2022 TEST_ATTESTATION_WITH_ROOT_CERT_FILENAME.fromPEMFileToByteArray() ) - assertThat(result).isEqualTo(RESULT_FAILURE) + 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 @@ -210,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 @@ -232,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() { @@ -247,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> { @@ -281,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/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java new file mode 100644 index 000000000000..3854ae6dc9b3 --- /dev/null +++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.security; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.security.cert.CertPathValidatorException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class CertificateRevocationStatusManagerTest { + + private static final String TEST_CERTIFICATE_FILE_1 = "test_attestation_with_root_certs.pem"; + private static final String TEST_CERTIFICATE_FILE_2 = "test_attestation_wrong_root_certs.pem"; + private static final String TEST_REMOTE_REVOCATION_LIST_FILE_NAME = + "test_remote_revocation_list.json"; + private static final String REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST = + "test_revocation_list_no_test_certs.json"; + private static final String REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST = + "test_revocation_list_with_test_certs.json"; + private static final String TEST_STORED_REVOCATION_LIST_FILE_NAME = + "test_stored_revocation_list.json"; + private static final String FILE_URL_PREFIX = "file://"; + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + private CertificateFactory mFactory; + private List<X509Certificate> mCertificates1; + private List<X509Certificate> mCertificates2; + private File mRemoteRevocationListFile; + private String mRevocationListUrl; + private String mNonExistentRevocationListUrl; + private File mStoredRevocationListFile; + private CertificateRevocationStatusManager mCertificateRevocationStatusManager; + + @Before + public void setUp() throws Exception { + mFactory = CertificateFactory.getInstance("X.509"); + mCertificates1 = getCertificateChain(TEST_CERTIFICATE_FILE_1); + mCertificates2 = getCertificateChain(TEST_CERTIFICATE_FILE_2); + mRemoteRevocationListFile = + new File(mContext.getFilesDir(), TEST_REMOTE_REVOCATION_LIST_FILE_NAME); + mRevocationListUrl = FILE_URL_PREFIX + mRemoteRevocationListFile.getAbsolutePath(); + File noSuchFile = new File(mContext.getFilesDir(), "file_does_not_exist"); + mNonExistentRevocationListUrl = FILE_URL_PREFIX + noSuchFile.getAbsolutePath(); + mStoredRevocationListFile = + new File(mContext.getFilesDir(), TEST_STORED_REVOCATION_LIST_FILE_NAME); + } + + @After + public void tearDown() throws Exception { + mRemoteRevocationListFile.delete(); + mStoredRevocationListFile.delete(); + } + + @Test + public void checkRevocationStatus_doesNotExistOnRemoteRevocationList_noException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_existsOnRemoteRevocationList_throwsException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void + checkRevocationStatus_cannotReachRemoteRevocationList_noStoredStatus_throwsException() + throws Exception { + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mStoredRevocationListFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_savesRevocationStatus() throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + + assertThat(mStoredRevocationListFile.length()).isGreaterThan(0); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_listSaved_noException() + throws Exception { + // call checkRevocationStatus once to save the revocation status + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // call checkRevocationStatus again with mNonExistentRevocationListUrl + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mStoredRevocationListFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_storedListTooOld_exception() + throws Exception { + // call checkRevocationStatus once to save the revocation status + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // set the last modified date of the stored list to an expired date + LocalDateTime now = LocalDateTime.now(); + LocalDateTime expiredListDate = + now.minusDays( + CertificateRevocationStatusManager.MAX_OFFLINE_REVOCATION_LIST_AGE_DAYS + + 1); + mStoredRevocationListFile.setLastModified( + expiredListDate.toEpochSecond(OffsetDateTime.now().getOffset()) * 1000); + // call checkRevocationStatus again with mNonExistentRevocationListUrl + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mStoredRevocationListFile, false); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_cannotReachRemoteList_storedListIsFresh_noException() + throws Exception { + // call checkRevocationStatus once to save the revocation status + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // set the last modified date of the stored list to a barely not expired date + LocalDateTime now = LocalDateTime.now(); + LocalDateTime barelyFreshDate = + now.minusDays( + CertificateRevocationStatusManager.MAX_OFFLINE_REVOCATION_LIST_AGE_DAYS + - 1); + mStoredRevocationListFile.setLastModified( + barelyFreshDate.toEpochSecond(OffsetDateTime.now().getOffset()) * 1000); + // call checkRevocationStatus again with mNonExistentRevocationListUrl + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mNonExistentRevocationListUrl, mStoredRevocationListFile, false); + + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void silentlyStoreRevocationList_storesCorrectly() throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + byte[] revocationList = + mCertificateRevocationStatusManager.fetchRemoteRevocationListBytes(); + + mCertificateRevocationStatusManager.silentlyStoreRevocationList(revocationList); + + byte[] bytesFromRemoteList; + byte[] bytesFromStoredList; + try (FileInputStream remoteListInputStream = + new FileInputStream(mRemoteRevocationListFile)) { + bytesFromRemoteList = remoteListInputStream.readAllBytes(); + } + try (FileInputStream storedListInputStream = + new FileInputStream(mStoredRevocationListFile)) { + bytesFromStoredList = storedListInputStream.readAllBytes(); + } + assertThat(bytesFromStoredList).isEqualTo(bytesFromRemoteList); + } + + @Test + public void checkRevocationStatus_recentlyChecked_doesNotFetchRemoteCrl() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // indirectly verifies the remote list is not fetched by simulating a remote revocation + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + + // no exception + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_recentlyCheckedAndRevoked_exception() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + @Test + public void checkRevocationStatus_barelyRecentlyChecked_doesNotFetchRemoteCrl() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // set the last modified date of the stored list to a barely recent date + LocalDateTime now = LocalDateTime.now(); + LocalDateTime barelyRecentDate = + now.minusHours(CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_FETCH - 1); + mStoredRevocationListFile.setLastModified( + barelyRecentDate.toEpochSecond(OffsetDateTime.now().getOffset()) * 1000); + // indirectly verifies the remote list is not fetched by simulating a remote revocation + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + + // Indirectly verify the remote CRL is not checked by checking there is no exception despite + // a certificate being revoked. This test differs from the next only in the stored list last + // modified date, one before the NUM_HOURS_BEFORE_NEXT_FETCH cutoff and one after + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_certificatesRevokedAfterCheck_throwsException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mStoredRevocationListFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // set the last modified date of the stored list to a barely not recent date + LocalDateTime now = LocalDateTime.now(); + LocalDateTime barelyNotRecentDate = + now.minusHours(CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_FETCH + 1); + mStoredRevocationListFile.setLastModified( + barelyNotRecentDate.toEpochSecond(OffsetDateTime.now().getOffset()) * 1000); + // simulate a remote revocation + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + + private List<X509Certificate> getCertificateChain(String fileName) throws Exception { + Collection<? extends Certificate> certificates = + mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName)); + ArrayList<X509Certificate> x509Certs = new ArrayList<>(); + for (Certificate cert : certificates) { + x509Certs.add((X509Certificate) cert); + } + return x509Certs; + } + + private void copyFromAssetToFile(String assetFileName, File targetFile) throws Exception { + byte[] data; + try (InputStream in = mContext.getResources().getAssets().open(assetFileName)) { + data = in.readAllBytes(); + } + try (FileOutputStream fileOutputStream = new FileOutputStream(targetFile)) { + fileOutputStream.write(data); + } + } +} diff --git a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java index 30cc002b4144..779676e4f979 100644 --- a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java +++ b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java @@ -159,41 +159,36 @@ public class BatteryUsageStatsPerfTest { private static BatteryUsageStats buildBatteryUsageStats() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, false, false, 0) + new BatteryUsageStats.Builder(new String[]{"FOO"}, false, false, false, 0) .setBatteryCapacity(4000) - .setDischargePercentage(20) - .setDischargedPowerRange(1000, 2000) + .addDischargePercentage(20) + .addDischargedPowerRange(1000, 2000) .setStatsStartTimestamp(1000) .setStatsEndTimestamp(3000); builder.getAggregateBatteryConsumerBuilder( BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS) - .setConsumedPower(123) - .setConsumedPower( - BatteryConsumer.POWER_COMPONENT_CPU, 10100) - .setConsumedPower( - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10200) - .setUsageDurationMillis( - BatteryConsumer.POWER_COMPONENT_CPU, 10300) - .setUsageDurationMillis( - BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10400); + .addConsumedPower(123) + .addConsumedPower(BatteryConsumer.POWER_COMPONENT_CPU, 10100) + .addConsumedPower(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10200) + .addUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_CPU, 10300) + .addUsageDurationMillis(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 10400); for (int i = 0; i < 1000; i++) { final UidBatteryConsumer.Builder consumerBuilder = builder.getOrCreateUidBatteryConsumerBuilder(i) .setPackageWithHighestDrain("example.packagename" + i) - .setTimeInStateMs(UidBatteryConsumer.STATE_FOREGROUND, i * 2000) - .setTimeInStateMs(UidBatteryConsumer.STATE_BACKGROUND, i * 1000); + .setTimeInProcessStateMs(UidBatteryConsumer.STATE_FOREGROUND, i * 2000) + .setTimeInProcessStateMs(UidBatteryConsumer.STATE_BACKGROUND, i * 1000); for (int componentId = 0; componentId < BatteryConsumer.POWER_COMPONENT_COUNT; componentId++) { - consumerBuilder.setConsumedPower(componentId, componentId * 123.0, - BatteryConsumer.POWER_MODEL_POWER_PROFILE); - consumerBuilder.setUsageDurationMillis(componentId, componentId * 1000); + consumerBuilder.addConsumedPower(componentId, componentId * 123.0); + consumerBuilder.addUsageDurationMillis(componentId, componentId * 1000); } consumerBuilder - .setConsumedPower(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 1234) - .setUsageDurationMillis(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 4321); + .addConsumedPower(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 1234) + .addUsageDurationMillis(BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID, 4321); } return builder.build(); } diff --git a/tests/BinaryTransparencyHostTest/Android.bp b/tests/BinaryTransparencyHostTest/Android.bp index e14e5fea001f..1c8386add1b1 100644 --- a/tests/BinaryTransparencyHostTest/Android.bp +++ b/tests/BinaryTransparencyHostTest/Android.bp @@ -31,6 +31,8 @@ java_test_host { ], static_libs: [ "truth", + "flag-junit-host", + "android.app.flags-aconfig-java-host", ], device_common_data: [ ":BinaryTransparencyTestApp", diff --git a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java index 6e5f08a11ed8..6d8dbcb5c963 100644 --- a/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java +++ b/tests/BinaryTransparencyHostTest/src/android/transparency/test/BinaryTransparencyHostTest.java @@ -24,6 +24,9 @@ import static org.junit.Assert.fail; import android.platform.test.annotations.LargeTest; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.host.HostFlagsValueProvider; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.log.LogUtil.CLog; @@ -34,6 +37,7 @@ import com.android.tradefed.util.CommandResult; import com.android.tradefed.util.CommandStatus; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,6 +53,10 @@ public final class BinaryTransparencyHostTest extends BaseHostJUnit4Test { /** Waiting time for the job to be scheduled */ private static final int JOB_CREATION_MAX_SECONDS = 30; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + HostFlagsValueProvider.createCheckFlagsRule(this::getDevice); + @Before public void setUp() throws Exception { cancelPendingJob(); @@ -123,6 +131,7 @@ public final class BinaryTransparencyHostTest extends BaseHostJUnit4Test { } } + @RequiresFlagsDisabled(android.app.Flags.FLAG_BACKGROUND_INSTALL_CONTROL_CALLBACK_API) @Test public void testPreloadUpdateTriggersJobScheduling() throws Exception { try { 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/CtsSurfaceControlTestsStaging/AndroidManifest.xml b/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml index d8eb9ff37e78..da510fcd4d63 100644 --- a/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml +++ b/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml @@ -18,12 +18,20 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.view.surfacecontroltests"> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER"/> + <uses-permission android:name="android.permission.OBSERVE_PICTURE_PROFILES"/> + <application android:debuggable="true" android:testOnly="true"> <uses-library android:name="android.test.runner"/> <activity android:name=".GraphicsActivity" android:exported="false"> </activity> + <activity android:name=".SurfaceControlPictureProfileTestActivity" + android:exported="true" + android:turnScreenOn="true" + android:showWhenLocked="true" + android:theme="@android:style/Theme.NoTitleBar.Fullscreen" /> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml b/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml index 5c0163fcfa7e..025bf378d82f 100644 --- a/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml +++ b/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml @@ -21,6 +21,9 @@ <option name="install-arg" value="-t" /> <option name="test-file-name" value="CtsSurfaceControlTestsStaging.apk" /> </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer" > + <option name="flag-value" value="media_tv/android.media.tv.flags.apply_picture_profiles=true" /> + </target_preparer> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="android.view.surfacecontroltests" /> <option name="hidden-api-checks" value="false" /> diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java index 700856c50bae..14c8de8db5fc 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java @@ -819,7 +819,7 @@ public class GraphicsActivity extends Activity { private List<Float> getExpectedFrameRateForCompatibility(int compatibility) { assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED for compatibility " + compatibility, - compatibility == Surface.FRAME_RATE_COMPATIBILITY_GTE); + compatibility == Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST); Display display = getDisplay(); List<Float> expectedFrameRates = getRefreshRates(display.getMode(), display) diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java new file mode 100644 index 000000000000..9ac08ed449fd --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java @@ -0,0 +1,258 @@ +/* + * 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.view.surfacecontroltests; + +import static android.Manifest.permission.OBSERVE_PICTURE_PROFILES; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assume.assumeTrue; + +import static java.util.Arrays.stream; + +import android.hardware.HardwareBuffer; +import android.media.quality.PictureProfileHandle; +import android.os.Process; +import android.view.SurfaceControl; +import android.view.SurfaceControlActivePicture; +import android.view.SurfaceControlActivePictureListener; +import android.view.SurfaceView; + +import androidx.test.filters.SmallTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.stream.LongStream; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SurfaceControlPictureProfileTest { + private static final String TAG = SurfaceControlPictureProfileTest.class.getSimpleName(); + + private SurfaceControl[] mSurfaceControls; + private SurfaceControl mSurfaceControl; + + @Rule + public ActivityTestRule<SurfaceControlPictureProfileTestActivity> mActivityRule = + new ActivityTestRule<>(SurfaceControlPictureProfileTestActivity.class); + + @Before + public void setup() { + SurfaceView[] surfaceViews = mActivityRule.getActivity().getSurfaceViews(); + mSurfaceControls = new SurfaceControl[surfaceViews.length]; + // Create a child surface control so we can set a buffer, priority and profile handle all + // on one single surface control + for (int i = 0; i < mSurfaceControls.length; ++i) { + mSurfaceControls[i] = new SurfaceControl.Builder().setName("test").setHidden(false) + .setParent(surfaceViews[i].getSurfaceControl()).build(); + } + mSurfaceControl = mSurfaceControls[0]; + } + + @Test + public void whenPictureProfileApplied_noExecptionsThrown() { + assumeTrue("Skipping test because feature flag is disabled", + com.android.graphics.libgui.flags.Flags.applyPictureProfiles()); + // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead + assumeTrue("Skipping test because no picture profile support", + SurfaceControl.getMaxPictureProfiles() > 0); + + // TODO(b/337330263): Load the handle from MediaQualityManager instead + PictureProfileHandle handle = new PictureProfileHandle(1); + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction() + .setBuffer(mSurfaceControl, buffer) + .setPictureProfileHandle(mSurfaceControl, handle) + .apply(); + } + + @Test + public void whenStartsListening_callsListener() { + assumeTrue("Skipping test because feature flag is disabled", + com.android.graphics.libgui.flags.Flags.applyPictureProfiles()); + // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead + assumeTrue("Skipping test because no picture profile support", + SurfaceControl.getMaxPictureProfiles() > 0); + + BlockingQueue<SurfaceControlActivePicture[]> picturesQueue = new LinkedBlockingQueue<>(); + SurfaceControlActivePicture[] pictures; + SurfaceControlActivePictureListener listener = new SurfaceControlActivePictureListener() { + @Override + public void onActivePicturesChanged(SurfaceControlActivePicture[] pictures) { + picturesQueue.add(pictures); + } + }; + // TODO(b/337330263): Call MediaQualityManager.addActivePictureListener instead + adoptShellPermissionIdentity(OBSERVE_PICTURE_PROFILES); + listener.startListening(); + { + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction().setBuffer(mSurfaceControl, buffer).apply(); + } + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(pictures).isEmpty(); + } + + @Test + public void whenPictureProfileApplied_callsListenerWithUidAndProfileId() { + assumeTrue("Skipping test because feature flag is disabled", + com.android.graphics.libgui.flags.Flags.applyPictureProfiles()); + // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead + assumeTrue("Skipping test because no picture profile support", + SurfaceControl.getMaxPictureProfiles() > 0); + + BlockingQueue<SurfaceControlActivePicture[]> picturesQueue = new LinkedBlockingQueue<>(); + SurfaceControlActivePicture[] pictures; + SurfaceControlActivePictureListener listener = new SurfaceControlActivePictureListener() { + @Override + public void onActivePicturesChanged(SurfaceControlActivePicture[] pictures) { + picturesQueue.add(pictures); + } + }; + // TODO(b/337330263): Call MediaQualityManager.addActivePictureListener instead + adoptShellPermissionIdentity(OBSERVE_PICTURE_PROFILES); + listener.startListening(); + { + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction().setBuffer(mSurfaceControl, buffer).apply(); + } + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(pictures).isEmpty(); + + // TODO(b/337330263): Load the handle from MediaQualityManager instead + PictureProfileHandle handle = new PictureProfileHandle(1); + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction() + .setBuffer(mSurfaceControl, buffer) + .setPictureProfileHandle(mSurfaceControl, handle) + .apply(); + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId())) + .containsExactly(handle.getId()); + assertThat(stream(pictures).map(picture -> picture.getOwnerUid())) + .containsExactly(Process.myUid()); + } + + @Test + public void whenPriorityChanges_callsListenerOnlyForLowerPriorityLayers() { + assumeTrue("Skipping test because feature flag is disabled", + com.android.graphics.libgui.flags.Flags.applyPictureProfiles()); + // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead + int maxPictureProfiles = SurfaceControl.getMaxPictureProfiles(); + assumeTrue("Skipping test because no picture profile support", maxPictureProfiles > 0); + + BlockingQueue<SurfaceControlActivePicture[]> picturesQueue = new LinkedBlockingQueue<>(); + SurfaceControlActivePicture[] pictures; + SurfaceControlActivePictureListener listener = new SurfaceControlActivePictureListener() { + @Override + public void onActivePicturesChanged(SurfaceControlActivePicture[] pictures) { + picturesQueue.add(pictures); + } + }; + // TODO(b/337330263): Call MediaQualityManager.addActivePictureListener instead + adoptShellPermissionIdentity(OBSERVE_PICTURE_PROFILES); + listener.startListening(); + { + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction().setBuffer(mSurfaceControl, buffer).apply(); + } + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(pictures).isEmpty(); + + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + // Use one more picture profile than allowed + for (int i = 0; i <= maxPictureProfiles; ++i) { + // Increase the number of surface views as necessary to support device configuration. + assertThat(i).isLessThan(mSurfaceControls.length); + + // TODO(b/337330263): Load the handle from MediaQualityManager instead + PictureProfileHandle handle = new PictureProfileHandle(i + 1); + HardwareBuffer buffer = getSolidBuffer(100, 100); + transaction + .setBuffer(mSurfaceControls[i], buffer) + .setPictureProfileHandle(mSurfaceControls[i], handle) + .setContentPriority(mSurfaceControls[i], 1); + } + transaction.setContentPriority(mSurfaceControls[0], -1); + transaction.setContentPriority(mSurfaceControls[maxPictureProfiles], 0); + transaction.apply(); + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(stream(pictures).map(picture -> picture.getLayerId())) + .containsNoDuplicates(); + // Expect all but the first layer to be listed as an active picture + assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId())) + .containsExactlyElementsIn(toIterableRange(2, maxPictureProfiles + 1)); + + // Elevate priority for the first layer and verify it gets to use a profile + new SurfaceControl.Transaction().setContentPriority(mSurfaceControls[0], 2).apply(); + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + // Expect all but the last layer to be listed as an active picture + assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId())) + .containsExactlyElementsIn(toIterableRange(1, maxPictureProfiles)); + } + + private static SurfaceControlActivePicture[] pollMs( + BlockingQueue<SurfaceControlActivePicture[]> picturesQueue, int waitMs) { + SurfaceControlActivePicture[] pictures = null; + long nowMs = System.currentTimeMillis(); + long endTimeMs = nowMs + waitMs; + while (nowMs < endTimeMs && pictures == null) { + try { + pictures = picturesQueue.poll(endTimeMs - nowMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // continue polling until timeout when interrupted + } + nowMs = System.currentTimeMillis(); + } + return pictures; + } + + Iterable<Long> toIterableRange(int start, int stop) { + return () -> LongStream.rangeClosed(start, stop).iterator(); + } + + private void adoptShellPermissionIdentity(String permission) { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(permission); + } + + private HardwareBuffer getSolidBuffer(int width, int height) { + // We can assume that RGBA_8888 format is supported for every platform. + return HardwareBuffer.create( + width, height, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_CPU_WRITE_OFTEN); + } +} diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTestActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTestActivity.java new file mode 100644 index 000000000000..42fcb261fa9d --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTestActivity.java @@ -0,0 +1,43 @@ +/* + * 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.view.surfacecontroltests; + +import android.app.Activity; +import android.os.Bundle; +import android.view.SurfaceView; + +public class SurfaceControlPictureProfileTestActivity extends Activity { + private SurfaceView[] mSurfaceViews; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.picture_profile_test_layout); + mSurfaceViews = new SurfaceView[3]; + mSurfaceViews[0] = (SurfaceView) findViewById(R.id.surfaceview1); + mSurfaceViews[1] = (SurfaceView) findViewById(R.id.surfaceview2); + mSurfaceViews[2] = (SurfaceView) findViewById(R.id.surfaceview3); + } + + public SurfaceView getSurfaceView() { + return mSurfaceViews[0]; + } + + public SurfaceView[] getSurfaceViews() { + return mSurfaceViews; + } +} diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java index 4d4827676c74..f1d4dc6b8faf 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java @@ -85,7 +85,8 @@ public class SurfaceControlTest { @Test public void testSurfaceControlFrameRateCompatibilityGte() throws InterruptedException { GraphicsActivity activity = mActivityRule.getActivity(); - activity.testSurfaceControlFrameRateCompatibility(Surface.FRAME_RATE_COMPATIBILITY_GTE); + activity.testSurfaceControlFrameRateCompatibility( + Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST); } @Test diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/picture_profile_test_layout.xml b/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/picture_profile_test_layout.xml new file mode 100644 index 000000000000..9aa25785d9f2 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/picture_profile_test_layout.xml @@ -0,0 +1,32 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <SurfaceView android:id="@+id/surfaceview1" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1"/> + <SurfaceView android:id="@+id/surfaceview2" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1"/> + <SurfaceView android:id="@+id/surfaceview3" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1"/> +</LinearLayout> diff --git a/tests/FlickerTests/ActivityEmbedding/Android.bp b/tests/FlickerTests/ActivityEmbedding/Android.bp index c681ce96a269..529f84ac4e90 100644 --- a/tests/FlickerTests/ActivityEmbedding/Android.bp +++ b/tests/FlickerTests/ActivityEmbedding/Android.bp @@ -24,71 +24,6 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -//////////////////////////////////////////////////////////////////////////////// -// Begin to cleanup after CL merges - -filegroup { - name: "FlickerTestsOtherCommon-src", - srcs: ["src/**/ActivityEmbeddingTestBase.kt"], -} - -filegroup { - name: "FlickerTestsOtherOpen-src", - srcs: ["src/**/open/*"], -} - -filegroup { - name: "FlickerTestsOtherRotation-src", - srcs: ["src/**/rotation/*"], -} - -java_library { - name: "FlickerTestsOtherCommon", - defaults: ["FlickerTestsDefault"], - srcs: [":FlickerTestsOtherCommon-src"], - static_libs: ["FlickerTestsBase"], -} - -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/*"], -} - -android_test { - name: "FlickerTestsOtherOpen", - defaults: ["FlickerTestsOtherDefaults"], - srcs: [":FlickerTestsOtherOpen-src"], -} - -android_test { - name: "FlickerTestsOtherRotation", - defaults: ["FlickerTestsOtherDefaults"], - srcs: [":FlickerTestsOtherRotation-src"], -} - -android_test { - name: "FlickerTestsOther", - defaults: ["FlickerTestsOtherDefaults"], - srcs: ["src/**/*"], - exclude_srcs: [ - ":FlickerTestsOtherOpen-src", - ":FlickerTestsOtherRotation-src", - ":FlickerTestsOtherCommon-src", - ], -} - -// End to cleanup after CL merges -//////////////////////////////////////////////////////////////////////////////// - android_test { name: "FlickerTestsActivityEmbedding", defaults: ["FlickerTestsDefault"], @@ -97,10 +32,7 @@ android_test { instrumentation_target_package: "com.android.server.wm.flicker", test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsOtherCommon", - ], + static_libs: ["FlickerTestsBase"], data: ["trace_config/*"], } diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml index 82de070921f0..685ae9a5fef2 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 --> @@ -41,6 +45,8 @@ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <!-- Disable AOD --> + <option name="run-command" value="settings put secure doze_always_on 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> 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..75bd5d157bb2 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 @@ -17,7 +17,6 @@ package com.android.server.wm.flicker.activityembedding.open import android.graphics.Rect -import android.platform.test.annotations.Presubmit import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest @@ -39,7 +38,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) @@ -68,13 +67,21 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest } } - @Ignore("Not applicable to this CUJ.") override fun navBarWindowIsVisibleAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun navBarWindowIsVisibleAtStartAndEnd() {} - @FlakyTest(bugId = 291575593) override fun entireScreenCovered() {} + @FlakyTest(bugId = 291575593) + @Test + override fun entireScreenCovered() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarWindowIsAlwaysVisible() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarWindowIsAlwaysVisible() {} - @Ignore("Not applicable to this CUJ.") override fun statusBarLayerPositionAtStartAndEnd() {} + @Ignore("Not applicable to this CUJ.") + @Test + override fun statusBarLayerPositionAtStartAndEnd() {} /** Transition begins with a split. */ @FlakyTest(bugId = 286952194) @@ -122,7 +129,6 @@ class MainActivityStartsSecondaryWithAlwaysExpandTest(flicker: LegacyFlickerTest /** Always expand activity is on top of the split. */ @FlakyTest(bugId = 286952194) - @Presubmit @Test fun endsWithAlwaysExpandActivityOnTop() { flicker.assertWmEnd { 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/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/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 c3e1a1fa2e18..e41364595648 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 @@ -47,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) @@ -176,12 +176,15 @@ class EnterSystemSplitTest(flicker: LegacyFlickerTest) : ActivityEmbeddingTestBa } @Ignore("Not applicable to this CUJ.") + @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() {} @FlakyTest(bugId = 342596801) + @Test override fun entireScreenCovered() = super.entireScreenCovered() @FlakyTest(bugId = 342596801) + @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() 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/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml index 4ffb11ab92ae..5f92d7fe830b 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 --> @@ -41,6 +45,8 @@ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <!-- Disable AOD --> + <option name="run-command" value="settings put secure doze_always_on 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> 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 b61739f100ab..17d0f967b1bd 100644 --- a/tests/FlickerTests/AppLaunch/Android.bp +++ b/tests/FlickerTests/AppLaunch/Android.bp @@ -24,69 +24,13 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -//////////////////////////////////////////////////////////////////////////////// -// Begin to cleanup after CL merges - -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", - defaults: ["FlickerTestsDefault"], - manifest: "AndroidManifest.xml", - test_config_template: "AndroidTestTemplate.xml", - srcs: [":FlickerTestsAppLaunch1-src"], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsAppLaunchCommon", - ], - 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", - ], - data: ["trace_config/*"], -} - -// End to cleanup after CL merges -//////////////////////////////////////////////////////////////////////////////// - android_test { name: "FlickerTestsAppLaunch", defaults: ["FlickerTestsDefault"], manifest: "AndroidManifest.xml", test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsAppLaunchCommon", - ], + static_libs: ["FlickerTestsBase"], data: ["trace_config/*"], } diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml index 0fa4d07b2eca..1b90e99a8ba2 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 --> @@ -41,6 +45,8 @@ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <!-- Disable AOD --> + <option name="run-command" value="settings put secure doze_always_on 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> 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..e59b6bd0617c 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:ActivityTransitionTest` * * 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..2bf8cc40d0a0 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:OpenAppFromIconColdTest` * * 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..9c6bf9de37ce 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:OpenAppFromIntentColdAfterCameraTest` * * 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..1a53a611c8d7 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:OpenAppFromIntentColdTest` * * 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..14b6a18bfe2e 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:OpenAppFromIntentWarmTest` * * 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 6e6a3275191f..f30fe96b9d05 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:OpenAppFromLockscreenViaIntentTest` * * Actions: * ``` 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..9c552eb478d4 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:OpenCameraFromHomeOnDoubleClickPowerButtonTest` * * Actions: * ``` @@ -140,14 +140,8 @@ class OpenCameraFromHomeOnDoubleClickPowerButtonTest(flicker: LegacyFlickerTest) @Postsubmit @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() { - flicker.assertLayers { - this.visibleLayersShownMoreThanOneConsecutiveEntry( - LayersTraceSubject.VISIBLE_FOR_MORE_THAN_ONE_ENTRY_IGNORE_LAYERS + - listOf(CAMERA_BACKGROUND) - ) - } - } + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() @Postsubmit @Test @@ -170,12 +164,5 @@ class OpenCameraFromHomeOnDoubleClickPowerButtonTest(flicker: LegacyFlickerTest) @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams() = LegacyFlickerTestFactory.nonRotationTests() - - private val CAMERA_BACKGROUND = - ComponentNameMatcher( - "Background for SurfaceView" + - "[com.google.android.GoogleCamera/" + - "com.google.android.apps.camera.legacy.app.activity.main.CameraActivity]" - ) } } 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/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml index 4d9fefbc7d88..ffdbb02984a7 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 --> @@ -41,6 +45,8 @@ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <!-- Disable AOD --> + <option name="run-command" value="settings put secure doze_always_on 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/flicker/LaunchTaskPortrait.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/flicker/LaunchTaskPortrait.kt new file mode 100644 index 000000000000..c82ce8a4cb1d --- /dev/null +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/flicker/LaunchTaskPortrait.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.transitions.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.server.wm.flicker.service.transitions.scenarios.LaunchTask +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class LaunchTaskPortrait : LaunchTask(Rotation.ROTATION_0) { + @ExpectedScenarios(["TASK_TRANSITION_SCENARIO", "OPEN_NEW_TASK_APP_SCENARIO"]) + @Test + override fun openNewTask() = super.openNewTask() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(TASK_TRANSITION_SCENARIO) + .use(OPEN_NEW_TASK_APP_SCENARIO) + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/scenarios/LaunchTask.kt b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/scenarios/LaunchTask.kt new file mode 100644 index 000000000000..147477d728be --- /dev/null +++ b/tests/FlickerTests/FlickerService/src/com/android/server/wm/flicker/service/transitions/scenarios/LaunchTask.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.transitions.scenarios + +import android.app.Instrumentation +import android.tools.Rotation +import android.tools.flicker.AssertionInvocationGroup +import android.tools.flicker.assertors.assertions.AppWindowCoversFullScreenAtStart +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd +import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart +import android.tools.flicker.assertors.assertions.BackgroundShowsInTransition +import android.tools.flicker.assertors.assertions.LayerBecomesInvisible +import android.tools.flicker.assertors.assertions.LayerBecomesVisible +import android.tools.flicker.assertors.assertions.LayerIsNeverVisible +import android.tools.flicker.assertors.assertions.AppWindowIsNeverVisible +import android.tools.flicker.config.AssertionTemplates +import android.tools.flicker.config.FlickerConfigEntry +import android.tools.flicker.config.ScenarioId +import android.tools.flicker.config.appclose.Components.CLOSING_APPS +import android.tools.flicker.config.appclose.Components.CLOSING_CHANGES +import android.tools.flicker.config.applaunch.Components.OPENING_CHANGES +import android.tools.flicker.config.common.Components.LAUNCHER +import android.tools.flicker.config.common.Components.WALLPAPER +import android.tools.flicker.extractors.TaggedCujTransitionMatcher +import android.tools.flicker.extractors.TaggedScenarioExtractorBuilder +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import android.tools.traces.events.CujType +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import org.junit.After +import org.junit.Before +import org.junit.Test + + +/** + * This tests performs a transition between tasks + */ +abstract class LaunchTask(val rotation: Rotation = Rotation.ROTATION_0) { + protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val tapl = LauncherInstrumentation() + + private val launchNewTaskApp = NewTasksAppHelper(instrumentation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + ChangeDisplayOrientationRule.setRotation(rotation) + tapl.setExpectedRotation(rotation.value) + launchNewTaskApp.launchViaIntent(wmHelper) + } + + @Test + open fun openNewTask() { + launchNewTaskApp.openNewTask(device, wmHelper) + } + + @After + fun tearDown() { + launchNewTaskApp.exit(wmHelper) + } + + companion object { + /** + * General task transition scenario that can be reused for any trace + */ + val TASK_TRANSITION_SCENARIO = + FlickerConfigEntry( + scenarioId = ScenarioId("TASK_TRANSITION_SCENARIO"), + extractor = TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DEFAULT_TASK_TO_TASK_ANIMATION) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = true) + ) + .build(), + assertions = listOf( + // Opening changes replace the closing ones + LayerBecomesInvisible(CLOSING_CHANGES), + AppWindowOnTopAtStart(CLOSING_CHANGES), + LayerBecomesVisible(OPENING_CHANGES), + AppWindowOnTopAtEnd(OPENING_CHANGES), + + // There is a background color and it's covering the transition area + BackgroundShowsInTransition(CLOSING_CHANGES) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) + ) + + /** + * Scenario that is making assertions that are valid for the new task app but that do not + * apply to other task transitions in general + */ + val OPEN_NEW_TASK_APP_SCENARIO = + FlickerConfigEntry( + scenarioId = ScenarioId("OPEN_NEW_TASK_APP_SCENARIO"), + extractor = TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DEFAULT_TASK_TO_TASK_ANIMATION) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = true) + ) + .build(), + assertions = AssertionTemplates.COMMON_ASSERTIONS + + listOf( + // Wallpaper and launcher never visible + LayerIsNeverVisible(WALLPAPER, mustExist = true), + LayerIsNeverVisible(LAUNCHER, mustExist = true), + AppWindowIsNeverVisible(LAUNCHER, mustExist = true), + // App window covers the display at start + AppWindowCoversFullScreenAtStart(CLOSING_APPS) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) + ) + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/IME/Android.bp b/tests/FlickerTests/IME/Android.bp index f80e6b4b2f5e..cba3d09ebefd 100644 --- a/tests/FlickerTests/IME/Android.bp +++ b/tests/FlickerTests/IME/Android.bp @@ -24,27 +24,6 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -//////////////////////////////////////////////////////////////////////////////// -// Begin to cleanup after CL merges - -filegroup { - name: "FlickerTestsImeCommon-src", - srcs: ["src/**/common/*"], -} - -filegroup { - name: "FlickerTestsIme1-src", - srcs: ["src/**/Close*"], -} - -filegroup { - name: "FlickerTestsIme2-src", - srcs: ["src/**/ShowImeOnAppStart*"], -} - -// End to cleanup after CL merges -//////////////////////////////////////////////////////////////////////////////// - android_test { name: "FlickerTestsIme", defaults: ["FlickerTestsDefault"], @@ -60,67 +39,6 @@ android_test { } //////////////////////////////////////////////////////////////////////////////// -// Begin to cleanup after CL merges - -java_library { - name: "FlickerTestsImeCommon", - defaults: ["FlickerTestsDefault"], - srcs: [":FlickerTestsImeCommon-src"], - static_libs: ["FlickerTestsBase"], -} - -android_test { - name: "FlickerTestsIme1", - defaults: ["FlickerTestsDefault"], - manifest: "AndroidManifest.xml", - test_config_template: "AndroidTestTemplate.xml", - test_suites: [ - "device-tests", - "device-platinum-tests", - ], - srcs: [":FlickerTestsIme1-src"], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsImeCommon", - ], - data: ["trace_config/*"], -} - -android_test { - name: "FlickerTestsIme2", - defaults: ["FlickerTestsDefault"], - manifest: "AndroidManifest.xml", - test_config_template: "AndroidTestTemplate.xml", - srcs: [":FlickerTestsIme2-src"], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsImeCommon", - ], - data: ["trace_config/*"], -} - -android_test { - name: "FlickerTestsIme3", - defaults: ["FlickerTestsDefault"], - manifest: "AndroidManifest.xml", - test_config_template: "AndroidTestTemplate.xml", - srcs: ["src/**/*"], - exclude_srcs: [ - ":FlickerTestsIme1-src", - ":FlickerTestsIme2-src", - ":FlickerTestsImeCommon-src", - ], - static_libs: [ - "FlickerTestsBase", - "FlickerTestsImeCommon", - ], - data: ["trace_config/*"], -} - -// End to cleanup after CL merges -//////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////////// // Begin breakdowns for FlickerTestsIme module test_module_config { diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml index b879c54dcab3..ac704e5e7c39 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 --> @@ -43,13 +47,17 @@ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <!-- Disable AOD --> + <option name="run-command" value="settings put secure doze_always_on 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> 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 522c68bba0d1..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) 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/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt index b8f11dcf8970..ad083fa428a9 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.ime -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.tools.Rotation import android.tools.flicker.junit.FlickerParametersRunnerFactory @@ -81,7 +80,6 @@ class ShowImeOnAppStartWhenLaunchingAppFromFixedOrientationTest(flicker: LegacyF } @FlakyTest(bugId = 290767483) - @Postsubmit @Test fun imeLayerAlphaOneAfterSnapshotStartingWindowRemoval() { val layerTrace = flicker.reader.readLayersTrace() ?: error("Unable to read layers trace") 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 82e53c81daaa..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) 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 eb63e4985a9f..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 @@ -39,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) diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml index 04b312a896b9..e2ac5a9579ae 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 --> @@ -41,6 +45,8 @@ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <!-- Disable AOD --> + <option name="run-command" value="settings put secure doze_always_on 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt index 8c9ab9aadb8e..def254a65945 100644 --- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt +++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt @@ -16,8 +16,6 @@ package com.android.server.wm.flicker.notification -import android.platform.test.annotations.Postsubmit -import android.platform.test.annotations.Presubmit import android.platform.test.rule.SettingOverrideRule import android.provider.Settings import android.tools.flicker.junit.FlickerParametersRunnerFactory @@ -26,6 +24,7 @@ import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.wakeUpAndGoToHomeScreen import android.tools.traces.component.ComponentNameMatcher +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import org.junit.ClassRule import org.junit.FixMethodOrder @@ -46,7 +45,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Postsubmit +@FlakyTest(bugId = 384046002) open class OpenAppFromLockscreenNotificationColdTest(flicker: LegacyFlickerTest) : OpenAppFromNotificationColdTest(flicker) { @@ -107,12 +106,10 @@ open class OpenAppFromLockscreenNotificationColdTest(flicker: LegacyFlickerTest) override fun navBarWindowIsAlwaysVisible() {} /** {@inheritDoc} */ - @Postsubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() - @Presubmit @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() { flicker.assertWm { diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt index e595100a2cbe..7da529d7650b 100644 --- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt +++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.notification -import android.platform.test.annotations.Presubmit import android.platform.test.rule.SettingOverrideRule import android.provider.Settings import android.tools.flicker.junit.FlickerParametersRunnerFactory @@ -46,6 +45,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 384046002) class OpenAppFromLockscreenNotificationWarmTest(flicker: LegacyFlickerTest) : OpenAppFromNotificationWarmTest(flicker) { @@ -72,7 +72,6 @@ class OpenAppFromLockscreenNotificationWarmTest(flicker: LegacyFlickerTest) : * window of the transition, with snapshot or splash screen windows optionally showing first. */ @Test - @Presubmit fun appWindowBecomesFirstAndOnlyTopWindow() { flicker.assertWm { this.hasNoVisibleAppWindow() @@ -87,7 +86,6 @@ class OpenAppFromLockscreenNotificationWarmTest(flicker: LegacyFlickerTest) : /** Checks that the screen is locked at the start of the transition */ @Test - @Presubmit fun screenLockedStart() { flicker.assertWmStart { isKeyguardShowing() } } @@ -117,7 +115,7 @@ class OpenAppFromLockscreenNotificationWarmTest(flicker: LegacyFlickerTest) : * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the * transition */ - @Presubmit @Test fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd() + @Test fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd() /** {@inheritDoc} */ @Test @@ -140,7 +138,6 @@ class OpenAppFromLockscreenNotificationWarmTest(flicker: LegacyFlickerTest) : override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() - @Presubmit @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() { flicker.assertWm { diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt index fbe1d34272c9..76b43b213132 100644 --- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt +++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWithOverlayAppTest.kt @@ -16,8 +16,6 @@ package com.android.server.wm.flicker.notification -import android.platform.test.annotations.Postsubmit -import android.platform.test.annotations.Presubmit import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest @@ -47,7 +45,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Postsubmit +@FlakyTest(bugId = 384046002) class OpenAppFromLockscreenNotificationWithOverlayAppTest(flicker: LegacyFlickerTest) : OpenAppFromLockscreenNotificationColdTest(flicker) { private val showWhenLockedApp = ShowWhenLockedAppHelper(instrumentation) @@ -106,7 +104,7 @@ class OpenAppFromLockscreenNotificationWithOverlayAppTest(flicker: LegacyFlicker } /** {@inheritDoc} */ - @Presubmit @Test override fun appLayerBecomesVisible() = super.appLayerBecomesVisible() + @Test override fun appLayerBecomesVisible() = super.appLayerBecomesVisible() /** {@inheritDoc} */ @FlakyTest(bugId = 227143265) @@ -120,7 +118,6 @@ class OpenAppFromLockscreenNotificationWithOverlayAppTest(flicker: LegacyFlicker override fun navBarLayerIsVisibleAtStartAndEnd() {} /** {@inheritDoc} */ - @Presubmit @Test override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt index c8ca644dde90..39302d82a5a0 100644 --- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt +++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationColdTest.kt @@ -16,14 +16,13 @@ package com.android.server.wm.flicker.notification -import android.platform.test.annotations.Postsubmit -import android.platform.test.annotations.Presubmit import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.wakeUpAndGoToHomeScreen import android.tools.traces.component.ComponentNameMatcher +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.statusBarLayerPositionAtEnd import org.junit.FixMethodOrder @@ -41,7 +40,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Postsubmit +@FlakyTest(bugId = 384046002) open class OpenAppFromNotificationColdTest(flicker: LegacyFlickerTest) : OpenAppFromNotificationWarmTest(flicker) { /** {@inheritDoc} */ @@ -59,9 +58,9 @@ open class OpenAppFromNotificationColdTest(flicker: LegacyFlickerTest) : teardown { testApp.exit(wmHelper) } } - @Presubmit @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart() + @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart() - @Presubmit @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_coldStart() + @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_coldStart() /** {@inheritDoc} */ @Test @@ -83,7 +82,7 @@ open class OpenAppFromNotificationColdTest(flicker: LegacyFlickerTest) : * Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the * transition */ - @Presubmit @Test open fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd() + @Test open fun statusBarLayerPositionAtEnd() = flicker.statusBarLayerPositionAtEnd() /** {@inheritDoc} */ @Test 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 ad70757a9a4d..f1e1b6f22d59 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 @@ -16,8 +16,6 @@ package com.android.server.wm.flicker.notification -import android.platform.test.annotations.Postsubmit -import android.platform.test.annotations.Presubmit import android.platform.test.rule.DisableNotificationCooldownSettingRule import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder @@ -28,6 +26,7 @@ import android.tools.helpers.wakeUpAndGoToHomeScreen import android.tools.traces.component.ComponentNameMatcher import android.view.WindowInsets import android.view.WindowManager +import androidx.test.filters.FlakyTest import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.NotificationAppHelper @@ -54,6 +53,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) +@FlakyTest(bugId = 384046002) open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : OpenAppTransition(flicker) { override val testApp: NotificationAppHelper = NotificationAppHelper(instrumentation) @@ -120,23 +120,20 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : // Wait for the app to launch wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify() } - @Presubmit @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_warmStart() + @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_warmStart() - @Presubmit @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_warmStart() + @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_warmStart() - @Presubmit @Test open fun notificationAppWindowVisibleAtEnd() { flicker.assertWmEnd { this.isAppWindowVisible(testApp) } } - @Presubmit @Test open fun notificationAppWindowOnTopAtEnd() { flicker.assertWmEnd { this.isAppWindowOnTop(testApp) } } - @Presubmit @Test open fun notificationAppLayerVisibleAtEnd() { flicker.assertLayersEnd { this.isVisible(testApp) } @@ -148,7 +145,6 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : * * Note: Large screen only */ - @Presubmit @Test open fun taskBarWindowIsVisibleAtEnd() { Assume.assumeTrue(usesTaskbar) @@ -160,7 +156,6 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : * * Note: Large screen only */ - @Presubmit @Test open fun taskBarLayerIsVisibleAtEnd() { Assume.assumeTrue(usesTaskbar) @@ -168,7 +163,6 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : } /** Checks the position of the [ComponentNameMatcher.NAV_BAR] at the end of the transition */ - @Presubmit @Test open fun navBarLayerPositionAtEnd() { Assume.assumeFalse(usesTaskbar) @@ -176,14 +170,12 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : } /** {@inheritDoc} */ - @Presubmit @Test open fun navBarLayerIsVisibleAtEnd() { Assume.assumeFalse(usesTaskbar) flicker.navBarLayerIsVisibleAtEnd() } - @Presubmit @Test open fun navBarWindowIsVisibleAtEnd() { Assume.assumeFalse(usesTaskbar) @@ -197,7 +189,6 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : /** {@inheritDoc} */ @Test - @Postsubmit override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() companion object { diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt index 4ba444b0815a..e825af910a38 100644 --- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt +++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppTransition.kt @@ -16,10 +16,10 @@ package com.android.server.wm.flicker.notification -import android.platform.test.annotations.Presubmit import android.tools.device.apphelpers.StandardAppHelper import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.traces.component.ComponentNameMatcher +import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.SimpleAppHelper import org.junit.Test @@ -41,7 +41,7 @@ abstract class OpenAppTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) * Checks that the [testApp] layer doesn't exist or is invisible at the start of the transition, * but is created and/or becomes visible during the transition. */ - @Presubmit + @FlakyTest(bugId = 384046002) @Test open fun appLayerBecomesVisible() { appLayerBecomesVisible_coldStart() @@ -80,7 +80,7 @@ abstract class OpenAppTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) * The `isAppWindowInvisible` step is optional because we log once per frame, upon logging, the * window may be visible or not depending on what was processed until that moment. */ - @Presubmit @Test open fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart() + @FlakyTest(bugId = 384046002) @Test open fun appWindowBecomesVisible() = appWindowBecomesVisible_coldStart() protected fun appWindowBecomesVisible_coldStart() { flicker.assertWm { @@ -108,7 +108,7 @@ abstract class OpenAppTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) * Checks that [testApp] window is not on top at the start of the transition, and then becomes * the top visible window until the end of the transition. */ - @Presubmit + @FlakyTest(bugId = 384046002) @Test open fun appWindowBecomesTopWindow() { flicker.assertWm { @@ -124,7 +124,7 @@ abstract class OpenAppTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) * Checks that [testApp] window is not on top at the start of the transition, and then becomes * the top visible window until the end of the transition. */ - @Presubmit + @FlakyTest(bugId = 384046002) @Test open fun appWindowIsTopWindowAtEnd() { flicker.assertWmEnd { this.isAppWindowOnTop(testApp) } diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml index 8acdabc2337d..1a4feb6e9eca 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 --> @@ -41,6 +45,8 @@ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <!-- Disable AOD --> + <option name="run-command" value="settings put secure doze_always_on 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> 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/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml index 91ece214aad5..1b2007deae27 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 --> @@ -41,13 +45,17 @@ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> <option name="test-user-token" value="%TEST_USER%"/> <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> + <!-- Disable AOD --> + <option name="run-command" value="settings put secure doze_always_on 0"/> <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> 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 851ce022bd81..eebe49de0010 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt @@ -23,6 +23,7 @@ import android.tools.flicker.junit.FlickerBuilderProvider import android.tools.flicker.legacy.FlickerBuilder import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.traces.component.ComponentNameMatcher +import android.tools.traces.executeShellCommand import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.android.launcher3.tapl.LauncherInstrumentation @@ -45,6 +46,9 @@ constructor( ) { init { tapl.setExpectedRotationCheckEnabled(true) + executeShellCommand( + "settings put system hide_rotation_lock_toggle_for_accessibility 1" + ) } private val logTag = this::class.java.simpleName 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/BottomHalfPipAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/BottomHalfPipAppHelper.kt new file mode 100644 index 000000000000..7b8b43af18da --- /dev/null +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/BottomHalfPipAppHelper.kt @@ -0,0 +1,63 @@ +/* + * 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.content.Intent +import android.tools.traces.parsers.WindowManagerStateHelper +import android.tools.traces.parsers.toFlickerComponent +import com.android.server.wm.flicker.testapp.ActivityOptions.BottomHalfPip + +class BottomHalfPipAppHelper( + instrumentation: Instrumentation, + private val useLaunchingActivity: Boolean = false, + private val fillTaskOnCreate: Boolean = false, +) : PipAppHelper( + instrumentation, + appName = BottomHalfPip.LABEL, + componentNameMatcher = BottomHalfPip.COMPONENT.toFlickerComponent() +) { + override val openAppIntent: Intent + get() = super.openAppIntent.apply { + component = if (useLaunchingActivity) { + BottomHalfPip.LAUNCHING_APP_COMPONENT + } else { + BottomHalfPip.COMPONENT + } + if (fillTaskOnCreate) { + putExtra(BottomHalfPip.EXTRA_BOTTOM_HALF_LAYOUT, false.toString()) + } + } + + override fun exitPipToOriginalTaskViaIntent(wmHelper: WindowManagerStateHelper) { + launchViaIntent( + wmHelper, + Intent().apply { + component = BottomHalfPip.COMPONENT + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + } + + fun toggleBottomHalfLayout() { + clickObject(TOGGLE_BOTTOM_HALF_LAYOUT_ID) + } + + companion object { + private const val TOGGLE_BOTTOM_HALF_LAYOUT_ID = "toggle_bottom_half_layout" + } +}
\ No newline at end of file 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 c77413b6a55a..55d6fd9b4a73 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,18 +16,26 @@ package com.android.server.wm.flicker.helpers +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 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.platform.uiautomatorhelpers.DeviceHelpers +import android.tools.PlatformConsts 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.KeyEvent.KEYCODE_EQUALS +import android.view.KeyEvent.KEYCODE_LEFT_BRACKET +import android.view.KeyEvent.KEYCODE_MINUS +import android.view.KeyEvent.KEYCODE_RIGHT_BRACKET +import android.view.KeyEvent.META_META_ON import android.view.WindowInsets import android.view.WindowManager +import android.window.DesktopModeFlags import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector @@ -35,8 +43,8 @@ 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 +import kotlin.math.abs /** * Wrapper class around App helper classes. This class adds functionality to the apps that the @@ -59,6 +67,11 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : BOTTOM } + enum class AppProperty { + STANDARD, + NON_RESIZABLE + } + /** Wait for an app moved to desktop to finish its transition. */ private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) { wmHelper @@ -69,13 +82,32 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : .waitForAndVerify() } + /** Launch an app and ensure it's moved to Desktop if it has not. */ + fun enterDesktopMode( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH), + shouldUseDragToDesktop: Boolean = false, + ) { + innerHelper.launchViaIntent(wmHelper) + if (isInDesktopWindowingMode(wmHelper)) return + if (shouldUseDragToDesktop) { + enterDesktopModeWithDrag( + wmHelper = wmHelper, + device = device, + motionEventHelper = motionEventHelper + ) + } else { + enterDesktopModeFromAppHandleMenu(wmHelper, device) + } + } + /** Move an app to Desktop by dragging the app handle at the top. */ - fun enterDesktopWithDrag( + private fun enterDesktopModeWithDrag( wmHelper: WindowManagerStateHelper, device: UiDevice, motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH) ) { - innerHelper.launchViaIntent(wmHelper) dragToDesktop( wmHelper = wmHelper, device = device, @@ -102,13 +134,9 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : // drag the window to move to desktop if (motionEventHelper.inputMethod == TOUCH - && Flags.enableHoldToDragAppHandle()) { + && DesktopModeFlags.ENABLE_HOLD_TO_DRAG_APP_HANDLE.isTrue) { // Touch requires hold-to-drag. - val downTime = SystemClock.uptimeMillis() - motionEventHelper.actionDown(startX, startY, time = downTime) - SystemClock.sleep(100L) // hold for 100ns before starting the move. - motionEventHelper.actionMove(startX, startY, startX, endY, 100, downTime = downTime) - motionEventHelper.actionUp(startX, endY, downTime = downTime) + motionEventHelper.holdToDrag(startX, startY, startX, endY, steps = 100) } else { device.drag(startX, startY, startX, endY, 100) } @@ -124,13 +152,75 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : } /** Click maximise button on the app header for the given app. */ - fun maximiseDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) { - val caption = getCaptionForTheApp(wmHelper, device) - val maximizeButton = getMaximizeButtonForTheApp(caption) - maximizeButton.click() + fun maximiseDesktopApp( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + usingKeyboard: Boolean = false + ) { + if (usingKeyboard) { + val keyEventHelper = KeyEventHelper(getInstrumentation()) + keyEventHelper.press(KEYCODE_EQUALS, META_META_ON) + } else { + val caption = getCaptionForTheApp(wmHelper, device) + 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, + isPip: Boolean = false, + usingKeyboard: Boolean = false, + ) { + if (usingKeyboard) { + val keyEventHelper = KeyEventHelper(getInstrumentation()) + keyEventHelper.press(KEYCODE_MINUS, META_META_ON) + } else { + val caption = getCaptionForTheApp(wmHelper, device) + val minimizeButton = getMinimizeButtonForTheApp(caption) + minimizeButton.click() + } + + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .apply { + if (isPip) withPipShown() + else + withWindowSurfaceDisappeared(innerHelper) + .withActivityState(innerHelper, PlatformConsts.STATE_STOPPED) + } + .waitForAndVerify() + } + + private fun getHeaderEmptyView(caption: UiObject2?): UiObject2 { + return caption + ?.children + ?.find { it.resourceName.endsWith(HEADER_EMPTY_VIEW) } + ?: error("Unable to find resource $HEADER_EMPTY_VIEW\n") + } + + /** Click on an existing window's header to bring it to the front. */ + fun bringToFront(wmHelper: WindowManagerStateHelper, device: UiDevice) { + val caption = getCaptionForTheApp(wmHelper, device) + val openHeaderView = getHeaderEmptyView(caption) + openHeaderView.click() + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withTopVisibleApp(innerHelper) + .waitForAndVerify() + } + /** Open maximize menu and click snap resize button on the app header for the given app. */ fun snapResizeDesktopApp( wmHelper: WindowManagerStateHelper, @@ -140,7 +230,7 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : ) { val caption = getCaptionForTheApp(wmHelper, device) val maximizeButton = getMaximizeButtonForTheApp(caption) - maximizeButton?.longClick() + maximizeButton.longClick() wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() val buttonResId = if (toLeft) SNAP_LEFT_BUTTON else SNAP_RIGHT_BUTTON @@ -152,6 +242,25 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : ?: error("Unable to find object with resource id $buttonResId") snapResizeButton.click() + waitAndVerifySnapResize(wmHelper, context, toLeft) + } + + fun snapResizeWithKeyboard( + wmHelper: WindowManagerStateHelper, + context: Context, + keyEventHelper: KeyEventHelper, + toLeft: Boolean, + ) { + val bracketKey = if (toLeft) KEYCODE_LEFT_BRACKET else KEYCODE_RIGHT_BRACKET + keyEventHelper.press(bracketKey, META_META_ON) + waitAndVerifySnapResize(wmHelper, context, toLeft) + } + + private fun waitAndVerifySnapResize( + wmHelper: WindowManagerStateHelper, + context: Context, + toLeft: Boolean + ) { val displayRect = getDisplayRect(wmHelper) val insets = getWindowInsets( context, WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars() @@ -162,19 +271,33 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : val expectedRect = Rect(displayRect).apply { if (toLeft) right -= expectedWidth else left += expectedWidth } - - wmHelper - .StateSyncBuilder() + wmHelper.StateSyncBuilder() .withAppTransitionIdle() - .withSurfaceVisibleRegion(this, Region(expectedRect)) + .withSurfaceMatchingVisibleRegion( + this, + Region(expectedRect), + { surfaceRegion, expectedRegion -> + areSnapWindowRegionsMatchingWithinThreshold( + surfaceRegion, expectedRegion, toLeft + ) + }) .waitForAndVerify() } - /** Click close button on the app header for the given app. */ - fun closeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) { - val caption = getCaptionForTheApp(wmHelper, device) - val closeButton = caption?.children?.find { it.resourceName.endsWith(CLOSE_BUTTON) } - closeButton?.click() + /** Close a desktop app by clicking the close button on the app header for the given app or by + * pressing back. */ + fun closeDesktopApp( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + usingBackNavigation: Boolean = false + ) { + if (usingBackNavigation) { + device.pressBack() + } else { + val caption = getCaptionForTheApp(wmHelper, device) + val closeButton = caption?.children?.find { it.resourceName.endsWith(CLOSE_BUTTON) } + closeButton?.click() + } wmHelper .StateSyncBuilder() .withAppTransitionIdle() @@ -280,7 +403,11 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : val displayRect = getDisplayRect(wmHelper) - val endX = if (isLeft) displayRect.left else displayRect.right + 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 @@ -324,6 +451,14 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : waitForTransitionToFullscreen(wmHelper) } + /** Maximize an app by dragging the app handle to the top drag zone. */ + fun maximizeAppWithDragToTopDragZone( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + ) { + dragAppWindowToTopDragZone(wmHelper, device) + } + private fun dragAppWindowToTopDragZone(wmHelper: WindowManagerStateHelper, device: UiDevice) { val windowRect = wmHelper.getWindowRegion(innerHelper).bounds val displayRect = getDisplayRect(wmHelper) @@ -384,8 +519,40 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : return metricInsets.getInsetsIgnoringVisibility(typeMask) } + // Requirement of DesktopWindowingMode is having a minimum of 1 app in WINDOWING_MODE_FREEFORM. + private fun isInDesktopWindowingMode(wmHelper: WindowManagerStateHelper) = + wmHelper.getWindow(innerHelper)?.windowingMode == WINDOWING_MODE_FREEFORM + + private fun areSnapWindowRegionsMatchingWithinThreshold( + surfaceRegion: Region, expectedRegion: Region, toLeft: Boolean + ): Boolean { + val surfaceBounds = surfaceRegion.bounds + val expectedBounds = expectedRegion.bounds + // If snapped to left, right bounds will be cut off by the center divider. + // Else if snapped to right, the left bounds will be cut off. + val leftSideMatching: Boolean + val rightSideMatching: Boolean + if (toLeft) { + leftSideMatching = surfaceBounds.left == expectedBounds.left + rightSideMatching = + abs(surfaceBounds.right - expectedBounds.right) <= + surfaceBounds.right * SNAP_WINDOW_MAX_THRESHOLD_DIFF + } else { + leftSideMatching = + abs(surfaceBounds.left - expectedBounds.left) <= + surfaceBounds.left * SNAP_WINDOW_MAX_THRESHOLD_DIFF + rightSideMatching = surfaceBounds.right == expectedBounds.right + } + + return surfaceBounds.top == expectedBounds.top && + surfaceBounds.bottom == expectedBounds.bottom && + leftSideMatching && + rightSideMatching + } + 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" @@ -394,7 +561,16 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : 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" + const val HEADER_EMPTY_VIEW: String = "caption_handle" val caption: BySelector get() = By.res(SYSTEMUI_PACKAGE, CAPTION) + // In DesktopMode, window snap can be done with just a single window. In this case, the + // divider tiling between left and right window won't be shown, and hence its states are not + // obtainable in test. + // As the test should just focus on ensuring window goes to one side of the screen, an + // acceptable approach is to ensure snapped window still fills > 95% of either side of the + // screen. + const val SNAP_WINDOW_MAX_THRESHOLD_DIFF = 0.05 } } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GestureHelper.java deleted file mode 100644 index eeee7b4dfc6b..000000000000 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GestureHelper.java +++ /dev/null @@ -1,306 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm.flicker.helpers; - -import android.annotation.NonNull; -import android.app.Instrumentation; -import android.app.UiAutomation; -import android.os.SystemClock; -import android.view.InputDevice; -import android.view.InputEvent; -import android.view.MotionEvent; -import android.view.MotionEvent.PointerCoords; -import android.view.MotionEvent.PointerProperties; - -import androidx.annotation.Nullable; - -/** - * Injects gestures given an {@link Instrumentation} object. - */ -public class GestureHelper { - // Inserted after each motion event injection. - private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5; - - private final UiAutomation mUiAutomation; - - /** - * Primary pointer should be cached here for separate release - */ - @Nullable private PointerProperties mPrimaryPtrProp; - @Nullable private PointerCoords mPrimaryPtrCoord; - private long mPrimaryPtrDownTime; - - /** - * A pair of floating point values. - */ - public static class Tuple { - public float x; - public float y; - - public Tuple(float x, float y) { - this.x = x; - this.y = y; - } - } - - public GestureHelper(Instrumentation instrumentation) { - mUiAutomation = instrumentation.getUiAutomation(); - } - - /** - * Injects a series of {@link MotionEvent}s to simulate tapping. - * - * @param point coordinates of pointer to tap - * @param times the number of times to tap - */ - public boolean tap(@NonNull Tuple point, int times) throws InterruptedException { - PointerProperties ptrProp = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER); - PointerCoords ptrCoord = getPointerCoord(point.x, point.y, 1, 1); - - for (int i = 0; i <= times; i++) { - // If already tapped, inject delay in between movements - if (times > 0) { - SystemClock.sleep(50L); - } - if (!primaryPointerDown(ptrProp, ptrCoord, SystemClock.uptimeMillis())) { - return false; - } - // Delay before releasing tap - SystemClock.sleep(100L); - if (!primaryPointerUp(ptrProp, ptrCoord, SystemClock.uptimeMillis())) { - return false; - } - } - return true; - } - - /** - * Injects a series of {@link MotionEvent}s to simulate a drag gesture without pointer release. - * - * Simulates a drag gesture without releasing the primary pointer. The primary pointer info - * will be cached for potential release later on by {@code releasePrimaryPointer()} - * - * @param startPoint initial coordinates of the primary pointer - * @param endPoint final coordinates of the primary pointer - * @param steps number of steps to take to animate dragging - * @return true if gesture is injected successfully - */ - public boolean dragWithoutRelease(@NonNull Tuple startPoint, - @NonNull Tuple endPoint, int steps) { - PointerProperties ptrProp = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER); - PointerCoords ptrCoord = getPointerCoord(startPoint.x, startPoint.y, 1, 1); - - PointerProperties[] ptrProps = new PointerProperties[] { ptrProp }; - PointerCoords[] ptrCoords = new PointerCoords[] { ptrCoord }; - - long downTime = SystemClock.uptimeMillis(); - - if (!primaryPointerDown(ptrProp, ptrCoord, downTime)) { - return false; - } - - // cache the primary pointer info for later potential release - mPrimaryPtrProp = ptrProp; - mPrimaryPtrCoord = ptrCoord; - mPrimaryPtrDownTime = downTime; - - return movePointers(ptrProps, ptrCoords, new Tuple[] { endPoint }, downTime, steps); - } - - /** - * Release primary pointer if previous gesture has cached the primary pointer info. - * - * @return true if the release was injected successfully - */ - public boolean releasePrimaryPointer() { - if (mPrimaryPtrProp != null && mPrimaryPtrCoord != null) { - return primaryPointerUp(mPrimaryPtrProp, mPrimaryPtrCoord, mPrimaryPtrDownTime); - } - - return false; - } - - /** - * Injects a series of {@link MotionEvent} objects to simulate a pinch gesture. - * - * @param startPoint1 initial coordinates of the first pointer - * @param startPoint2 initial coordinates of the second pointer - * @param endPoint1 final coordinates of the first pointer - * @param endPoint2 final coordinates of the second pointer - * @param steps number of steps to take to animate pinching - * @return true if gesture is injected successfully - */ - public boolean pinch(@NonNull Tuple startPoint1, @NonNull Tuple startPoint2, - @NonNull Tuple endPoint1, @NonNull Tuple endPoint2, int steps) { - PointerProperties ptrProp1 = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER); - PointerProperties ptrProp2 = getPointerProp(1, MotionEvent.TOOL_TYPE_FINGER); - - PointerCoords ptrCoord1 = getPointerCoord(startPoint1.x, startPoint1.y, 1, 1); - PointerCoords ptrCoord2 = getPointerCoord(startPoint2.x, startPoint2.y, 1, 1); - - PointerProperties[] ptrProps = new PointerProperties[] { - ptrProp1, ptrProp2 - }; - - PointerCoords[] ptrCoords = new PointerCoords[] { - ptrCoord1, ptrCoord2 - }; - - long downTime = SystemClock.uptimeMillis(); - - if (!primaryPointerDown(ptrProp1, ptrCoord1, downTime)) { - return false; - } - - if (!nonPrimaryPointerDown(ptrProps, ptrCoords, downTime, 1)) { - return false; - } - - if (!movePointers(ptrProps, ptrCoords, new Tuple[] { endPoint1, endPoint2 }, - downTime, steps)) { - return false; - } - - if (!nonPrimaryPointerUp(ptrProps, ptrCoords, downTime, 1)) { - return false; - } - - return primaryPointerUp(ptrProp1, ptrCoord1, downTime); - } - - private boolean primaryPointerDown(@NonNull PointerProperties prop, - @NonNull PointerCoords coord, long downTime) { - MotionEvent event = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, 1, - new PointerProperties[]{ prop }, new PointerCoords[]{ coord }); - - return injectEventSync(event); - } - - private boolean nonPrimaryPointerDown(@NonNull PointerProperties[] props, - @NonNull PointerCoords[] coords, long downTime, int index) { - // at least 2 pointers are needed - if (props.length != coords.length || coords.length < 2) { - return false; - } - - long eventTime = SystemClock.uptimeMillis(); - - MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_DOWN - + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords); - - return injectEventSync(event); - } - - private boolean movePointers(@NonNull PointerProperties[] props, - @NonNull PointerCoords[] coords, @NonNull Tuple[] endPoints, long downTime, int steps) { - // the number of endpoints should be the same as the number of pointers - if (props.length != coords.length || coords.length != endPoints.length) { - return false; - } - - // prevent division by 0 and negative number of steps - if (steps < 1) { - steps = 1; - } - - // save the starting points before updating any pointers - Tuple[] startPoints = new Tuple[coords.length]; - - for (int i = 0; i < coords.length; i++) { - startPoints[i] = new Tuple(coords[i].x, coords[i].y); - } - - MotionEvent event; - long eventTime; - - for (int i = 0; i < steps; i++) { - // inject a delay between movements - SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS); - - // update the coordinates - for (int j = 0; j < coords.length; j++) { - coords[j].x += (endPoints[j].x - startPoints[j].x) / steps; - coords[j].y += (endPoints[j].y - startPoints[j].y) / steps; - } - - eventTime = SystemClock.uptimeMillis(); - - event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_MOVE, - coords.length, props, coords); - - boolean didInject = injectEventSync(event); - - if (!didInject) { - return false; - } - } - - return true; - } - - private boolean primaryPointerUp(@NonNull PointerProperties prop, - @NonNull PointerCoords coord, long downTime) { - long eventTime = SystemClock.uptimeMillis(); - - MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_UP, 1, - new PointerProperties[]{ prop }, new PointerCoords[]{ coord }); - - return injectEventSync(event); - } - - private boolean nonPrimaryPointerUp(@NonNull PointerProperties[] props, - @NonNull PointerCoords[] coords, long downTime, int index) { - // at least 2 pointers are needed - if (props.length != coords.length || coords.length < 2) { - return false; - } - - long eventTime = SystemClock.uptimeMillis(); - - MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_UP - + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords); - - return injectEventSync(event); - } - - private PointerCoords getPointerCoord(float x, float y, float pressure, float size) { - PointerCoords ptrCoord = new PointerCoords(); - ptrCoord.x = x; - ptrCoord.y = y; - ptrCoord.pressure = pressure; - ptrCoord.size = size; - return ptrCoord; - } - - private PointerProperties getPointerProp(int id, int toolType) { - PointerProperties ptrProp = new PointerProperties(); - ptrProp.id = id; - ptrProp.toolType = toolType; - return ptrProp; - } - - private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, - int pointerCount, PointerProperties[] ptrProps, PointerCoords[] ptrCoords) { - return MotionEvent.obtain(downTime, eventTime, action, pointerCount, - ptrProps, ptrCoords, 0, 0, 1.0f, 1.0f, - 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); - } - - private boolean injectEventSync(InputEvent event) { - return mUiAutomation.injectInputEvent(event, true); - } -} diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/KeyEventHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/KeyEventHelper.kt new file mode 100644 index 000000000000..55ed09154aee --- /dev/null +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/KeyEventHelper.kt @@ -0,0 +1,60 @@ +/* + * 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.KeyEvent.ACTION_DOWN +import android.view.KeyEvent.ACTION_UP +import android.view.KeyEvent + +/** + * Helper class for injecting a custom key event. This is used for instrumenting keyboard shortcut + * actions. + */ +class KeyEventHelper( + private val instr: Instrumentation, +) { + fun press(keyCode: Int, metaState: Int = 0) { + actionDown(keyCode, metaState) + actionUp(keyCode, metaState) + } + + fun actionDown(keyCode: Int, metaState: Int = 0, time: Long = SystemClock.uptimeMillis()) { + injectKeyEvent(ACTION_DOWN, keyCode, metaState, downTime = time, eventTime = time) + } + + fun actionUp(keyCode: Int, metaState: Int = 0, time: Long = SystemClock.uptimeMillis()) { + injectKeyEvent(ACTION_UP, keyCode, metaState, downTime = time, eventTime = time) + } + + private fun injectKeyEvent( + action: Int, + keyCode: Int, + metaState: Int = 0, + downTime: Long = SystemClock.uptimeMillis(), + eventTime: Long = SystemClock.uptimeMillis() + ): KeyEvent { + val event = KeyEvent(downTime, eventTime, action, keyCode, /* repeat= */ 0, metaState) + injectKeyEvent(event) + return event + } + + private fun injectKeyEvent(event: KeyEvent) { + instr.uiAutomation.injectInputEvent(event, true) + } +}
\ No newline at end of file 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..d5334cbd541c 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 @@ -21,6 +21,7 @@ import android.graphics.Rect import android.graphics.Region import android.tools.device.apphelpers.StandardAppHelper import android.tools.helpers.FIND_TIMEOUT +import android.tools.helpers.GestureHelper import android.tools.helpers.SYSTEMUI_PACKAGE import android.tools.traces.component.ComponentNameMatcher import android.tools.traces.parsers.WindowManagerStateHelper @@ -33,12 +34,13 @@ 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) + private val gestureHelper: GestureHelper = + GestureHelper(instrumentation) fun clickRestart(wmHelper: WindowManagerStateHelper) { val restartButton = @@ -128,6 +130,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 index 86a0b0f8c66e..1fe60888fa52 100644 --- 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 @@ -54,7 +54,15 @@ class MotionEventHelper( injectMotionEvent(ACTION_UP, x, y, downTime = downTime) } - fun actionMove(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int, downTime: Long) { + 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) @@ -65,9 +73,33 @@ class MotionEventHelper( 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, @@ -120,4 +152,9 @@ class MotionEventHelper( 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 931e4f88aa8d..344cac1ac7e5 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 @@ -18,29 +18,26 @@ package com.android.server.wm.flicker.helpers import android.app.Instrumentation import android.content.Intent -import android.graphics.Rect import android.graphics.Region import android.media.session.MediaController import android.media.session.MediaSessionManager -import android.tools.datatypes.coversMoreThan -import android.tools.device.apphelpers.StandardAppHelper +import android.tools.device.apphelpers.BasePipAppHelper import android.tools.helpers.FIND_TIMEOUT import android.tools.helpers.SYSTEMUI_PACKAGE import android.tools.traces.ConditionsFactory +import android.tools.traces.component.ComponentNameMatcher import android.tools.traces.component.IComponentMatcher 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.Until import com.android.server.wm.flicker.testapp.ActivityOptions -open class PipAppHelper(instrumentation: Instrumentation) : - StandardAppHelper( - instrumentation, - ActivityOptions.Pip.LABEL, - ActivityOptions.Pip.COMPONENT.toFlickerComponent() - ) { +open class PipAppHelper( + instrumentation: Instrumentation, + appName: String = ActivityOptions.Pip.LABEL, + componentNameMatcher: ComponentNameMatcher = ActivityOptions.Pip.COMPONENT.toFlickerComponent(), +) : BasePipAppHelper(instrumentation, appName, componentNameMatcher) { private val mediaSessionManager: MediaSessionManager get() = context.getSystemService(MediaSessionManager::class.java) @@ -52,189 +49,6 @@ open class PipAppHelper(instrumentation: Instrumentation) : it.packageName == packageName } - private val gestureHelper: GestureHelper = GestureHelper(instrumentation) - - open fun clickObject(resId: String) { - val selector = By.res(packageName, resId) - val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") - - obj.click() - } - - /** Drags the PIP window to the provided final coordinates without releasing the pointer. */ - fun dragPipWindowAwayFromEdgeWithoutRelease(wmHelper: WindowManagerStateHelper, steps: Int) { - val initWindowRect = Rect(getWindowRect(wmHelper)) - - // initial pointer at the center of the window - val initialCoord = - GestureHelper.Tuple( - initWindowRect.centerX().toFloat(), - initWindowRect.centerY().toFloat() - ) - - // the offset to the right (or left) of the window center to drag the window to - val offset = 50 - - // the actual final x coordinate with the offset included; - // if the pip window is closer to the right edge of the display the offset is negative - // otherwise the offset is positive - val endX = - initWindowRect.centerX() + offset * (if (isCloserToRightEdge(wmHelper)) -1 else 1) - val finalCoord = GestureHelper.Tuple(endX.toFloat(), initWindowRect.centerY().toFloat()) - - // drag to the final coordinate - gestureHelper.dragWithoutRelease(initialCoord, finalCoord, steps) - } - - /** - * Releases the primary pointer. - * - * Injects the release of the primary pointer if the primary pointer info was cached after - * another gesture was injected without pointer release. - */ - fun releasePipAfterDragging() { - gestureHelper.releasePrimaryPointer() - } - - /** - * Drags the PIP window away from the screen edge while not crossing the display center. - * - * @throws IllegalStateException if default display bounds are not available - */ - fun dragPipWindowAwayFromEdge(wmHelper: WindowManagerStateHelper, steps: Int) { - val initWindowRect = Rect(getWindowRect(wmHelper)) - - // initial pointer at the center of the window - val startX = initWindowRect.centerX() - val y = initWindowRect.centerY() - - val displayRect = - wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect - ?: throw IllegalStateException("Default display is null") - - // the offset to the right (or left) of the display center to drag the window to - val offset = 20 - - // the actual final x coordinate with the offset included; - // if the pip window is closer to the right edge of the display the offset is positive - // otherwise the offset is negative - val endX = displayRect.centerX() + offset * (if (isCloserToRightEdge(wmHelper)) 1 else -1) - - // drag the window to the left but not beyond the center of the display - uiDevice.drag(startX, y, endX, y, steps) - } - - /** - * Returns true if PIP window is closer to the right edge of the display than left. - * - * @throws IllegalStateException if default display bounds are not available - */ - fun isCloserToRightEdge(wmHelper: WindowManagerStateHelper): Boolean { - val windowRect = getWindowRect(wmHelper) - - val displayRect = - wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect - ?: throw IllegalStateException("Default display is null") - - return windowRect.centerX() > displayRect.centerX() - } - - /** - * Expands the PIP window by using the pinch out gesture. - * - * @param percent The percentage by which to increase the pip window size. - * @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f - */ - fun pinchOpenPipWindow(wmHelper: WindowManagerStateHelper, percent: Float, steps: Int) { - // the percentage must be between 0.0f and 1.0f - if (percent <= 0.0f || percent > 1.0f) { - throw IllegalArgumentException("Percent must be between 0.0f and 1.0f") - } - - val windowRect = getWindowRect(wmHelper) - - // first pointer's initial x coordinate is halfway between the left edge and the center - val initLeftX = (windowRect.centerX() - windowRect.width() / 4).toFloat() - // second pointer's initial x coordinate is halfway between the right edge and the center - val initRightX = (windowRect.centerX() + windowRect.width() / 4).toFloat() - - // horizontal distance the window should increase by - val distIncrease = windowRect.width() * percent - - // final x-coordinates - val finalLeftX = initLeftX - (distIncrease / 2) - val finalRightX = initRightX + (distIncrease / 2) - - // y-coordinate is the same throughout this animation - val yCoord = windowRect.centerY().toFloat() - - var adjustedSteps = MIN_STEPS_TO_ANIMATE - - // if distance per step is at least 1, then we can use the number of steps requested - if (distIncrease.toInt() / (steps * 2) >= 1) { - adjustedSteps = steps - } - - // if the distance per step is less than 1, carry out the animation in two steps - gestureHelper.pinch( - GestureHelper.Tuple(initLeftX, yCoord), - GestureHelper.Tuple(initRightX, yCoord), - GestureHelper.Tuple(finalLeftX, yCoord), - GestureHelper.Tuple(finalRightX, yCoord), - adjustedSteps - ) - - waitForPipWindowToExpandFrom(wmHelper, Region(windowRect)) - } - - /** - * Minimizes the PIP window by using the pinch in gesture. - * - * @param percent The percentage by which to decrease the pip window size. - * @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f - */ - fun pinchInPipWindow(wmHelper: WindowManagerStateHelper, percent: Float, steps: Int) { - // the percentage must be between 0.0f and 1.0f - if (percent <= 0.0f || percent > 1.0f) { - throw IllegalArgumentException("Percent must be between 0.0f and 1.0f") - } - - val windowRect = getWindowRect(wmHelper) - - // first pointer's initial x coordinate is halfway between the left edge and the center - val initLeftX = (windowRect.centerX() - windowRect.width() / 4).toFloat() - // second pointer's initial x coordinate is halfway between the right edge and the center - val initRightX = (windowRect.centerX() + windowRect.width() / 4).toFloat() - - // decrease by the distance specified through the percentage - val distDecrease = windowRect.width() * percent - - // get the final x-coordinates and make sure they are not passing the center of the window - val finalLeftX = Math.min(initLeftX + (distDecrease / 2), windowRect.centerX().toFloat()) - val finalRightX = Math.max(initRightX - (distDecrease / 2), windowRect.centerX().toFloat()) - - // y-coordinate is the same throughout this animation - val yCoord = windowRect.centerY().toFloat() - - var adjustedSteps = MIN_STEPS_TO_ANIMATE - - // if distance per step is at least 1, then we can use the number of steps requested - if (distDecrease.toInt() / (steps * 2) >= 1) { - adjustedSteps = steps - } - - // if the distance per step is less than 1, carry out the animation in two steps - gestureHelper.pinch( - GestureHelper.Tuple(initLeftX, yCoord), - GestureHelper.Tuple(initRightX, yCoord), - GestureHelper.Tuple(finalLeftX, yCoord), - GestureHelper.Tuple(finalRightX, yCoord), - adjustedSteps - ) - - waitForPipWindowToMinimizeFrom(wmHelper, Region(windowRect)) - } - /** * Launches the app through an intent instead of interacting with the launcher and waits until * the app window is in PIP mode @@ -250,24 +64,20 @@ open class PipAppHelper(instrumentation: Instrumentation) : wmHelper, launchedAppComponentMatcherOverride, action, - stringExtras, - waitConditionsBuilder = - wmHelper - .StateSyncBuilder() - .add(ConditionsFactory.isWMStateComplete()) - .withAppTransitionIdle() - .add(ConditionsFactory.hasPipWindow()) + stringExtras ) wmHelper .StateSyncBuilder() .withWindowSurfaceAppeared(this) + .add(ConditionsFactory.isWMStateComplete()) .withPipShown() .waitForAndVerify() } - /** Expand the PIP window back to full screen via intent and wait until the app is visible */ - fun exitPipToFullScreenViaIntent(wmHelper: WindowManagerStateHelper) = launchViaIntent(wmHelper) + /** Expand the PIP window back to original task via intent and wait until the app is visible */ + open fun exitPipToOriginalTaskViaIntent(wmHelper: WindowManagerStateHelper) = + launchViaIntent(wmHelper) fun changeAspectRatio(wmHelper: WindowManagerStateHelper) { val intent = Intent("com.android.wm.shell.flicker.testapp.ASPECT_RATIO") @@ -336,126 +146,6 @@ open class PipAppHelper(instrumentation: Instrumentation) : closePipWindow(WindowManagerStateHelper(instrumentation)) } - /** Returns the pip window bounds. */ - fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect { - val windowRegion = wmHelper.getWindowRegion(this) - require(!windowRegion.isEmpty) { "Unable to find a PIP window in the current state" } - return windowRegion.bounds - } - - /** Taps the pip window and dismisses it by clicking on the X button. */ - open fun closePipWindow(wmHelper: WindowManagerStateHelper) { - val windowRect = getWindowRect(wmHelper) - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - // search and interact with the dismiss button - val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") - uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) - val dismissPipObject = - uiDevice.findObject(dismissSelector) ?: error("PIP window dismiss button not found") - val dismissButtonBounds = dismissPipObject.visibleBounds - uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY()) - - // Wait for animation to complete. - wmHelper.StateSyncBuilder().withPipGone().withHomeActivityVisible().waitForAndVerify() - } - - open fun tapPipToShowMenu(wmHelper: WindowManagerStateHelper) { - val windowRect = getWindowRect(wmHelper) - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - // search and interact with the dismiss button - val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") - uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) - } - - /** Close the pip window by pressing the expand button */ - fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) { - val windowRect = getWindowRect(wmHelper) - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - // search and interact with the expand button - val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button") - uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT) - val expandPipObject = - uiDevice.findObject(expandSelector) ?: error("PIP window expand button not found") - val expandButtonBounds = expandPipObject.visibleBounds - uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY()) - wmHelper.StateSyncBuilder().withPipGone().withFullScreenApp(this).waitForAndVerify() - } - - /** Double click on the PIP window to expand it */ - fun doubleClickPipWindow(wmHelper: WindowManagerStateHelper) { - val windowRect = getWindowRect(wmHelper) - Log.d(TAG, "First click") - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - Log.d(TAG, "Second click") - uiDevice.click(windowRect.centerX(), windowRect.centerY()) - Log.d(TAG, "Wait for app transition to end") - wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() - waitForPipWindowToExpandFrom(wmHelper, Region(windowRect)) - } - - private fun waitForPipWindowToExpandFrom( - wmHelper: WindowManagerStateHelper, - windowRect: Region - ) { - wmHelper - .StateSyncBuilder() - .add("pipWindowExpanded") { - val pipAppWindow = - it.wmState.visibleWindows.firstOrNull { window -> - this.windowMatchesAnyOf(window) - } - ?: return@add false - val pipRegion = pipAppWindow.frameRegion - return@add pipRegion.coversMoreThan(windowRect) - } - .waitForAndVerify() - } - - private fun waitForPipWindowToMinimizeFrom( - wmHelper: WindowManagerStateHelper, - windowRect: Region - ) { - wmHelper - .StateSyncBuilder() - .add("pipWindowMinimized") { - val pipAppWindow = - it.wmState.visibleWindows.firstOrNull { window -> - this.windowMatchesAnyOf(window) - } - Log.d(TAG, "window " + pipAppWindow) - if (pipAppWindow == null) return@add false - val pipRegion = pipAppWindow.frameRegion - Log.d( - TAG, - "region " + pipRegion + " covers " + windowRect.coversMoreThan(pipRegion) - ) - return@add windowRect.coversMoreThan(pipRegion) - } - .waitForAndVerify() - } - - /** - * Waits until the PIP window snaps horizontally to the provided bounds. - * - * @param finalBounds the bounds to wait for PIP window to snap to - */ - fun waitForPipToSnapTo(wmHelper: WindowManagerStateHelper, finalBounds: android.graphics.Rect) { - wmHelper - .StateSyncBuilder() - .add("pipWindowSnapped") { - val pipAppWindow = - it.wmState.visibleWindows.firstOrNull { window -> - this.windowMatchesAnyOf(window) - } - ?: return@add false - val pipRegionBounds = pipAppWindow.frameRegion.bounds - return@add pipRegionBounds.left == finalBounds.left && - pipRegionBounds.right == finalBounds.right - } - .add(ConditionsFactory.isWMStateComplete()) - .waitForAndVerify() - } - companion object { private const val TAG = "PipAppHelper" private const val ENTER_PIP_BUTTON_ID = "enter_pip" @@ -464,8 +154,5 @@ open class PipAppHelper(instrumentation: Instrumentation) : private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual" private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter" private const val SOURCE_RECT_HINT = "set_source_rect_hint" - // minimum number of steps to take, when animating gestures, needs to be 2 - // so that there is at least a single intermediate layer that flicker tests can check - private const val MIN_STEPS_TO_ANIMATE = 2 } -} +}
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/RecentTasksUtils.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/RecentTasksUtils.kt new file mode 100644 index 000000000000..1a5fda7c8324 --- /dev/null +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/RecentTasksUtils.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.helpers + +import android.app.Instrumentation + +object RecentTasksUtils { + fun clearAllVisibleRecentTasks(instrumentation: Instrumentation) { + instrumentation.uiAutomation.executeShellCommand( + "dumpsys activity service SystemUIService WMShell recents clearAll" + ) + } +} 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 index 69fde0168b14..9e488486e16a 100644 --- 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 @@ -65,10 +65,45 @@ constructor( .waitForAndVerify() } + fun startSingleAppMediaProjectionWithExtraIntent( + wmHelper: WindowManagerStateHelper, + targetApp: StandardAppHelper + ) { + clickStartMediaProjectionWithExtraIntentButton() + chooseSingleAppOption() + startScreenSharing() + selectTargetApp(targetApp.appName) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withHomeActivityVisible() + .waitForAndVerify() + } + + fun startSingleAppMediaProjectionFromRecents( + wmHelper: WindowManagerStateHelper, + targetApp: StandardAppHelper, + recentTasksIndex: Int = 0, + ) { + clickStartMediaProjectionButton() + chooseSingleAppOption() + startScreenSharing() + selectTargetAppRecent(recentTasksIndex) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceAppeared(targetApp) + .waitForAndVerify() + } + private fun clickStartMediaProjectionButton() { findObject(By.res(packageName, START_MEDIA_PROJECTION_BUTTON_ID)).also { it.click() } } + private fun clickStartMediaProjectionWithExtraIntentButton() { + findObject(By.res(packageName, START_MEDIA_PROJECTION_NEW_INTENT_BUTTON_ID)).also { it.click() } + } + private fun chooseEntireScreenOption() { findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() } @@ -92,6 +127,13 @@ constructor( findObject(By.text(targetAppName)).also { it.click() } } + private fun selectTargetAppRecent(recentTasksIndex: Int) { + // Scroll to to find target app to launch then click app icon it to start capture + val recentsTasksRecycler = + findObject(By.res(SYSTEMUI_PACKAGE, MEDIA_PROJECTION_RECENT_TASKS)) + recentsTasksRecycler.children[recentTasksIndex].also{ it.click() } + } + private fun chooseSingleAppOption() { findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() } @@ -116,8 +158,10 @@ constructor( 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" + const val START_MEDIA_PROJECTION_NEW_INTENT_BUTTON_ID: String = "button_start_mp_new_intent" val SCREEN_SHARE_OPTIONS_PATTERN: Pattern = Pattern.compile("$SYSTEMUI_PACKAGE:id/screen_share_mode_(options|spinner)") + const val MEDIA_PROJECTION_RECENT_TASKS: String = "media_projection_recent_tasks_recycler" const val ENTIRE_SCREEN_STRING_RES_NAME: String = "screen_share_permission_dialog_option_entire_screen" const val SINGLE_APP_STRING_RES_NAME: String = diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index f891606f0066..7c24a4adca3d 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -115,6 +115,19 @@ <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" @@ -143,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" @@ -333,12 +347,34 @@ <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> </intent-filter> </activity> + <activity android:name=".BottomHalfPipLaunchingActivity" + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" + android:taskAffinity="com.android.server.wm.flicker.testapp.BottomHalfPipLaunchingActivity" + android:theme="@style/CutoutShortEdges" + android:label="BottomHalfPipLaunchingActivity" + 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=".BottomHalfPipActivity" + android:resizeableActivity="true" + android:supportsPictureInPicture="true" + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" + android:taskAffinity="com.android.server.wm.flicker.testapp.BottomHalfPipLaunchingActivity" + android:theme="@style/TranslucentTheme" + android:label="BottomHalfPipActivity" + android:exported="true"> + </activity> <activity android:name=".SplitScreenActivity" android:resizeableActivity="true" 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"/> @@ -349,7 +385,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"/> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bottom_half_pip.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bottom_half_pip.xml new file mode 100644 index 000000000000..2f9c3aa82057 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_bottom_half_pip.xml @@ -0,0 +1,154 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="@android:color/holo_blue_bright"> + + <!-- All the buttons (and other clickable elements) should be arranged in a way so that it is + possible to "cycle" over all them by clicking on the D-Pad DOWN button. The way we do it + here is by arranging them this vertical LL and by relying on the nextFocusDown attribute + where things are arranged differently and to circle back up to the top once we reach the + bottom. --> + + <Button + android:id="@+id/enter_pip" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Enter PIP" + android:onClick="enterPip"/> + + <Button + android:id="@+id/toggle_bottom_half_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Set Bottom Half Layout" + android:onClick="toggleBottomHalfLayout"/> + + <CheckBox + android:id="@+id/with_custom_actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="With custom actions"/> + + <RadioGroup + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:checkedButton="@id/enter_pip_on_leave_disabled"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Enter PiP on home press"/> + + <RadioButton + android:id="@+id/enter_pip_on_leave_disabled" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Disabled" + android:onClick="onAutoPipSelected"/> + + <RadioButton + android:id="@+id/enter_pip_on_leave_manual" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Via code behind" + android:onClick="onAutoPipSelected"/> + + <RadioButton + android:id="@+id/enter_pip_on_leave_autoenter" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Auto-enter PiP" + android:onClick="onAutoPipSelected"/> + </RadioGroup> + + <RadioGroup + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:checkedButton="@id/ratio_default"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Ratio"/> + + <RadioButton + android:id="@+id/ratio_default" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Default" + android:onClick="onRatioSelected"/> + + <RadioButton + android:id="@+id/ratio_square" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Square [1:1]" + android:onClick="onRatioSelected"/> + + <RadioButton + android:id="@+id/ratio_wide" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Wide [2:1]" + android:onClick="onRatioSelected"/> + + <RadioButton + android:id="@+id/ratio_tall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Tall [1:2]" + android:onClick="onRatioSelected"/> + </RadioGroup> + + <CheckBox + android:id="@+id/set_source_rect_hint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Set SourceRectHint"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Media Session"/> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <Button + android:id="@+id/media_session_start" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:nextFocusDown="@id/media_session_stop" + android:text="Start"/> + + <Button + android:id="@+id/media_session_stop" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:nextFocusDown="@id/enter_pip" + android:text="Stop"/> + + </LinearLayout> + +</LinearLayout> 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 index 46f01e6c9752..c34d2003ef42 100644 --- 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 @@ -16,17 +16,27 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" 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:layout_margin="16dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:gravity="center_vertical|center_horizontal" android:text="Start Media Projection" android:textAppearance="?android:attr/textAppearanceLarge"/> + <Button + android:id="@+id/button_start_mp_new_intent" + android:layout_margin="16dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical|center_horizontal" + android:text="Start Media Projection with extra intent" + android:textAppearance="?android:attr/textAppearanceLarge"/> </LinearLayout>
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml index 47d113717ae0..837d050b73ff 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml @@ -62,6 +62,12 @@ <item name="android:backgroundDimEnabled">false</item> </style> + <style name="TranslucentTheme" parent="@style/OptOutEdgeToEdge"> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:backgroundDimEnabled">false</item> + </style> + <style name="no_starting_window" parent="@style/OptOutEdgeToEdge"> <item name="android:windowDisablePreview">true</item> </style> 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 e4de2c574553..0c1ac9951d32 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,12 @@ 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, @@ -235,6 +241,21 @@ public class ActivityOptions { FLICKER_APP_PACKAGE + ".PipActivity"); } + public static class BottomHalfPip { + public static final String LAUNCHING_APP_LABEL = "BottomHalfPipLaunchingActivity"; + // Test App > Bottom Half PIP Activity + public static final String LABEL = "BottomHalfPipActivity"; + + // Use the bottom half layout for PIP Activity + public static final String EXTRA_BOTTOM_HALF_LAYOUT = "bottom_half"; + + public static final ComponentName LAUNCHING_APP_COMPONENT = new ComponentName( + FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".BottomHalfPipLaunchingActivity"); + + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".BottomHalfPipActivity"); + } + public static class SplitScreen { public static class Primary { public static final String LABEL = "SplitScreenPrimaryActivity"; diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipActivity.java new file mode 100644 index 000000000000..1f82d674cfa7 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipActivity.java @@ -0,0 +1,108 @@ +/* + * 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 static com.android.server.wm.flicker.testapp.ActivityOptions.BottomHalfPip.EXTRA_BOTTOM_HALF_LAYOUT; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +public class BottomHalfPipActivity extends PipActivity { + + private boolean mUseBottomHalfLayout = true; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_bottom_half_pip); + setTheme(R.style.TranslucentTheme); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + updateLayout(); + } + + /** + * Toggles the layout mode between fill task and half-bottom modes. + */ + public void toggleBottomHalfLayout(View v) { + mUseBottomHalfLayout = !mUseBottomHalfLayout; + updateLayout(); + } + + /** + * Sets to match parent layout if the activity is + * {@link Activity#isInPictureInPictureMode()}. Otherwise, + * follows {@link #mUseBottomHalfLayout}. + * + * @see #setToBottomHalfMode(boolean) + */ + private void updateLayout() { + final boolean useBottomHalfLayout; + if (isInPictureInPictureMode()) { + useBottomHalfLayout = false; + } else { + useBottomHalfLayout = mUseBottomHalfLayout; + } + setToBottomHalfMode(useBottomHalfLayout); + } + + /** + * Sets `useBottomHalfLayout` to `true` to use the bottom half layout. Use the + * [LayoutParams.MATCH_PARENT] layout. + */ + private void setToBottomHalfMode(boolean useBottomHalfLayout) { + final WindowManager.LayoutParams attrs = getWindow().getAttributes(); + attrs.gravity = Gravity.BOTTOM; + if (useBottomHalfLayout) { + final int taskHeight = getWindowManager().getCurrentWindowMetrics().getBounds() + .height(); + attrs.height = taskHeight / 2; + } else { + attrs.height = LayoutParams.MATCH_PARENT; + } + getWindow().setAttributes(attrs); + } + + @Override + void handleIntentExtra(@NonNull Intent intent) { + super.handleIntentExtra(intent); + if (intent.hasExtra(EXTRA_BOTTOM_HALF_LAYOUT)) { + final String booleanString = intent.getStringExtra(EXTRA_BOTTOM_HALF_LAYOUT); + // We don't use Boolean#parseBoolean here because the impl only checks if the string + // equals to "true", and returns for any other cases. We use our own impl here to + // prevent false positive. + if ("true".equals(booleanString)) { + mUseBottomHalfLayout = true; + } else if ("false".equals(booleanString)) { + mUseBottomHalfLayout = false; + } + } + updateLayout(); + } +} diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipLaunchingActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipLaunchingActivity.java new file mode 100644 index 000000000000..209f71e4f307 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipLaunchingActivity.java @@ -0,0 +1,35 @@ +/* + * 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.content.Intent; +import android.os.Bundle; + +public class BottomHalfPipLaunchingActivity extends SimpleActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Intent intent = new Intent(this, BottomHalfPipActivity.class); + // Pass extras to BottomHalfPipActivity. + final Bundle extras = getIntent().getExtras(); + if (extras != null) { + intent.putExtras(extras); + } + startActivity(intent); + } +} 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 13d7f7f0d521..ee25ab2fb66c 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 @@ -350,7 +350,7 @@ public class PipActivity extends Activity { mMediaSession.setActive(newState != STATE_STOPPED); } - private void handleIntentExtra(Intent intent) { + void handleIntentExtra(Intent intent) { // Set the fixed orientation if requested if (intent.hasExtra(EXTRA_PIP_ORIENTATION)) { final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_PIP_ORIENTATION)); 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 index a24a48269d7c..b29b87450197 100644 --- 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 @@ -19,7 +19,8 @@ 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 static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE_NORMAL; +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE_EXTRA_INTENT; import android.app.Activity; import android.content.ComponentName; @@ -71,13 +72,17 @@ public class StartMediaProjectionActivity extends Activity { setContentView(R.layout.activity_start_media_projection); Button startMediaProjectionButton = findViewById(R.id.button_start_mp); + Button startMediaProjectionButton2 = findViewById(R.id.button_start_mp_new_intent); startMediaProjectionButton.setOnClickListener(v -> - startActivityForResult(mService.createScreenCaptureIntent(), REQUEST_CODE)); + startActivityForResult(mService.createScreenCaptureIntent(), REQUEST_CODE_NORMAL)); + startMediaProjectionButton2.setOnClickListener(v -> + startActivityForResult(mService.createScreenCaptureIntent(), + REQUEST_CODE_EXTRA_INTENT)); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode != REQUEST_CODE) { + if (requestCode != REQUEST_CODE_NORMAL && requestCode != REQUEST_CODE_EXTRA_INTENT) { throw new IllegalStateException("Unknown request code: " + requestCode); } if (resultCode != RESULT_OK) { @@ -85,6 +90,11 @@ public class StartMediaProjectionActivity extends Activity { } Log.d(TAG, "onActivityResult"); startMediaProjectionService(resultCode, data); + if (requestCode == REQUEST_CODE_EXTRA_INTENT) { + Intent startMain = new Intent(Intent.ACTION_MAIN); + startMain.addCategory(Intent.CATEGORY_HOME); + startActivity(startMain); + } } private void startMediaProjectionService(int resultCode, Intent resultData) { @@ -122,7 +132,7 @@ public class StartMediaProjectionActivity extends Activity { displayBounds.width(), displayBounds.height(), PixelFormat.RGBA_8888, 1); mVirtualDisplay = mMediaProjection.createVirtualDisplay( - "DanielDisplay", + "TestDisplay", displayBounds.width(), displayBounds.height(), DisplayMetrics.DENSITY_HIGH, diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp index bacb5eb1cfdf..1f0bd61b5c3f 100644 --- a/tests/Input/Android.bp +++ b/tests/Input/Android.bp @@ -38,6 +38,7 @@ android_test { "hamcrest-library", "junit-params", "kotlin-test", + "mockito-kotlin-nodeps", "mockito-target-extended-minus-junit4", "platform-test-annotations", "platform-screenshot-diff-core", diff --git a/tests/Input/AndroidManifest.xml b/tests/Input/AndroidManifest.xml index 914adc40194d..8d380f0d72a6 100644 --- a/tests/Input/AndroidManifest.xml +++ b/tests/Input/AndroidManifest.xml @@ -32,7 +32,7 @@ android:process=":externalProcess"> </activity> - <activity android:name="com.android.test.input.CaptureEventActivity" + <activity android:name="com.android.cts.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" diff --git a/tests/Input/res/xml/bookmarks.xml b/tests/Input/res/xml/bookmarks.xml new file mode 100644 index 000000000000..68ec1233cdd7 --- /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 xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <!-- 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" + androidprv:keycode="KEYCODE_B" + androidprv:modifierState="META" /> + <bookmark + category="android.intent.category.APP_CONTACTS" + androidprv:keycode="KEYCODE_P" + androidprv:modifierState="META" /> + <bookmark + category="android.intent.category.APP_EMAIL" + androidprv:keycode="KEYCODE_E" + androidprv:modifierState="META" /> + <bookmark + category="android.intent.category.APP_CALENDAR" + androidprv:keycode="KEYCODE_C" + androidprv:modifierState="META" /> + <bookmark + category="android.intent.category.APP_MAPS" + androidprv:keycode="KEYCODE_M" + androidprv:modifierState="META" /> + <bookmark + category="android.intent.category.APP_CALCULATOR" + androidprv:keycode="KEYCODE_U" + androidprv:modifierState="META" /> + + <bookmark + role="android.app.role.BROWSER" + androidprv:keycode="KEYCODE_B" + androidprv:modifierState="META|SHIFT" /> + + <bookmark + category="android.intent.category.APP_CONTACTS" + androidprv:keycode="KEYCODE_P" + shift="true" /> + + <bookmark + package="com.test" + class="com.test.BookmarkTest" + androidprv:keycode="KEYCODE_J" + shift="true" /> +</bookmarks> diff --git a/tests/Input/res/xml/bookmarks_legacy.xml b/tests/Input/res/xml/bookmarks_legacy.xml new file mode 100644 index 000000000000..78cc48b19416 --- /dev/null +++ b/tests/Input/res/xml/bookmarks_legacy.xml @@ -0,0 +1,54 @@ +<?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 are legacy way defining bookmarks and we + should prefer new way of defining bookmarks as shown in {@link bookmarks.xml} --> + <bookmark + role="android.app.role.BROWSER" + shortcut="b" /> + <bookmark + category="android.intent.category.APP_CONTACTS" + shortcut="p" /> + <bookmark + category="android.intent.category.APP_EMAIL" + shortcut="e" /> + <bookmark + category="android.intent.category.APP_CALENDAR" + shortcut="c" /> + <bookmark + category="android.intent.category.APP_MAPS" + shortcut="m" /> + <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="p" + shift="true" /> + + <bookmark + package="com.test" + class="com.test.BookmarkTest" + shortcut="j" + shift="true" /> +</bookmarks> diff --git a/tests/Input/res/xml/keyboard_glyph_maps.xml b/tests/Input/res/xml/keyboard_glyph_maps.xml index d0616ff5ccaa..42561c1a9923 100644 --- a/tests/Input/res/xml/keyboard_glyph_maps.xml +++ b/tests/Input/res/xml/keyboard_glyph_maps.xml @@ -19,4 +19,8 @@ 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_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 index 072341dcefae..e99c81493394 100644 --- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt @@ -18,20 +18,17 @@ package android.hardware.input import android.content.Context import android.content.ContextWrapper -import android.os.Handler import android.os.IBinder -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 org.junit.After +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.Mock import org.mockito.Mockito import org.mockito.Mockito.doAnswer import org.mockito.Mockito.`when` @@ -69,20 +66,16 @@ class KeyGestureEventHandlerTest { @get:Rule val rule = SetFlagsRule() + @get:Rule + val inputManagerRule = MockInputManagerRule() - private val testLooper = TestLooper() private var registeredListener: IKeyGestureHandler? = 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) @@ -97,7 +90,7 @@ class KeyGestureEventHandlerTest { } registeredListener = listener null - }.`when`(iInputManagerMock).registerKeyGestureHandler(any()) + }.`when`(inputManagerRule.mock).registerKeyGestureHandler(any()) // Handle key gesture handler being unregistered. doAnswer { @@ -108,14 +101,7 @@ class KeyGestureEventHandlerTest { } registeredListener = null null - }.`when`(iInputManagerMock).unregisterKeyGestureHandler(any()) - } - - @After - fun tearDown() { - if (this::inputManagerGlobalSession.isInitialized) { - inputManagerGlobalSession.close() - } + }.`when`(inputManagerRule.mock).unregisterKeyGestureHandler(any()) } private fun handleKeyGestureEvent(event: KeyGestureEvent) { diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt index ca9de6000a5a..cf0bfcc4f6df 100644 --- a/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt +++ b/tests/Input/src/android/hardware/input/KeyGestureEventListenerTest.kt @@ -26,20 +26,19 @@ 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.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.fail /** * Tests for [InputManager.KeyGestureEventListener]. @@ -63,21 +62,18 @@ class KeyGestureEventListenerTest { @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 - 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) @@ -92,7 +88,7 @@ class KeyGestureEventListenerTest { } registeredListener = listener null - }.`when`(iInputManagerMock).registerKeyGestureEventListener(any()) + }.`when`(inputManagerRule.mock).registerKeyGestureEventListener(any()) // Handle key gesture event listener being unregistered. doAnswer { @@ -103,14 +99,7 @@ class KeyGestureEventListenerTest { } registeredListener = null null - }.`when`(iInputManagerMock).unregisterKeyGestureEventListener(any()) - } - - @After - fun tearDown() { - if (this::inputManagerGlobalSession.isInitialized) { - inputManagerGlobalSession.close() - } + }.`when`(inputManagerRule.mock).unregisterKeyGestureEventListener(any()) } private fun notifyKeyGestureEvent(event: KeyGestureEvent) { 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/KeyboardLayoutPreviewTests.kt b/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt index ae32bdaf80d7..bcff2fcfca93 100644 --- a/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt +++ b/tests/Input/src/android/hardware/input/KeyboardLayoutPreviewTests.kt @@ -16,17 +16,11 @@ package android.hardware.input -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import android.content.ContextWrapper import android.graphics.drawable.Drawable import android.platform.test.annotations.Presubmit -import android.platform.test.flag.junit.SetFlagsRule import androidx.test.platform.app.InstrumentationRegistry -import com.android.hardware.input.Flags import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.junit.MockitoJUnitRunner @@ -46,9 +40,6 @@ class KeyboardLayoutPreviewTests { const val HEIGHT = 100 } - @get:Rule - val setFlagsRule = SetFlagsRule() - private fun createDrawable(): Drawable? { val context = ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext()) val inputManager = context.getSystemService(InputManager::class.java)!! @@ -56,16 +47,9 @@ class KeyboardLayoutPreviewTests { } @Test - @EnableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) fun testKeyboardLayoutDrawable_hasCorrectDimensions() { val drawable = createDrawable()!! assertEquals(WIDTH, drawable.intrinsicWidth) assertEquals(HEIGHT, drawable.intrinsicHeight) } - - @Test - @DisableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) - fun testKeyboardLayoutDrawable_isNull_ifFlagOff() { - assertNull(createDrawable()) - } }
\ No newline at end of file diff --git a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt index bcd56ad0c669..cc58bbc38e2d 100644 --- a/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt +++ b/tests/Input/src/android/hardware/input/StickyModifierStateListenerTest.kt @@ -21,27 +21,24 @@ import android.content.ContextWrapper import android.os.Handler import android.os.HandlerExecutor 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.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]. @@ -51,29 +48,20 @@ import kotlin.test.fail */ @Presubmit @RunWith(MockitoJUnitRunner::class) -@EnableFlags( - com.android.hardware.input.Flags.FLAG_KEYBOARD_A11Y_STICKY_KEYS_FLAG, - com.android.input.flags.Flags.FLAG_ENABLE_INPUT_FILTER_RUST_IMPL, -) class StickyModifierStateListenerTest { @get:Rule - val rule = SetFlagsRule() + 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 +76,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 +87,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/InputDataStoreTests.kt b/tests/Input/src/com/android/server/input/InputDataStoreTests.kt new file mode 100644 index 000000000000..78c828bafd8f --- /dev/null +++ b/tests/Input/src/com/android/server/input/InputDataStoreTests.kt @@ -0,0 +1,504 @@ +/* + * 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.hardware.input.AppLaunchData +import android.hardware.input.InputGestureData +import android.hardware.input.KeyGestureEvent +import android.platform.test.annotations.Presubmit +import android.util.AtomicFile +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.nio.charset.StandardCharsets +import kotlin.test.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito + +/** + * Tests for {@link InputDataStore}. + * + * Build/Install/Run: + * atest InputTests:InputDataStoreTests + */ +@Presubmit +class InputDataStoreTests { + + companion object { + const val USER_ID = 1 + } + + private lateinit var context: Context + private lateinit var inputDataStore: InputDataStore + private lateinit var tempFile: File + + @Before + fun setup() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + setupInputDataStore() + } + + private fun setupInputDataStore() { + tempFile = File.createTempFile("input_gestures", ".xml") + inputDataStore = InputDataStore(object : InputDataStore.FileInjector("input_gestures") { + private val atomicFile: AtomicFile = AtomicFile(tempFile) + + override fun openRead(userId: Int): InputStream? { + return atomicFile.openRead() + } + + override fun startWrite(userId: Int): FileOutputStream? { + return atomicFile.startWrite() + } + + override fun finishWrite(userId: Int, fos: FileOutputStream?, success: Boolean) { + if (success) { + atomicFile.finishWrite(fos) + } else { + atomicFile.failWrite(fos) + } + } + }) + } + + private fun getPrintableXml(inputGestures: List<InputGestureData>): String { + val outputStream = ByteArrayOutputStream() + inputDataStore.writeInputGestureXml(outputStream, true, inputGestures) + return outputStream.toString(StandardCharsets.UTF_8).trimIndent() + } + + @Test + fun saveToDiskKeyGesturesOnly() { + val inputGestures = listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_2, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build() + ) + + inputDataStore.saveInputGestures(USER_ID, inputGestures) + assertEquals( + inputGestures, + inputDataStore.loadInputGestures(USER_ID), + getPrintableXml(inputGestures) + ) + } + + @Test + fun saveToDiskTouchpadGestures() { + val inputGestures = listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + ) + + inputDataStore.saveInputGestures(USER_ID, inputGestures) + assertEquals( + inputGestures, + inputDataStore.loadInputGestures(USER_ID), + getPrintableXml(inputGestures) + ) + } + + @Test + fun saveToDiskAppLaunchGestures() { + val inputGestures = listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) + .setAppLaunchData(AppLaunchData.createLaunchDataForRole(RoleManager.ROLE_BROWSER)) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_2, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) + .setAppLaunchData(AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS)) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) + .setAppLaunchData( + AppLaunchData.createLaunchDataForComponent( + "com.test", + "com.test.BookmarkTest" + ) + ) + .build() + ) + + inputDataStore.saveInputGestures(USER_ID, inputGestures) + assertEquals( + inputGestures, + inputDataStore.loadInputGestures(USER_ID), + getPrintableXml(inputGestures) + ) + } + + @Test + fun saveToDiskCombinedGestures() { + val inputGestures = listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_2, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_9, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) + .setAppLaunchData(AppLaunchData.createLaunchDataForCategory(Intent.CATEGORY_APP_CONTACTS)) + .build(), + ) + + inputDataStore.saveInputGestures(USER_ID, inputGestures) + assertEquals( + inputGestures, + inputDataStore.loadInputGestures(USER_ID), + getPrintableXml(inputGestures) + ) + } + + @Test + fun validXmlParse() { + val xmlData = """ + <?xml version='1.0' encoding='utf-8' standalone='yes' ?> + <root> + <input_gesture_list> + <input_gesture key_gesture_type="3"> + <key_trigger keycode="8" modifiers="69632" /> + </input_gesture> + <input_gesture key_gesture_type="21"> + <key_trigger keycode="9" modifiers="65536" /> + </input_gesture> + <input_gesture key_gesture_type="1"> + <touchpad_trigger touchpad_gesture_type="1" /> + </input_gesture> + </input_gesture_list> + </root>""".trimIndent() + val validInputGestures = listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_2, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + ) + val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) + assertEquals( + validInputGestures, + inputDataStore.readInputGesturesXml(inputStream, true) + ) + } + + @Test + fun missingTriggerData() { + val xmlData = """ + <?xml version='1.0' encoding='utf-8' standalone='yes' ?> + <root> + <input_gesture_list> + <input_gesture key_gesture_type="3"> + </input_gesture> + <input_gesture key_gesture_type="21"> + <key_trigger keycode="9" modifiers="65536" /> + </input_gesture> + <input_gesture key_gesture_type="1"> + <touchpad_trigger touchpad_gesture_type="1" /> + </input_gesture> + </input_gesture_list> + </root>""".trimIndent() + val validInputGestures = listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_2, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + ) + val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) + assertEquals( + validInputGestures, + inputDataStore.readInputGesturesXml(inputStream, true) + ) + } + + @Test + fun invalidKeycode() { + val xmlData = """ + <?xml version='1.0' encoding='utf-8' standalone='yes' ?> + <root> + <input_gesture_list> + <input_gesture key_gesture_type="3"> + <key_trigger keycode="8" modifiers="69632" /> + </input_gesture> + <input_gesture key_gesture_type="21"> + <key_trigger keycode="9999999" modifiers="65536" /> + </input_gesture> + <input_gesture key_gesture_type="1"> + <touchpad_trigger touchpad_gesture_type="1" /> + </input_gesture> + </input_gesture_list> + </root>""".trimIndent() + val validInputGestures = listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + ) + val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) + assertEquals( + validInputGestures, + inputDataStore.readInputGesturesXml(inputStream, true) + ) + } + + @Test + fun invalidTriggerName() { + val xmlData = """ + <?xml version='1.0' encoding='utf-8' standalone='yes' ?> + <root> + <input_gesture_list> + <input_gesture key_gesture_type="3"> + <key_trigger keycode="8" modifiers="69632" /> + </input_gesture> + <input_gesture key_gesture_type="21"> + <key_trigger keycode="9" modifiers="65536" /> + </input_gesture> + <input_gesture key_gesture_type="1"> + <invalid_trigger_name touchpad_gesture_type="1" /> + </input_gesture> + </input_gesture_list> + </root>""".trimIndent() + val validInputGestures = listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_2, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + ) + val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) + assertEquals( + validInputGestures, + inputDataStore.readInputGesturesXml(inputStream, true) + ) + } + + @Test + fun invalidTouchpadGestureType() { + val xmlData = """ + <?xml version='1.0' encoding='utf-8' standalone='yes' ?> + <root> + <input_gesture_list> + <input_gesture key_gesture_type="3"> + <key_trigger keycode="8" modifiers="69632" /> + </input_gesture> + <input_gesture key_gesture_type="21"> + <key_trigger keycode="9" modifiers="65536" /> + </input_gesture> + <input_gesture key_gesture_type="1"> + <touchpad_trigger touchpad_gesture_type="9999" /> + </input_gesture> + </input_gesture_list> + </root>""".trimIndent() + val validInputGestures = listOf( + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_1, + KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build(), + InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_2, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS) + .build(), + ) + val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) + assertEquals( + validInputGestures, + inputDataStore.readInputGesturesXml(inputStream, true) + ) + } + + @Test + fun emptyInputGestureList() { + val xmlData = """ + <?xml version='1.0' encoding='utf-8' standalone='yes' ?> + <root> + <input_gesture_list> + </input_gesture_list> + </root>""".trimIndent() + val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) + assertEquals( + listOf(), + inputDataStore.readInputGesturesXml(inputStream, true) + ) + } + + @Test + fun invalidTag() { + val xmlData = """ + <?xml version='1.0' encoding='utf-8' standalone='yes' ?> + <root> + <invalid_tag_name> + </invalid_tag_name> + </root>""".trimIndent() + val inputStream = ByteArrayInputStream(xmlData.toByteArray(Charsets.UTF_8)) + assertEquals( + listOf(), + inputDataStore.readInputGesturesXml(inputStream, true) + ) + } +}
\ No newline at end of file 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..e281a3fb1287 --- /dev/null +++ b/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt @@ -0,0 +1,210 @@ +/* + * 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, /* filter = */null) + ) + + inputGestureManager.removeCustomInputGesture(USER_ID, customGesture) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + ) + } + + @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, /* filter = */null) + ) + } + + @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, /* filter = */null) + ) + } + + @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, /* filter = */null) + ) + + inputGestureManager.removeAllCustomInputGestures(USER_ID, /* filter = */null) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + ) + } + + @Test + fun filteringBasedOnTouchpadOrKeyGestures() { + val customKeyGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customKeyGesture) + val customTouchpadGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customTouchpadGesture) + + assertEquals( + listOf(customTouchpadGesture, customKeyGesture), + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + ) + assertEquals( + listOf(customKeyGesture), + inputGestureManager.getCustomInputGestures(USER_ID, InputGestureData.Filter.KEY) + ) + assertEquals( + listOf(customTouchpadGesture), + inputGestureManager.getCustomInputGestures( + USER_ID, + InputGestureData.Filter.TOUCHPAD + ) + ) + + inputGestureManager.removeAllCustomInputGestures(USER_ID, InputGestureData.Filter.KEY) + assertEquals( + listOf(customTouchpadGesture), + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + ) + + inputGestureManager.removeAllCustomInputGestures( + USER_ID, + InputGestureData.Filter.TOUCHPAD + ) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + ) + } +}
\ 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 351ec4635977..eac426700ec1 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -28,11 +28,15 @@ import android.hardware.display.DisplayViewport import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager import android.hardware.input.InputManagerGlobal +import android.hardware.input.InputSettings +import android.hardware.input.KeyGestureEvent import android.os.InputEventInjectionSync +import android.os.PermissionEnforcer import android.os.SystemClock +import android.os.test.FakePermissionEnforcer import android.os.test.TestLooper -import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.Presubmit +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.view.View.OnKeyListener @@ -65,11 +69,13 @@ import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.Mockito.doReturn 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.verifyNoMoreInteractions import org.mockito.Mockito.verifyZeroInteractions import org.mockito.Mockito.`when` import org.mockito.stubbing.OngoingStubbing @@ -93,14 +99,16 @@ class InputManagerServiceTests { ) } - @JvmField - @Rule + @get:Rule val extendedMockitoRule = - ExtendedMockitoRule.Builder(this).mockStatic(LocalServices::class.java) - .mockStatic(PermissionChecker::class.java).build()!! + ExtendedMockitoRule.Builder(this) + .mockStatic(LocalServices::class.java) + .mockStatic(PermissionChecker::class.java) + .mockStatic(KeyCharacterMap::class.java) + .mockStatic(InputSettings::class.java) + .build()!! - @JvmField - @Rule + @get:Rule val setFlagsRule = SetFlagsRule() @get:Rule @@ -124,23 +132,35 @@ class InputManagerServiceTests { @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 private lateinit var testLooper: TestLooper private lateinit var contentResolver: MockContentResolver private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + private lateinit var fakePermissionEnforcer: FakePermissionEnforcer @Before fun setup() { context = spy(ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext())) + fakePermissionEnforcer = FakePermissionEnforcer() + doReturn(Context.PERMISSION_ENFORCER_SERVICE).`when`(context).getSystemServiceName( + eq(PermissionEnforcer::class.java) + ) + doReturn(fakePermissionEnforcer).`when`(context).getSystemService( + eq(Context.PERMISSION_ENFORCER_SERVICE) + ) + contentResolver = MockContentResolver(context) contentResolver.addProvider(Settings.AUTHORITY, FakeSettingsProvider()) whenever(context.contentResolver).thenReturn(contentResolver) testLooper = TestLooper() service = InputManagerService(object : InputManagerService.Injector( - context, testLooper.looper, uEventManager) { + context, testLooper.looper, testLooper.looper, uEventManager) { override fun getNativeService( service: InputManagerService? ): NativeInputManagerService { @@ -152,12 +172,11 @@ class InputManagerServiceTests { } override fun getKeyboardBacklightController( - nativeService: NativeInputManagerService?, - dataStore: PersistentDataStore? + nativeService: NativeInputManagerService? ): InputManagerService.KeyboardBacklightControllerInterface { return kbdController } - }) + }, fakePermissionEnforcer) inputManagerGlobalSession = InputManagerGlobal.createTestSession(service) val inputManager = InputManager(context) whenever(context.getSystemService(InputManager::class.java)).thenReturn(inputManager) @@ -173,6 +192,9 @@ class InputManagerServiceTests { 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) @@ -208,6 +230,8 @@ class InputManagerServiceTests { verify(native).setTouchpadTapDraggingEnabled(anyBoolean()) verify(native).setShouldNotifyTouchpadHardwareState(anyBoolean()) verify(native).setTouchpadRightClickZoneEnabled(anyBoolean()) + verify(native).setTouchpadThreeFingerTapShortcutEnabled(anyBoolean()) + verify(native).setTouchpadSystemGesturesEnabled(anyBoolean()) verify(native).setShowTouches(anyBoolean()) verify(native).setMotionClassifierEnabled(anyBoolean()) verify(native).setMaximumObscuringOpacityForTouch(anyFloat()) @@ -304,7 +328,75 @@ class InputManagerServiceTests { } } - private fun createVirtualDisplays(count: Int): List<VirtualDisplay> { + @Test + @EnableFlags(com.android.hardware.input.Flags.FLAG_KEY_EVENT_ACTIVITY_DETECTION) + fun testKeyActivenessNotifyEventsLifecycle() { + service.systemRunning() + + fakePermissionEnforcer.grant(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY) + + val inputManager = context.getSystemService(InputManager::class.java) + + /* register for key event activeness */ + var listener = mock(InputManager.KeyEventActivityListener::class.java) + assertEquals(true, inputManager.registerKeyEventActivityListener(listener)) + + /* mimic key event pressed */ + val event = createKeycodeAEvent(createInputDevice(), KeyEvent.ACTION_DOWN) + service.interceptKeyBeforeQueueing(event, 0) + + /* verify onKeyEventActivity callback called */ + verify(listener, times(1)).onKeyEventActivity() + + /* unregister for key event activeness */ + assertEquals(true, inputManager.unregisterKeyEventActivityListener(listener)) + + /* mimic key event pressed */ + service.interceptKeyBeforeQueueing(event, /* policyFlags */ 0) + + /* verify onKeyEventActivity callback not called */ + verifyNoMoreInteractions(listener) + } + + @Test + fun testKeyEventsForwardedToFocusedWindow_whenWmAllows() { + service.systemRunning() + overrideSendActionKeyEventsToFocusedWindow( + /* hasPermission = */false, + /* hasPrivateFlag = */false + ) + whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(0) + + val event = KeyEvent( /* downTime= */0, /* eventTime= */0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SPACE, /* repeat= */0, KeyEvent.META_CTRL_ON) + assertEquals(0, service.interceptKeyBeforeDispatching(null, event, 0)) + } + + @Test + fun testKeyEventsNotForwardedToFocusedWindow_whenWmConsumes() { + service.systemRunning() + overrideSendActionKeyEventsToFocusedWindow( + /* hasPermission = */false, + /* hasPrivateFlag = */false + ) + whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(-1) + + val event = KeyEvent( /* downTime= */0, /* eventTime= */0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SPACE, /* repeat= */0, KeyEvent.META_CTRL_ON) + assertEquals(-1, service.interceptKeyBeforeDispatching(null, event, 0)) + } + + private class AutoClosingVirtualDisplays(val displays: List<VirtualDisplay>) : AutoCloseable { + operator fun get(i: Int): VirtualDisplay = displays[i] + + override fun close() { + for (display in displays) { + display.release() + } + } + } + + private fun createVirtualDisplays(count: Int): AutoClosingVirtualDisplays { val displayManager: DisplayManager = context.getSystemService( DisplayManager::class.java ) as DisplayManager @@ -319,7 +411,7 @@ class InputManagerServiceTests { /* flags= */ 0 )) } - return virtualDisplays + return AutoClosingVirtualDisplays(virtualDisplays) } // Helper function that creates a KeyEvent with Keycode A with the given action @@ -364,50 +456,51 @@ class InputManagerServiceTests { val mockSurfaceHolder2 = mock(SurfaceHolder::class.java) `when`(mockSurfaceView2.holder).thenReturn(mockSurfaceHolder2) - val virtualDisplays = createVirtualDisplays(2) - - // Simulate an InputDevice - val inputDevice = createInputDevice() + createVirtualDisplays(2).use { virtualDisplays -> + // Simulate an InputDevice + val inputDevice = createInputDevice() - // Associate input device with display - service.addUniqueIdAssociationByDescriptor( - inputDevice.descriptor, - virtualDisplays[0].display.displayId.toString() - ) + // Associate input device with display + service.addUniqueIdAssociationByDescriptor( + inputDevice.descriptor, + virtualDisplays[0].display.displayId.toString() + ) - // Simulate 2 different KeyEvents - val downEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_DOWN) - val upEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_UP) + // Simulate 2 different KeyEvents + val downEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_DOWN) + val upEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_UP) - // Create a mock OnKeyListener object - val mockOnKeyListener = mock(OnKeyListener::class.java) + // Create a mock OnKeyListener object + val mockOnKeyListener = mock(OnKeyListener::class.java) - // Verify that the event went to Display 1 not Display 2 - service.injectInputEvent(downEvent, InputEventInjectionSync.NONE) + // Verify that the event went to Display 1 not Display 2 + service.injectInputEvent(downEvent, InputEventInjectionSync.NONE) - // Call the onKey method on the mock OnKeyListener object - mockOnKeyListener.onKey(mockSurfaceView1, /* keyCode= */ KeyEvent.KEYCODE_A, downEvent) - mockOnKeyListener.onKey(mockSurfaceView2, /* keyCode= */ KeyEvent.KEYCODE_A, upEvent) + // Call the onKey method on the mock OnKeyListener object + mockOnKeyListener.onKey(mockSurfaceView1, /* keyCode= */ KeyEvent.KEYCODE_A, downEvent) + mockOnKeyListener.onKey(mockSurfaceView2, /* keyCode= */ KeyEvent.KEYCODE_A, upEvent) - // Verify that the onKey method was called with the expected arguments - verify(mockOnKeyListener).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, downEvent) - verify(mockOnKeyListener, never()).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, downEvent) + // Verify that the onKey method was called with the expected arguments + verify(mockOnKeyListener).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, downEvent) + verify(mockOnKeyListener, never()) + .onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, downEvent) - // Remove association - service.removeUniqueIdAssociationByDescriptor(inputDevice.descriptor) + // Remove association + service.removeUniqueIdAssociationByDescriptor(inputDevice.descriptor) - // Associate with Display 2 - service.addUniqueIdAssociationByDescriptor( - inputDevice.descriptor, - virtualDisplays[1].display.displayId.toString() - ) + // Associate with Display 2 + service.addUniqueIdAssociationByDescriptor( + inputDevice.descriptor, + virtualDisplays[1].display.displayId.toString() + ) - // Simulate a KeyEvent - service.injectInputEvent(upEvent, InputEventInjectionSync.NONE) + // Simulate a KeyEvent + service.injectInputEvent(upEvent, InputEventInjectionSync.NONE) - // Verify that the event went to Display 2 not Display 1 - verify(mockOnKeyListener).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, upEvent) - verify(mockOnKeyListener, never()).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, upEvent) + // Verify that the event went to Display 2 not Display 1 + verify(mockOnKeyListener).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, upEvent) + verify(mockOnKeyListener, never()).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, upEvent) + } } @Test @@ -426,79 +519,141 @@ class InputManagerServiceTests { val mockSurfaceHolder2 = mock(SurfaceHolder::class.java) `when`(mockSurfaceView2.holder).thenReturn(mockSurfaceHolder2) - val virtualDisplays = createVirtualDisplays(2) - - // Simulate an InputDevice - val inputDevice = createInputDevice() + createVirtualDisplays(2).use { virtualDisplays -> + // Simulate an InputDevice + val inputDevice = createInputDevice() - // Associate input device with display - service.addUniqueIdAssociationByPort( - inputDevice.name, - virtualDisplays[0].display.displayId.toString() - ) + // Associate input device with display + service.addUniqueIdAssociationByPort( + inputDevice.name, + virtualDisplays[0].display.displayId.toString() + ) - // Simulate 2 different KeyEvents - val downEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_DOWN) - val upEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_UP) + // Simulate 2 different KeyEvents + val downEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_DOWN) + val upEvent = createKeycodeAEvent(inputDevice, KeyEvent.ACTION_UP) - // Create a mock OnKeyListener object - val mockOnKeyListener = mock(OnKeyListener::class.java) + // Create a mock OnKeyListener object + val mockOnKeyListener = mock(OnKeyListener::class.java) - // Verify that the event went to Display 1 not Display 2 - service.injectInputEvent(downEvent, InputEventInjectionSync.NONE) + // Verify that the event went to Display 1 not Display 2 + service.injectInputEvent(downEvent, InputEventInjectionSync.NONE) - // Call the onKey method on the mock OnKeyListener object - mockOnKeyListener.onKey(mockSurfaceView1, /* keyCode= */ KeyEvent.KEYCODE_A, downEvent) - mockOnKeyListener.onKey(mockSurfaceView2, /* keyCode= */ KeyEvent.KEYCODE_A, upEvent) + // Call the onKey method on the mock OnKeyListener object + mockOnKeyListener.onKey(mockSurfaceView1, /* keyCode= */ KeyEvent.KEYCODE_A, downEvent) + mockOnKeyListener.onKey(mockSurfaceView2, /* keyCode= */ KeyEvent.KEYCODE_A, upEvent) - // Verify that the onKey method was called with the expected arguments - verify(mockOnKeyListener).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, downEvent) - verify(mockOnKeyListener, never()).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, downEvent) + // Verify that the onKey method was called with the expected arguments + verify(mockOnKeyListener).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, downEvent) + verify(mockOnKeyListener, never()) + .onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, downEvent) - // Remove association - service.removeUniqueIdAssociationByPort(inputDevice.name) + // Remove association + service.removeUniqueIdAssociationByPort(inputDevice.name) - // Associate with Display 2 - service.addUniqueIdAssociationByPort( - inputDevice.name, - virtualDisplays[1].display.displayId.toString() - ) + // Associate with Display 2 + service.addUniqueIdAssociationByPort( + inputDevice.name, + virtualDisplays[1].display.displayId.toString() + ) - // Simulate a KeyEvent - service.injectInputEvent(upEvent, InputEventInjectionSync.NONE) + // Simulate a KeyEvent + service.injectInputEvent(upEvent, InputEventInjectionSync.NONE) - // Verify that the event went to Display 2 not Display 1 - verify(mockOnKeyListener).onKey(mockSurfaceView2, KeyEvent.KEYCODE_A, upEvent) - verify(mockOnKeyListener, never()).onKey(mockSurfaceView1, KeyEvent.KEYCODE_A, upEvent) + // Verify that the event went to Display 2 not Display 1 + 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) + val backlightDownEvent = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN) + .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + .build() + service.handleKeyGestureEvent(backlightDownEvent) verify(kbdController).decrementKeyboardBacklight(anyInt()) - val backlightUpEvent = createKeyEvent(KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP) - service.interceptKeyBeforeDispatching(null, backlightUpEvent, /* policyFlags = */0) + val backlightUpEvent = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP) + .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + .build() + service.handleKeyGestureEvent(backlightUpEvent) verify(kbdController).incrementKeyboardBacklight(anyInt()) } @Test - @EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER) - fun handleKeyGestures_toggleCapsLock() { - service.systemRunning() + fun handleKeyGestures_a11yBounceKeysShortcut() { + val toggleBounceKeysEvent = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS) + .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + .build() + service.handleKeyGestureEvent(toggleBounceKeysEvent) + ExtendedMockito.verify { + InputSettings.setAccessibilityBounceKeysThreshold( + any(), + eq(InputSettings.DEFAULT_BOUNCE_KEYS_THRESHOLD_MILLIS) + ) + } + } - 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) + @Test + fun handleKeyGestures_a11yMouseKeysShortcut() { + ExtendedMockito.doReturn(true).`when` { + InputSettings.isAccessibilityMouseKeysFeatureFlagEnabled() + } + val toggleMouseKeysEvent = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS) + .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + .build() + service.handleKeyGestureEvent(toggleMouseKeysEvent) + ExtendedMockito.verify { + InputSettings.setAccessibilityMouseKeysEnabled(any(), eq(true)) + } + } + + @Test + fun handleKeyGestures_a11yStickyKeysShortcut() { + val toggleStickyKeysEvent = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS) + .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + .build() + service.handleKeyGestureEvent(toggleStickyKeysEvent) + ExtendedMockito.verify { + InputSettings.setAccessibilityStickyKeysEnabled(any(), eq(true)) + } + } + + @Test + fun handleKeyGestures_a11ySlowKeysShortcut() { + val toggleSlowKeysEvent = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS) + .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + .build() + service.handleKeyGestureEvent(toggleSlowKeysEvent) + ExtendedMockito.verify { + InputSettings.setAccessibilitySlowKeysThreshold( + any(), + eq(InputSettings.DEFAULT_SLOW_KEYS_THRESHOLD_MILLIS) + ) + } + } + + @Test + fun handleKeyGestures_toggleCapsLock() { + val toggleCapsLockEvent = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK) + .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + .build() + service.handleKeyGestureEvent(toggleCapsLockEvent) verify(native).toggleCapsLock(anyInt()) } @@ -533,29 +688,11 @@ class InputManagerServiceTests { 0 }, "title", - /* uid = */0 + /* uid = */0, + /* inputFeatureFlags = */ 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 index ba360070abc3..c666fb7e05f1 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -16,31 +16,50 @@ package com.android.server.input +import android.app.role.RoleManager import android.content.Context import android.content.ContextWrapper -import android.hardware.input.IInputManager +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.hardware.input.KeyGestureEvent.KeyGestureType 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.util.AtomicFile 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 java.io.File +import java.io.FileOutputStream +import java.io.InputStream import junitparams.JUnitParamsRunner import junitparams.Parameters +import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule @@ -78,35 +97,104 @@ class KeyGestureControllerTests { 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).build()!! + .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 packageManager: PackageManager + private var currentPid = 0 - private lateinit var keyGestureController: KeyGestureController private lateinit var context: Context + private lateinit var resources: Resources + private lateinit var keyGestureController: KeyGestureController private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession private lateinit var testLooper: TestLooper + private lateinit var tempFile: File + private lateinit var inputDataStore: InputDataStore private var events = mutableListOf<KeyGestureEvent>() - private var handleEvents = mutableListOf<KeyGestureEvent>() @Before fun setup() { context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) - inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + resources = Mockito.spy(context.resources) setupInputDevices() + setupBehaviors() testLooper = TestLooper() currentPid = Process.myPid() - keyGestureController = KeyGestureController(context, testLooper.looper) + tempFile = File.createTempFile("input_gestures", ".xml") + inputDataStore = + InputDataStore(object : InputDataStore.FileInjector("input_gestures.xml") { + private val atomicFile: AtomicFile = AtomicFile(tempFile) + + override fun openRead(userId: Int): InputStream? { + return atomicFile.openRead() + } + + override fun startWrite(userId: Int): FileOutputStream? { + return atomicFile.startWrite() + } + + override fun finishWrite(userId: Int, fos: FileOutputStream?, success: Boolean) { + if (success) { + atomicFile.finishWrite(fos) + } else { + atomicFile.failWrite(fos) + } + } + + override fun getAtomicFileForUserId(userId: Int): AtomicFile { + return atomicFile + } + }) + } + + @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) + 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 setupBookmarks(bookmarkRes: Int) { + val testBookmarks: XmlResourceParser = context.resources.getXml(bookmarkRes) + Mockito.`when`(resources.getXml(R.xml.bookmarks)).thenReturn(testBookmarks) } 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) @@ -114,16 +202,29 @@ class KeyGestureControllerTests { 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, testLooper.looper, inputDataStore) + Mockito.`when`(iInputManager.getAppLaunchBookmarks()) + .thenReturn(keyGestureController.appLaunchBookmarks) + keyGestureController.systemRunning() + testLooper.dispatchAll() } private fun notifyHomeGestureCompleted() { - keyGestureController.notifyKeyGestureCompleted(DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H), + keyGestureController.notifyKeyGestureCompleted( + DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H), KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON, - KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + KeyGestureEvent.KEY_GESTURE_TYPE_HOME + ) } @Test fun testKeyGestureEvent_registerUnregisterListener() { + setupKeyGestureController() val listener = KeyGestureEventListener() // Register key gesture event listener @@ -155,20 +256,22 @@ class KeyGestureControllerTests { @Test fun testKeyGestureEvent_multipleGestureHandlers() { + setupKeyGestureController() + // Set up two callbacks. var callbackCount1 = 0 var callbackCount2 = 0 var selfCallback = 0 val externalHandler1 = KeyGestureHandler { _, _ -> - callbackCount1++; + callbackCount1++ true } val externalHandler2 = KeyGestureHandler { _, _ -> - callbackCount2++; + callbackCount2++ true } val selfHandler = KeyGestureHandler { _, _ -> - selfCallback++; + selfCallback++ false } @@ -184,7 +287,7 @@ class KeyGestureControllerTests { 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 + /* focusedToken = */ null, /* flags = */ 0, /* appLaunchData = */null ) assertEquals( @@ -211,12 +314,13 @@ class KeyGestureControllerTests { val expectedKeys: IntArray, val expectedModifierState: Int, val expectedActions: IntArray, + val expectedAppLaunchData: AppLaunchData? = null, ) { override fun toString(): String = name } @Keep - private fun keyGestureEventHandlerTestArguments(): Array<TestData> { + private fun systemGesturesTestArguments(): Array<TestData> { return arrayOf( TestData( "META + A -> Launch Assistant", @@ -227,25 +331,6 @@ class KeyGestureControllerTests { intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), 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( "META + H -> Go Home", intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_H), KeyGestureEvent.KEY_GESTURE_TYPE_HOME, @@ -286,27 +371,14 @@ class KeyGestureControllerTests { intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "META + CTRL + N -> Open Notes", + "META + S -> Take Screenshot", 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, + KeyEvent.META_META_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( @@ -382,30 +454,479 @@ class KeyGestureControllerTests { intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( - "META + ALT + DPAD_LEFT -> Change Splitscreen Focus Left", + "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_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_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_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_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 + 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 + 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_DPAD_LEFT + KeyEvent.KEYCODE_3 ), - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT, - intArrayOf(KeyEvent.KEYCODE_DPAD_LEFT), + 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 + CTRL + DPAD_RIGHT -> Change Splitscreen Focus Right", + "Meta + Alt + 4 -> Toggle Mouse Keys", intArrayOf( KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_DPAD_RIGHT + KeyEvent.KEYCODE_4 ), - KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT, - intArrayOf(KeyEvent.KEYCODE_DPAD_RIGHT), + 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( + "META + [ -> Resizes a task to fit the left half of the screen", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_LEFT_BRACKET + ), + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ] -> Resizes a task to fit the right half of the screen", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_RIGHT_BRACKET + ), + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + '=' -> Toggles maximization of a task to maximized and restore its bounds", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_EQUALS + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_EQUALS), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + '-' -> Minimizes a freeform task", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_MINUS + ), + KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_MINUS), + KeyEvent.META_META_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ALT + M -> Toggle Magnification", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_M + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION, + intArrayOf(KeyEvent.KEYCODE_M), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + TestData( + "META + ALT + S -> Activate Select to Speak", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_S + ), + KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK, + intArrayOf(KeyEvent.KEYCODE_S), KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) ), TestData( + "META + ALT + 'V' -> Toggle Voice Access", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_V + ), + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS, + intArrayOf(KeyEvent.KEYCODE_V), + KeyEvent.META_META_ON or 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_MOUSE_KEYS, + com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, + com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, + 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_MOUSE_KEYS, + com.android.hardware.input.Flags.FLAG_ENABLE_TALKBACK_AND_MAGNIFIER_KEY_GESTURES, + com.android.hardware.input.Flags.FLAG_ENABLE_VOICE_ACCESS_KEY_GESTURES, + 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 bookmarkArguments(): Array<TestData> { + return arrayOf( + 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 + P -> Launch Default Contacts", + 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_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 + C -> Launch Default Calendar", + 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_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 + 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 + P -> Launch Default Contacts", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_P + ), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_P), + 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") + ) + ) + } + + @Test + @Parameters(method = "bookmarkArguments") + fun testBookmarks(test: TestData) { + setupBookmarks(com.android.test.input.R.xml.bookmarks) + setupKeyGestureController() + testKeyGestureInternal(test) + } + + @Test + @Parameters(method = "bookmarkArguments") + fun testBookmarksLegacy(test: TestData) { + setupBookmarks(com.android.test.input.R.xml.bookmarks_legacy) + setupKeyGestureController() + testKeyGestureInternal(test) + } + + @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, @@ -494,35 +1015,285 @@ class KeyGestureControllerTests { 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), + "SYSRQ -> Take screenshot", + intArrayOf(KeyEvent.KEYCODE_SYSRQ), + KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, + intArrayOf(KeyEvent.KEYCODE_SYSRQ), 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), + "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( - "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, + "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) + ), + TestData( + "LOCK -> Lock Screen", + intArrayOf(KeyEvent.KEYCODE_LOCK), + KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, + intArrayOf(KeyEvent.KEYCODE_LOCK), + 0, 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, + "FULLSCREEN -> Turns a task into fullscreen", + intArrayOf(KeyEvent.KEYCODE_FULLSCREEN), + KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION, + intArrayOf(KeyEvent.KEYCODE_FULLSCREEN), + 0, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ), + ) + } + + @Test + @Parameters(method = "systemKeysTestArguments") + @EnableFlags(com.android.hardware.input.Flags.FLAG_ENABLE_NEW_25Q2_KEYCODES) + 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, + KeyEvent.KEYCODE_DO_NOT_DISTURB, + KeyEvent.KEYCODE_LOCK, + KeyEvent.KEYCODE_FULLSCREEN + ) + + 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 @@ -532,24 +1303,351 @@ class KeyGestureControllerTests { } @Test - @Parameters(method = "keyGestureEventHandlerTestArguments") - fun testKeyGestures(test: TestData) { + @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_KEY_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 trigger = InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) + val builder = InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger(trigger) + if (test.expectedAppLaunchData != null) { + builder.setAppLaunchData(test.expectedAppLaunchData) + } + val inputGestureData = builder.build() + + assertNull( + test.toString(), + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) + assertEquals( + test.toString(), + InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, + keyGestureController.addCustomInputGesture(0, builder.build().aidlData) + ) + assertEquals( + test.toString(), + inputGestureData.aidlData, + keyGestureController.getInputGesture(0, trigger.aidlTrigger) + ) + testKeyGestureInternal(test) + } + + @Test + @Parameters(method = "customInputGesturesTestArguments") + fun testCustomKeyGesturesSavedAndLoadedByController(test: TestData) { + val userId = 10 + 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.setCurrentUserId(userId) + testLooper.dispatchAll() + keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData) + testLooper.dispatchAll() + + // Reinitialize the gesture controller simulating a login/logout for the user. + setupKeyGestureController() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + val savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 1, + savedInputGestures.size + ) + assertEquals( + "Test: $test doesn't produce correct input gesture data", inputGestureData, + InputGestureData(savedInputGestures[0]) + ) + } + + @Test + @Parameters(method = "customInputGesturesTestArguments") + fun testCustomKeyGestureRestoredFromBackup(test: TestData) { + val userId = 10 + 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.setCurrentUserId(userId) + testLooper.dispatchAll() + keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData) + testLooper.dispatchAll() + val backupData = keyGestureController.getInputGestureBackupPayload(userId) + + // Delete the old data and reinitialize the controller simulating a "fresh" install. + tempFile.delete() + setupKeyGestureController() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + + // Initially there should be no gestures registered. + var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 0, + savedInputGestures.size + ) + + // After the restore, there should be the original gesture re-registered. + keyGestureController.applyInputGesturesBackupPayload(backupData, userId) + savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 1, + savedInputGestures.size + ) + assertEquals( + "Test: $test doesn't produce correct input gesture data", inputGestureData, + InputGestureData(savedInputGestures[0]) + ) + } + + 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, _ -> - handleEvents.add(KeyGestureEvent(event)) + handledEvents.add(KeyGestureEvent(event)) true } keyGestureController.registerKeyGestureHandler(handler, 0) - handleEvents.clear() + handledEvents.clear() - sendKeys(test.keys, /* assertAllConsumed = */ false) + 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) + } + + @Test + @Parameters(method = "customTouchpadGesturesTestArguments") + fun testCustomTouchpadGesturesSavedAndLoadedByController(test: TouchpadTestData) { + val userId = 10 + 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.setCurrentUserId(userId) + testLooper.dispatchAll() + keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData) + testLooper.dispatchAll() + + // Reinitialize the gesture controller simulating a login/logout for the user. + setupKeyGestureController() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + val savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 1, + savedInputGestures.size + ) + assertEquals( + "Test: $test doesn't produce correct input gesture data", inputGestureData, + InputGestureData(savedInputGestures[0]) + ) + } + + + @Test + @Parameters(method = "customTouchpadGesturesTestArguments") + fun testCustomTouchpadGesturesRestoredFromBackup(test: TouchpadTestData) { + val userId = 10 + 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.setCurrentUserId(userId) + testLooper.dispatchAll() + keyGestureController.addCustomInputGesture(userId, inputGestureData.aidlData) + testLooper.dispatchAll() + val backupData = keyGestureController.getInputGestureBackupPayload(userId) + + // Delete the old data and reinitialize the controller simulating a "fresh" install. + tempFile.delete() + setupKeyGestureController() + keyGestureController.setCurrentUserId(userId) + testLooper.dispatchAll() + + // Initially there should be no gestures registered. + var savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 0, + savedInputGestures.size + ) + + // After the restore, there should be the original gesture re-registered. + keyGestureController.applyInputGesturesBackupPayload(backupData, userId) + savedInputGestures = keyGestureController.getCustomInputGestures(userId, null) + assertEquals( + "Test: $test doesn't produce correct number of saved input gestures", + 1, + savedInputGestures.size + ) + assertEquals( + "Test: $test doesn't produce correct input gesture data", inputGestureData, + InputGestureData(savedInputGestures[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, - handleEvents.size + handledEvents.size ) - for (i in handleEvents.indices) { - val event = handleEvents[i] + for (i in handledEvents.indices) { + val event = handledEvents[i] assertArrayEquals( "Test: $test doesn't produce correct key gesture keycodes", test.expectedKeys, @@ -570,60 +1668,39 @@ class KeyGestureControllerTests { test.expectedActions[i], event.action ) + assertEquals( + "Test: $test doesn't produce correct app launch data", + test.expectedAppLaunchData, + event.appLaunchData + ) } keyGestureController.unregisterKeyGestureHandler(handler, 0) } - @Test - fun testKeycodesFullyConsumed_irrespectiveOfHandlers() { - 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 } + 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() - for (key in testKeys) { - sendKeys(intArrayOf(key), /* assertAllConsumed = */ true) - } + sendKeys(testKeys) + assertEquals("Test: $testName should not produce Key gesture", 0, handledEvents.size) } - private fun sendKeys(testKeys: IntArray, assertAllConsumed: Boolean) { + private fun sendKeys(testKeys: IntArray, assertNotSentToApps: Boolean = false) { var metaState = 0 + val now = SystemClock.uptimeMillis() for (key in testKeys) { val downEvent = KeyEvent( - /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_DOWN, key, - 0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, - 0 /*flags*/, InputDevice.SOURCE_KEYBOARD + now, now, KeyEvent.ACTION_DOWN, key, 0 /*repeat*/, metaState, + DEVICE_ID, 0 /*scancode*/, 0 /*flags*/, + InputDevice.SOURCE_KEYBOARD ) - val consumed = - keyGestureController.interceptKeyBeforeDispatching(null, downEvent, 0) == -1L - if (assertAllConsumed) { - assertTrue( - "interceptKeyBeforeDispatching should consume all events $downEvent", - consumed - ) - } + interceptKey(downEvent, assertNotSentToApps) metaState = metaState or MODIFIER.getOrDefault(key, 0) downEvent.recycle() @@ -632,24 +1709,35 @@ class KeyGestureControllerTests { for (key in testKeys.reversed()) { val upEvent = KeyEvent( - /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_UP, key, - 0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, - 0 /*flags*/, InputDevice.SOURCE_KEYBOARD + now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState, + DEVICE_ID, 0 /*scancode*/, 0 /*flags*/, + InputDevice.SOURCE_KEYBOARD ) - val consumed = - keyGestureController.interceptKeyBeforeDispatching(null, upEvent, 0) == -1L - if (assertAllConsumed) { - assertTrue( - "interceptKeyBeforeDispatching should consume all events $upEvent", - consumed - ) - } + 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)) @@ -667,4 +1755,4 @@ class KeyGestureControllerTests { 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..644d5a0679de 100644 --- a/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -19,25 +19,27 @@ package com.android.server.input import android.animation.ValueAnimator import android.content.Context import android.content.ContextWrapper +import android.content.res.Resources 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.SystemProperties import android.os.UEventObserver import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.view.InputDevice +import android.util.TypedValue import androidx.test.annotation.UiThreadTest import androidx.test.core.app.ApplicationProvider +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.internal.R +import com.android.modules.utils.testing.ExtendedMockitoRule 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 org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -47,15 +49,11 @@ import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.Mockito.any +import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.anyInt import org.mockito.Mockito.eq import org.mockito.Mockito.spy import org.mockito.Mockito.`when` -import org.mockito.junit.MockitoJUnit -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream private fun createKeyboard(deviceId: Int): InputDevice = InputDevice.Builder() @@ -96,22 +94,24 @@ class KeyboardBacklightControllerTests { const val LIGHT_ID = 2 const val SECOND_LIGHT_ID = 3 const val MAX_BRIGHTNESS = 255 + const val USER_INACTIVITY_THRESHOLD_MILLIS = 30000 } @get:Rule - val rule = MockitoJUnit.rule()!! + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this).mockStatic(SystemProperties::class.java).build()!! + @get:Rule + val inputManagerRule = MockInputManagerRule() @Mock - private lateinit var iInputManager: IInputManager - @Mock private lateinit var native: NativeInputManagerService @Mock private lateinit var uEventManager: UEventManager + @Mock + private lateinit var resources: Resources private lateinit var keyboardBacklightController: KeyboardBacklightController private lateinit var context: Context - private lateinit var dataStore: PersistentDataStore private lateinit var testLooper: TestLooper - private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession private var lightColorMap: HashMap<Int, Int> = HashMap() private var lastBacklightState: KeyboardBacklightState? = null private var sysfsNodeChanges = 0 @@ -120,24 +120,12 @@ class KeyboardBacklightControllerTests { @Before fun setup() { context = spy(ContextWrapper(ApplicationProvider.getApplicationContext())) - dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { - override fun openRead(): InputStream? { - throw FileNotFoundException() - } - - override fun startWrite(): FileOutputStream? { - throw IOException() - } - - override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} - }) + `when`(context.resources).thenReturn(resources) testLooper = TestLooper() - keyboardBacklightController = KeyboardBacklightController(context, native, dataStore, - testLooper.looper, FakeAnimatorFactory(), uEventManager) - inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + setupConfig() 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,270 +140,191 @@ class KeyboardBacklightControllerTests { } } - @After - fun tearDown() { - if (this::inputManagerGlobalSession.isInitialized) { - inputManagerGlobalSession.close() + private fun setupConfig() { + val brightnessValues = intArrayOf(100, 200, 0) + val decreaseThresholds = intArrayOf(-1, 900, 1900) + val increaseThresholds = intArrayOf(1000, 2000, -1) + `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightBrightnessValues)) + .thenReturn(brightnessValues) + `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightDecreaseLuxThreshold)) + .thenReturn(decreaseThresholds) + `when`(resources.getIntArray(R.array.config_autoKeyboardBacklightIncreaseLuxThreshold)) + .thenReturn(increaseThresholds) + `when`(resources.getInteger(R.integer.config_keyboardBacklightTimeoutMs)) + .thenReturn(USER_INACTIVITY_THRESHOLD_MILLIS) + `when`( + resources.getValue( + eq(R.dimen.config_autoKeyboardBrightnessSmoothingConstant), + any(TypedValue::class.java), + anyBoolean() + ) + ).then { + val args = it.arguments + val outValue = args[1] as TypedValue + outValue.data = java.lang.Float.floatToRawIntBits(1.0f) + Unit } } + private fun setupController() { + keyboardBacklightController = KeyboardBacklightController(context, native, + testLooper.looper, FakeAnimatorFactory(), uEventManager) + } + @Test fun testKeyboardBacklightIncrementDecrement() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, - DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL) - } + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL) } @Test fun testKeyboardWithoutBacklight() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = false - ).use { - val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) - val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - incrementKeyboardBacklight(DEVICE_ID) - assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) - } + setupController() + val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) + val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + incrementKeyboardBacklight(DEVICE_ID) + assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) } @Test fun testKeyboardWithMultipleLight() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn( - listOf( - keyboardBacklight, - keyboardInputLight - ) - ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - incrementKeyboardBacklight(DEVICE_ID) - assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) - assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) - assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) - } - } - - @Test - fun testRestoreBacklightOnInputDeviceAdded() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - - for (level in 1 until DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size) { - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1 - ) - - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the " + - "data store", - Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) - } - } - } - - @Test - fun testRestoreBacklightOnInputDeviceChanged() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) - - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertTrue( - "Keyboard backlight should not be changed until its added", - lightColorMap.isEmpty() + setupController() + 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`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn( + listOf( + keyboardBacklight, + keyboardInputLight ) + ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data store", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - } + incrementKeyboardBacklight(DEVICE_ID) + assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) + assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) + assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) } @Test fun testKeyboardBacklight_registerUnregisterListener() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - val maxLevel = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size - 1 - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - // Register backlight listener - val listener = KeyboardBacklightListener() - keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) - - lastBacklightState = null - keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) - testLooper.dispatchNext() + setupController() + 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`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + // Register backlight listener + val listener = KeyboardBacklightListener() + keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) + + lastBacklightState = null + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() - assertEquals( - "Backlight state device Id should be $DEVICE_ID", - DEVICE_ID, - lastBacklightState!!.deviceId - ) - assertEquals( - "Backlight state brightnessLevel should be 1", - 1, - lastBacklightState!!.brightnessLevel - ) - assertEquals( - "Backlight state maxBrightnessLevel should be $maxLevel", - maxLevel, - lastBacklightState!!.maxBrightnessLevel - ) - assertEquals( - "Backlight state isTriggeredByKeyPress should be true", - true, - lastBacklightState!!.isTriggeredByKeyPress - ) + assertEquals( + "Backlight state device Id should be $DEVICE_ID", + DEVICE_ID, + lastBacklightState!!.deviceId + ) + assertEquals( + "Backlight state brightnessLevel should be 1", + 1, + lastBacklightState!!.brightnessLevel + ) + assertEquals( + "Backlight state maxBrightnessLevel should be $maxLevel", + maxLevel, + lastBacklightState!!.maxBrightnessLevel + ) + assertEquals( + "Backlight state isTriggeredByKeyPress should be true", + true, + lastBacklightState!!.isTriggeredByKeyPress + ) - // Unregister listener - keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) + // Unregister listener + keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) - lastBacklightState = null - incrementKeyboardBacklight(DEVICE_ID) + lastBacklightState = null + incrementKeyboardBacklight(DEVICE_ID) - assertNull("Listener should not receive any updates", lastBacklightState) - } + assertNull("Listener should not receive any updates", lastBacklightState) } @Test fun testKeyboardBacklight_userActivity() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) - - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data store", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + incrementKeyboardBacklight(DEVICE_ID) + assertNotEquals( + "Keyboard backlight level should be incremented to a non-zero value", + 0, + lightColorMap[LIGHT_ID] + ) - testLooper.moveTimeForward(USER_INACTIVITY_THRESHOLD_MILLIS + 1000) - testLooper.dispatchNext() - assertEquals( - "Keyboard backlight level should be turned off after inactivity", - 0, - lightColorMap[LIGHT_ID] - ) - } + testLooper.moveTimeForward((USER_INACTIVITY_THRESHOLD_MILLIS + 1000).toLong()) + testLooper.dispatchNext() + assertEquals( + "Keyboard backlight level should be turned off after inactivity", + 0, + lightColorMap[LIGHT_ID] + ) } @Test fun testKeyboardBacklight_displayOnOff() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - MAX_BRIGHTNESS - ) + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + incrementKeyboardBacklight(DEVICE_ID) + + val currentValue = lightColorMap[LIGHT_ID] + assertNotEquals( + "Keyboard backlight level should be incremented to a non-zero value", + 0, + lightColorMap[LIGHT_ID] + ) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) - assertEquals( - "Keyboard backlight level should be restored to the level saved in the data " + - "store when display turned on", - Color.argb(MAX_BRIGHTNESS, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be turned off after display is turned off", + 0, + lightColorMap[LIGHT_ID] + ) - keyboardBacklightController.handleInteractiveStateChange(false /* isDisplayOn */) - assertEquals( - "Keyboard backlight level should be turned off after display is turned off", - 0, - lightColorMap[LIGHT_ID] - ) - } + keyboardBacklightController.handleInteractiveStateChange(true /* isDisplayOn */) + assertEquals( + "Keyboard backlight level should be turned on after display is turned on", + currentValue, + lightColorMap[LIGHT_ID] + ) } @Test fun testKeyboardBacklightSysfsNodeAdded_AfterInputDeviceAdded() { + setupController() var counter = sysfsNodeChanges keyboardBacklightController.onKeyboardBacklightUEvent(UEventObserver.UEvent( "ACTION=add\u0000SUBSYSTEM=leds\u0000DEVPATH=/xyz/leds/abc::no_backlight\u0000" @@ -475,249 +384,160 @@ class KeyboardBacklightControllerTests { @Test @UiThreadTest fun testKeyboardBacklightAnimation_onChangeLevels() { - KeyboardBacklightFlags( - animationEnabled = true, - customLevelsEnabled = false, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - incrementKeyboardBacklight(DEVICE_ID) - assertEquals( - "Should start animation from level 0", - DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[0], - lastAnimationValues[0] - ) - assertEquals( - "Should start animation to level 1", - DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], - lastAnimationValues[1] - ) + ExtendedMockito.doReturn("true").`when` { + SystemProperties.get(eq("persist.input.keyboard.backlight_animation.enabled")) } + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `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( + "Should start animation from level 0", + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[0], + lastAnimationValues[0] + ) + assertEquals( + "Should start animation to level 1", + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], + lastAnimationValues[1] + ) } @Test fun testKeyboardBacklightPreferredLevels() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = true, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val suggestedLevels = intArrayOf(0, 22, 63, 135, 196, 255) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, - suggestedLevels) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, - suggestedLevels) - } + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val suggestedLevels = intArrayOf(0, 22, 63, 135, 196, 255) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, + suggestedLevels) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, suggestedLevels) } @Test fun testKeyboardBacklightPreferredLevels_moreThanMax_shouldUseDefault() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = true, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val suggestedLevels = IntArray(MAX_BRIGHTNESS_CHANGE_STEPS + 1) { 10 * (it + 1) } - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, - suggestedLevels) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, - DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL) - } + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val suggestedLevels = IntArray(MAX_BRIGHTNESS_CHANGE_STEPS + 1) { 10 * (it + 1) } + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, + suggestedLevels) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, + DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL) } @Test fun testKeyboardBacklightPreferredLevels_mustHaveZeroAndMaxBrightnessAsBounds() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = true, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val suggestedLevels = intArrayOf(22, 63, 135, 196) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, - suggestedLevels) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - // Framework will add the lowest and maximum levels if not provided via config - assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, - intArrayOf(0, 22, 63, 135, 196, 255)) - } + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val suggestedLevels = intArrayOf(22, 63, 135, 196) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, + suggestedLevels) + `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 + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, + intArrayOf(0, 22, 63, 135, 196, 255)) } @Test fun testKeyboardBacklightPreferredLevels_dropsOutOfBoundsLevels() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = true, - ambientControlEnabled = false - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val suggestedLevels = intArrayOf(22, 63, 135, 400, 196, 1000) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, - suggestedLevels) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - - // Framework will drop out of bound levels in the config - assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, - intArrayOf(0, 22, 63, 135, 196, 255)) - } - } - - @Test - fun testAmbientBacklightControl_doesntRestoreBacklightLevel() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = true - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - - dataStore.setKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, - LIGHT_ID, - DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1] - ) - - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - keyboardBacklightController.notifyUserActivity() - testLooper.dispatchNext() - assertNull( - "Keyboard backlight level should not be restored to the saved level", - lightColorMap[LIGHT_ID] - ) - } - } - - @Test - fun testAmbientBacklightControl_doesntBackupBacklightLevel() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = true - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - incrementKeyboardBacklight(DEVICE_ID) - assertFalse( - "Light value should not be backed up if ambient control is enabled", - dataStore.getKeyboardBacklightBrightness( - keyboardWithBacklight.descriptor, LIGHT_ID - ).isPresent - ) - } + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val suggestedLevels = intArrayOf(22, 63, 135, 400, 196, 1000) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT, + suggestedLevels) + `when`(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 + assertIncrementDecrementForLevels(keyboardWithBacklight, keyboardBacklight, + intArrayOf(0, 22, 63, 135, 196, 255)) } @Test fun testAmbientBacklightControl_incrementLevel_afterAmbientChange() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = true - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - sendAmbientBacklightValue(1) - assertEquals( - "Light value should be changed to ambient provided value", - Color.argb(1, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + sendAmbientBacklightValue(1) + assertEquals( + "Light value should be changed to ambient provided value", + Color.argb(1, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) - incrementKeyboardBacklight(DEVICE_ID) + incrementKeyboardBacklight(DEVICE_ID) - assertEquals( - "Light value for level after increment post Ambient change is mismatched", - Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - } + assertEquals( + "Light value for level after increment post Ambient change is mismatched", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) } @Test fun testAmbientBacklightControl_decrementLevel_afterAmbientChange() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = true - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - sendAmbientBacklightValue(254) - assertEquals( - "Light value should be changed to ambient provided value", - Color.argb(254, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(inputManagerRule.mock.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(inputManagerRule.mock.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + sendAmbientBacklightValue(254) + assertEquals( + "Light value should be changed to ambient provided value", + Color.argb(254, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) - decrementKeyboardBacklight(DEVICE_ID) + decrementKeyboardBacklight(DEVICE_ID) - val numLevels = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size - assertEquals( - "Light value for level after decrement post Ambient change is mismatched", - Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[numLevels - 2], 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - } + val numLevels = DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL.size + assertEquals( + "Light value for level after decrement post Ambient change is mismatched", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[numLevels - 2], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) } @Test fun testAmbientBacklightControl_ambientChanges_afterManualChange() { - KeyboardBacklightFlags( - animationEnabled = false, - customLevelsEnabled = false, - ambientControlEnabled = true - ).use { - val keyboardWithBacklight = createKeyboard(DEVICE_ID) - val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) - `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) - `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) - keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) - incrementKeyboardBacklight(DEVICE_ID) - assertEquals( - "Light value should be changed to the first level", - Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), - lightColorMap[LIGHT_ID] - ) + setupController() + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `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( + "Light value should be changed to the first level", + Color.argb(DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[1], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) - sendAmbientBacklightValue(100) - assertNotEquals( - "Light value should not change based on ambient changes after manual changes", - Color.argb(100, 0, 0, 0), - lightColorMap[LIGHT_ID] - ) - } + sendAmbientBacklightValue(100) + assertNotEquals( + "Light value should not change based on ambient changes after manual changes", + Color.argb(100, 0, 0, 0), + lightColorMap[LIGHT_ID] + ) } private fun assertIncrementDecrementForLevels( @@ -734,11 +554,6 @@ class KeyboardBacklightControllerTests { Color.argb(expectedLevels[level], 0, 0, 0), lightColorMap[lightId] ) - assertEquals( - "Light value for level $level must be correctly stored in the datastore", - expectedLevels[level], - dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt - ) } // Increment above max level @@ -748,11 +563,6 @@ class KeyboardBacklightControllerTests { Color.argb(MAX_BRIGHTNESS, 0, 0, 0), lightColorMap[lightId] ) - assertEquals( - "Light value for max level must be correctly stored in the datastore", - MAX_BRIGHTNESS, - dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt - ) for (level in expectedLevels.size - 2 downTo 0) { decrementKeyboardBacklight(deviceId) @@ -761,11 +571,6 @@ class KeyboardBacklightControllerTests { Color.argb(expectedLevels[level], 0, 0, 0), lightColorMap[lightId] ) - assertEquals( - "Light value for level $level must be correctly stored in the datastore", - expectedLevels[level], - dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt - ) } // Decrement below min level @@ -775,11 +580,6 @@ class KeyboardBacklightControllerTests { Color.argb(0, 0, 0, 0), lightColorMap[lightId] ) - assertEquals( - "Light value for min level must be correctly stored in the datastore", - 0, - dataStore.getKeyboardBacklightBrightness(device.descriptor, lightId).asInt - ) } inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() { @@ -822,23 +622,6 @@ class KeyboardBacklightControllerTests { val isTriggeredByKeyPress: Boolean ) - private inner class KeyboardBacklightFlags constructor( - animationEnabled: Boolean, - customLevelsEnabled: Boolean, - ambientControlEnabled: Boolean - ) : AutoCloseable { - init { - InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(animationEnabled) - InputFeatureFlagProvider.setKeyboardBacklightCustomLevelsEnabled(customLevelsEnabled) - InputFeatureFlagProvider - .setAmbientKeyboardBacklightControlEnabled(ambientControlEnabled) - } - - override fun close() { - InputFeatureFlagProvider.clearOverrides() - } - } - private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory { override fun makeIntAnimator(from: Int, to: Int): ValueAnimator { lastAnimationValues[0] = from diff --git a/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt index c073c7aae678..5da0beb9cc8a 100644 --- a/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt @@ -23,9 +23,7 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.content.pm.ServiceInfo -import android.hardware.input.IInputManager import android.hardware.input.InputManager -import android.hardware.input.InputManagerGlobal import android.hardware.input.KeyGlyphMap.KeyCombination import android.os.Bundle import android.os.test.TestLooper @@ -36,8 +34,8 @@ 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.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -61,34 +59,31 @@ class KeyboardGlyphManagerTests { 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" } - @JvmField - @Rule(order = 0) + @get:Rule val setFlagsRule = SetFlagsRule() - - @JvmField - @Rule(order = 1) + @get:Rule val mockitoRule = MockitoJUnit.rule()!! + @get:Rule + val inputManagerRule = MockInputManagerRule() @Mock private lateinit var packageManager: PackageManager - @Mock - private lateinit var iInputManager: IInputManager - private lateinit var keyboardGlyphManager: KeyboardGlyphManager private lateinit var context: Context private lateinit var testLooper: TestLooper - private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession private lateinit var keyboardDevice: InputDevice @Before fun setup() { context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) - inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) testLooper = TestLooper() keyboardGlyphManager = KeyboardGlyphManager(context, testLooper.looper) @@ -98,21 +93,17 @@ class KeyboardGlyphManagerTests { testLooper.dispatchAll() } - @After - fun tearDown() { - if (this::inputManagerGlobalSession.isInitialized) { - inputManagerGlobalSession.close() - } - } - private fun setupInputDevices() { val inputManager = InputManager(context) Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) .thenReturn(inputManager) keyboardDevice = createKeyboard(DEVICE_ID, VENDOR_ID, PRODUCT_ID, 0, "", "") - Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) - Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) + 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() { @@ -158,6 +149,10 @@ class KeyboardGlyphManagerTests { "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) @@ -173,6 +168,7 @@ class KeyboardGlyphManagerTests { 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) diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt index 301c0e6a159f..4440a839caef 100644 --- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -24,11 +24,12 @@ 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.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE +import android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_USER import android.icu.util.ULocale import android.os.Bundle import android.os.test.TestLooper @@ -42,8 +43,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,10 +58,6 @@ 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 fun createKeyboard( deviceId: Int, @@ -120,8 +121,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 +149,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 +171,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 +184,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) } @@ -290,6 +283,41 @@ class KeyboardLayoutManagerTests { } @Test + fun testGetSetKeyboardLayoutOverrideForInputDevice() { + val imeSubtype = createImeSubtype() + + keyboardLayoutManager.setKeyboardLayoutOverrideForInputDevice( + keyboardDevice.identifier, + ENGLISH_UK_LAYOUT_DESCRIPTOR + ) + var result = + keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype + ) + assertEquals(LAYOUT_SELECTION_CRITERIA_DEVICE, result.selectionCriteria) + assertEquals( + "getKeyboardLayoutForInputDevice API should return the set layout", + ENGLISH_UK_LAYOUT_DESCRIPTOR, + result.layoutDescriptor + ) + + // This should replace the overriding layout set above + keyboardLayoutManager.setKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype, + ENGLISH_US_LAYOUT_DESCRIPTOR + ) + result = keyboardLayoutManager.getKeyboardLayoutForInputDevice( + keyboardDevice.identifier, USER_ID, imeInfo, imeSubtype + ) + assertEquals(LAYOUT_SELECTION_CRITERIA_USER, result.selectionCriteria) + assertEquals( + "getKeyboardLayoutForInputDevice API should return the user set layout", + ENGLISH_US_LAYOUT_DESCRIPTOR, + result.layoutDescriptor + ) + } + + @Test fun testGetKeyboardLayoutListForInputDevice() { // Check Layouts for "hi-Latn". It should return all 'Latn' keyboard layouts var keyboardLayouts = 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/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java index b5258dfc9c3c..60fa52f85e34 100644 --- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java +++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java @@ -402,4 +402,73 @@ public class TouchpadDebugViewTest { // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture). verify(mWindowManager, times(0)).updateViewLayout(any(), any()); } -}
\ No newline at end of file + + @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 cd6ab30d8678..73192eac89c0 100644 --- a/tests/Input/src/com/android/test/input/AnrTest.kt +++ b/tests/Input/src/com/android/test/input/AnrTest.kt @@ -27,8 +27,6 @@ import android.hardware.display.DisplayManager import android.os.Build import android.os.IInputConstants.UNMULTIPLIED_DEFAULT_DISPATCHING_TIMEOUT_MILLIS import android.os.SystemClock -import android.provider.Settings -import android.provider.Settings.Global.HIDE_ERROR_DIALOGS import android.server.wm.CtsWindowInfoUtils.waitForStableWindowGeometry import android.testing.PollingCheck @@ -38,6 +36,7 @@ import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until import com.android.cts.input.DebugInputRule +import com.android.cts.input.ShowErrorDialogsRule import com.android.cts.input.UinputTouchScreen import java.time.Duration @@ -79,18 +78,16 @@ class AnrTest { @get:Rule val debugInputRule = DebugInputRule() + @get:Rule + val showErrorDialogs = ShowErrorDialogsRule() + @Before fun setUp() { - val contentResolver = instrumentation.targetContext.contentResolver - hideErrorDialogs = Settings.Global.getInt(contentResolver, HIDE_ERROR_DIALOGS, 0) - Settings.Global.putInt(contentResolver, HIDE_ERROR_DIALOGS, 0) PACKAGE_NAME = UnresponsiveGestureMonitorActivity::class.java.getPackage()!!.getName() } @After fun tearDown() { - val contentResolver = instrumentation.targetContext.contentResolver - Settings.Global.putInt(contentResolver, HIDE_ERROR_DIALOGS, hideErrorDialogs) } @Test diff --git a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt b/tests/Input/src/com/android/test/input/CaptureEventActivity.kt deleted file mode 100644 index d54e3470d9c4..000000000000 --- a/tests/Input/src/com/android/test/input/CaptureEventActivity.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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/KeyCharacterMapTest.kt b/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt new file mode 100644 index 000000000000..860d9f680c4c --- /dev/null +++ b/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt @@ -0,0 +1,74 @@ +/* + * 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.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule + +import android.view.KeyCharacterMap +import android.view.KeyEvent + +import com.android.hardware.input.Flags + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Rule +import org.junit.Test + +/** + * Tests for {@link KeyCharacterMap}. + * + * <p>Build/Install/Run: + * atest KeyCharacterMapTest + * + */ +class KeyCharacterMapTest { + @get:Rule + val setFlagsRule = SetFlagsRule() + + @Test + @EnableFlags(Flags.FLAG_REMOVE_FALLBACK_MODIFIERS) + fun testGetFallback() { + // Based off of VIRTUAL kcm fallbacks. + val keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD) + + // One modifier fallback. + val oneModifierFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, + KeyEvent.META_CTRL_ON) + assertEquals(KeyEvent.KEYCODE_LANGUAGE_SWITCH, oneModifierFallback.keyCode) + assertEquals(0, oneModifierFallback.metaState) + + // Multiple modifier fallback. + val twoModifierFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_DEL, + KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON) + assertEquals(KeyEvent.KEYCODE_BACK, twoModifierFallback.keyCode) + assertEquals(0, twoModifierFallback.metaState) + + // No default button, fallback only. + val keyOnlyFallback = + keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_BUTTON_A, 0) + assertEquals(KeyEvent.KEYCODE_DPAD_CENTER, keyOnlyFallback.keyCode) + assertEquals(0, keyOnlyFallback.metaState) + + // A key event that is not an exact match for a fallback. Expect a null return. + // E.g. Ctrl + Space -> LanguageSwitch + // Ctrl + Alt + Space -> Ctrl + Alt + Space (No fallback). + val noMatchFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, + KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON) + assertNull(noMatchFallback) + } +} 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/UinputRecordingIntegrationTests.kt b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt index c61a25021949..1a0837b6d3d7 100644 --- a/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt +++ b/tests/Input/src/com/android/test/input/UinputRecordingIntegrationTests.kt @@ -15,18 +15,19 @@ */ 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.InputDevice 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.CaptureEventActivity +import com.android.cts.input.DebugInputRule +import com.android.cts.input.EvemuDevice import com.android.cts.input.InputJsonParser import com.android.cts.input.VirtualDisplayActivityScenario import com.android.cts.input.inputeventmatchers.isResampled @@ -66,9 +67,13 @@ class UinputRecordingIntegrationTests { 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 + "GooglePixelTabletTouchscreen", + R.raw.google_pixel_tablet_touchscreen, + R.raw.google_pixel_tablet_touchscreen_events, + Size(1600, 2560), + vendorId = 0x0603, + productId = 0x7806, + deviceSources = InputDevice.SOURCE_TOUCHSCREEN, ), ) @@ -88,6 +93,7 @@ class UinputRecordingIntegrationTests { val displaySize: Size, val vendorId: Int, val productId: Int, + val deviceSources: Int, ) { override fun toString(): String = name } @@ -96,6 +102,9 @@ class UinputRecordingIntegrationTests { private lateinit var parser: InputJsonParser @get:Rule + val debugInputRule = DebugInputRule() + + @get:Rule val testName = TestName() @Parameterized.Parameter(0) @@ -107,6 +116,7 @@ class UinputRecordingIntegrationTests { parser = InputJsonParser(instrumentation.context) } + @DebugInputRule.DebugInput(bug = 389901828) @Test fun testEvemuRecording() { VirtualDisplayActivityScenario.AutoClose<CaptureEventActivity>( @@ -115,37 +125,27 @@ class UinputRecordingIntegrationTests { ).use { scenario -> scenario.activity.window.decorView.requestUnbufferedDispatch(INPUT_DEVICE_SOURCE_ALL) - try { - instrumentation.uiAutomation.adoptShellPermissionIdentity( - ASSOCIATE_INPUT_DEVICE_TO_DISPLAY, - ) + EvemuDevice( + instrumentation, + testData.deviceSources, + testData.vendorId, + testData.productId, + testData.uinputRecordingResource, + scenario.virtualDisplay.display + ).use { evemuDevice -> - val inputPort = "uinput:1:${testData.vendorId}:${testData.productId}" - val inputManager = - instrumentation.context.getSystemService(InputManager::class.java)!! - try { - inputManager.addUniqueIdAssociationByPort( - inputPort, - scenario.virtualDisplay.display.uniqueId!!, - ) + evemuDevice.injectEvents() - 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) + if (DEBUG_RECEIVED_EVENTS) { + printReceivedEventsToLogcat(scenario.activity) + fail("Test cannot pass in debug mode!") } - } finally { - instrumentation.uiAutomation.dropShellPermissionIdentity() + + val verifier = EventVerifier( + BatchedEventSplitter { scenario.activity.getInputEvent() } + ) + verifyEvents(verifier) + scenario.activity.assertNoEvents() } } } @@ -162,35 +162,6 @@ class UinputRecordingIntegrationTests { } } - /** - * 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) { diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt index e85578663764..e1294b1f034e 100644 --- a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt +++ b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewAnsiScreenshotTest.kt @@ -19,12 +19,9 @@ package com.android.input.screenshot import android.content.Context import android.hardware.input.KeyboardLayout import android.os.LocaleList -import android.platform.test.flag.junit.SetFlagsRule -import com.android.hardware.input.Flags import java.util.Locale import org.junit.Rule import org.junit.Test -import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec @@ -38,18 +35,14 @@ class KeyboardLayoutPreviewAnsiScreenshotTest(emulationSpec: DeviceEmulationSpec fun getTestSpecs() = DeviceEmulationSpec.PhoneMinimal } - val setFlagsRule = SetFlagsRule() + @get:Rule val screenshotRule = InputScreenshotTestRule( emulationSpec, "frameworks/base/tests/InputScreenshotTest/assets" ) - @get:Rule - val ruleChain = RuleChain.outerRule(screenshotRule).around(setFlagsRule) - @Test fun test() { - setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) screenshotRule.screenshotTest("layout-preview-ansi") { context: Context -> LayoutPreview.createLayoutPreview( context, @@ -66,5 +59,4 @@ class KeyboardLayoutPreviewAnsiScreenshotTest(emulationSpec: DeviceEmulationSpec ) } } - }
\ No newline at end of file diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt index ab7bb4eda899..ddad6dea5e32 100644 --- a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt +++ b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewIsoScreenshotTest.kt @@ -17,14 +17,8 @@ package com.android.input.screenshot import android.content.Context -import android.hardware.input.KeyboardLayout -import android.os.LocaleList -import android.platform.test.flag.junit.SetFlagsRule -import com.android.hardware.input.Flags -import java.util.Locale import org.junit.Rule import org.junit.Test -import org.junit.rules.RuleChain import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @@ -39,21 +33,16 @@ class KeyboardLayoutPreviewIsoScreenshotTest(emulationSpec: DeviceEmulationSpec) fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal } - val setFlagsRule = SetFlagsRule() + @get:Rule val screenshotRule = InputScreenshotTestRule( emulationSpec, "frameworks/base/tests/InputScreenshotTest/assets" ) - @get:Rule - val ruleChain = RuleChain.outerRule(screenshotRule).around(setFlagsRule) - @Test fun test() { - setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) screenshotRule.screenshotTest("layout-preview") { context: Context -> LayoutPreview.createLayoutPreview(context, null) } } - } diff --git a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt index 5231c14bfc9a..8a8e4f058d8a 100644 --- a/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt +++ b/tests/InputScreenshotTest/src/android/input/screenshot/KeyboardLayoutPreviewJisScreenshotTest.kt @@ -19,12 +19,9 @@ package com.android.input.screenshot import android.content.Context import android.hardware.input.KeyboardLayout import android.os.LocaleList -import android.platform.test.flag.junit.SetFlagsRule -import com.android.hardware.input.Flags import java.util.Locale import org.junit.Rule import org.junit.Test -import org.junit.rules.RuleChain import org.junit.runner.RunWith import org.junit.runners.Parameterized import platform.test.screenshot.DeviceEmulationSpec @@ -38,18 +35,14 @@ class KeyboardLayoutPreviewJisScreenshotTest(emulationSpec: DeviceEmulationSpec) fun getTestSpecs() = DeviceEmulationSpec.PhoneMinimal } - val setFlagsRule = SetFlagsRule() + @get:Rule val screenshotRule = InputScreenshotTestRule( emulationSpec, "frameworks/base/tests/InputScreenshotTest/assets" ) - @get:Rule - val ruleChain = RuleChain.outerRule(screenshotRule).around(setFlagsRule) - @Test fun test() { - setFlagsRule.enableFlags(Flags.FLAG_KEYBOARD_LAYOUT_PREVIEW_FLAG) screenshotRule.screenshotTest("layout-preview-jis") { context: Context -> LayoutPreview.createLayoutPreview( context, @@ -66,5 +59,4 @@ class KeyboardLayoutPreviewJisScreenshotTest(emulationSpec: DeviceEmulationSpec) ) } } - }
\ No newline at end of file diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp index 3e58517579b8..35564066266b 100644 --- a/tests/Internal/Android.bp +++ b/tests/Internal/Android.bp @@ -32,6 +32,28 @@ 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", + "truth", + ], + 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: [ @@ -44,4 +66,11 @@ android_ravenwood_test { "src/com/android/internal/util/ParcellingTests.java", ], auto_gen_config: true, + team: "trendy_team_ravenwood", +} + +java_test_helper_library { + name: "ApplicationSharedMemoryTestRule", + srcs: ["src/com/android/internal/os/ApplicationSharedMemoryTestRule.java"], + static_libs: ["junit"], } 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/os/ApplicationSharedMemoryTest.java b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java new file mode 100644 index 000000000000..5ce0ede3e425 --- /dev/null +++ b/tests/Internal/src/com/android/internal/os/ApplicationSharedMemoryTest.java @@ -0,0 +1,178 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.FileDescriptor; +import java.io.IOException; + +/** 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"); + instance.writeSystemFeaturesCache(new int[] {1, 2, 3, 4, 5}); + fail("Attempted feature 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) { + } + } + + /** If system feature caching is enabled, it should be auto-written into app shared memory. */ + @Test + public void canReadSystemFeatures() throws IOException { + assumeTrue(android.content.pm.Flags.cacheSdkSystemFeatures()); + ApplicationSharedMemory instance = ApplicationSharedMemory.getInstance(); + assertThat(instance.readSystemFeaturesCache()).isNotEmpty(); + } + + @Test + public void systemFeaturesShareMemory() throws IOException { + ApplicationSharedMemory instance1 = ApplicationSharedMemory.create(); + + int[] featureVersions = new int[] {1, 2, 3, 4, 5}; + instance1.writeSystemFeaturesCache(featureVersions); + assertThat(featureVersions).isEqualTo(instance1.readSystemFeaturesCache()); + + ApplicationSharedMemory instance2 = + ApplicationSharedMemory.fromFileDescriptor( + instance1.getReadOnlyFileDescriptor(), /* mutable= */ false); + assertThat(featureVersions).isEqualTo(instance2.readSystemFeaturesCache()); + } + + @Test + public void systemFeaturesAreWriteOnce() throws IOException { + ApplicationSharedMemory instance1 = ApplicationSharedMemory.create(); + + try { + instance1.writeSystemFeaturesCache(new int[5000]); + fail("Cannot write an overly large system feature version buffer."); + } catch (IllegalArgumentException expected) { + } + + int[] featureVersions = new int[] {1, 2, 3, 4, 5}; + instance1.writeSystemFeaturesCache(featureVersions); + + int[] newFeatureVersions = new int[] {1, 2, 3, 4, 5, 6, 7}; + try { + instance1.writeSystemFeaturesCache(newFeatureVersions); + fail("Cannot update system features after first write."); + } catch (IllegalStateException expected) { + } + + ApplicationSharedMemory instance2 = + ApplicationSharedMemory.fromFileDescriptor( + instance1.getReadOnlyFileDescriptor(), /* mutable= */ false); + try { + instance2.writeSystemFeaturesCache(newFeatureVersions); + fail("Cannot update system features for read-only ashmem."); + } catch (IllegalStateException 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/NetworkSecurityConfigTest/Android.bp b/tests/NetworkSecurityConfigTest/Android.bp index 4c48eaa4622e..6a68f2bb7ff9 100644 --- a/tests/NetworkSecurityConfigTest/Android.bp +++ b/tests/NetworkSecurityConfigTest/Android.bp @@ -11,11 +11,12 @@ android_test { name: "NetworkSecurityConfigTests", certificate: "platform", libs: [ - "android.test.runner.stubs.system", "android.test.base.stubs.system", + "android.test.runner.stubs.system", ], static_libs: ["junit"], // Include all test java files. srcs: ["src/**/*.java"], platform_apis: true, + test_suites: ["general-tests"], } diff --git a/tests/NetworkSecurityConfigTest/TEST_MAPPING b/tests/NetworkSecurityConfigTest/TEST_MAPPING new file mode 100644 index 000000000000..d1b9aa1e3b53 --- /dev/null +++ b/tests/NetworkSecurityConfigTest/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "NetworkSecurityConfigTests" + } + ] +} diff --git a/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml b/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml index 99106ad37783..5d488410aef7 100644 --- a/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml +++ b/tests/NetworkSecurityConfigTest/res/xml/domain_whitespace.xml @@ -5,7 +5,7 @@ </domain> <domain> developer.android.com </domain> <pin-set> - <pin digest="SHA-256"> zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w= </pin> + <pin digest="SHA-256"> YPtHaftLw6/0vnc2BnNKGF54xiCA28WFcccjkA4ypCM= </pin> </pin-set> </domain-config> </network-security-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml index 232f88ff6cc9..731f0f041042 100644 --- a/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml +++ b/tests/NetworkSecurityConfigTest/res/xml/nested_domains.xml @@ -9,7 +9,7 @@ <domain-config> <domain>developer.android.com</domain> <pin-set> - <pin digest="SHA-256">zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w=</pin> + <pin digest="SHA-256">YPtHaftLw6/0vnc2BnNKGF54xiCA28WFcccjkA4ypCM=</pin> </pin-set> </domain-config> </domain-config> diff --git a/tests/NetworkSecurityConfigTest/res/xml/pins1.xml b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml index 7cc81b0101f1..2e49188ec4dc 100644 --- a/tests/NetworkSecurityConfigTest/res/xml/pins1.xml +++ b/tests/NetworkSecurityConfigTest/res/xml/pins1.xml @@ -3,7 +3,7 @@ <domain-config> <domain>android.com</domain> <pin-set> - <pin digest="SHA-256">zCTnfLwLKbS9S2sbp+uFz4KZOocFvXxkV06Ce9O5M2w=</pin> + <pin digest="SHA-256">YPtHaftLw6/0vnc2BnNKGF54xiCA28WFcccjkA4ypCM=</pin> </pin-set> </domain-config> </network-security-config> diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java index c6fe06858e3f..6207a6295ebf 100644 --- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java +++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/NetworkSecurityConfigTests.java @@ -40,9 +40,9 @@ public class NetworkSecurityConfigTests extends ActivityUnitTestCase<Activity> { super(Activity.class); } - // SHA-256 of the GTS intermediate CA (CN = GTS CA 1C3) for android.com (as of 09/2023). + // SHA-256 of the GTS intermediate CA (CN = WR2) for android.com (as of 01/2025). private static final byte[] GTS_INTERMEDIATE_SPKI_SHA256 = - hexToBytes("cc24e77cbc0b29b4bd4b6b1ba7eb85cf82993a8705bd7c64574e827bd3b9336c"); + hexToBytes("60fb4769fb4bc3aff4be773606734a185e78c62080dbc58571c723900e32a423"); private static final byte[] TEST_CA_BYTES = hexToBytes( diff --git a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java index 39b5cb4c4f0d..e140d1a0a94c 100644 --- a/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java +++ b/tests/NetworkSecurityConfigTest/src/android/security/net/config/TestUtils.java @@ -55,6 +55,7 @@ public final class TestUtils { throws Exception { URL url = new URL("https://" + host + ":" + port); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); connection.setSSLSocketFactory(context.getSocketFactory()); try { connection.getInputStream(); @@ -68,6 +69,7 @@ public final class TestUtils { throws Exception { URL url = new URL("https://" + host + ":" + port); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); connection.setSSLSocketFactory(context.getSocketFactory()); connection.getInputStream(); } diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp index 096555eb3056..16c6e3baf051 100644 --- a/tests/PackageWatchdog/Android.bp +++ b/tests/PackageWatchdog/Android.bp @@ -26,16 +26,23 @@ android_test { name: "PackageWatchdogTest", srcs: ["src/**/*.java"], static_libs: [ - "junit", - "mockito-target-extended-minus-junit4", + "PlatformProperties", + "androidx.test.rules", + "androidx.test.runner", "flag-junit", "frameworks-base-testutils", - "androidx.test.rules", - "PlatformProperties", + "junit", + "mockito-target-extended-minus-junit4", "services.core", "services.net", "truth", - ], + ] + select(soong_config_variable("ANDROID", "release_crashrecovery_module"), { + "true": [ + "service-crashrecovery-pre-jarjar", + "framework-crashrecovery.impl", + ], + default: [], + }), libs: ["android.test.runner.stubs.system"], jni_libs: [ // mockito-target-extended dependencies @@ -43,5 +50,9 @@ android_test { "libstaticjvmtiagent", ], platform_apis: true, - test_suites: ["device-tests"], + test_suites: [ + "device-tests", + "mts-crashrecovery", + ], + min_sdk_version: "36", } diff --git a/tests/PackageWatchdog/AndroidManifest.xml b/tests/PackageWatchdog/AndroidManifest.xml index 540edb41f66f..334d50fe6d10 100644 --- a/tests/PackageWatchdog/AndroidManifest.xml +++ b/tests/PackageWatchdog/AndroidManifest.xml @@ -15,7 +15,7 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.tests.packagewatchdog" > + package="com.android.server" > <application android:debuggable="true"> <uses-library android:name="android.test.runner" /> @@ -23,6 +23,6 @@ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" - android:targetPackage="com.android.tests.packagewatchdog" - android:label="PackageWatchdog Test"/> + android:targetPackage="com.android.server" + android:label="PackageWatchdog Test"/> </manifest> diff --git a/tests/PackageWatchdog/AndroidTest.xml b/tests/PackageWatchdog/AndroidTest.xml new file mode 100644 index 000000000000..45a88cdf5abe --- /dev/null +++ b/tests/PackageWatchdog/AndroidTest.xml @@ -0,0 +1,36 @@ +<?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="Runs PackageWatchdog Tests."> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="PackageWatchdogTest.apk" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-tag" value="PackageWatchdogTest" /> + + <!-- Only run this tests in MTS if the Crashrecovery Mainline module is installed. --> + <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.crashrecovery" /> + </object> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.server" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java index c0e90f9232d6..c2ab0550ea05 100644 --- a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java +++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java @@ -48,8 +48,6 @@ import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener; import android.os.Handler; import android.os.SystemProperties; import android.os.test.TestLooper; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.util.AtomicFile; @@ -83,6 +81,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -112,6 +111,7 @@ public class CrashRecoveryTest { private final TestClock mTestClock = new TestClock(); private TestLooper mTestLooper; + private Executor mTestExecutor; private Context mSpyContext; // Keep track of all created watchdogs to apply device config changes private List<PackageWatchdog> mAllocatedWatchdogs; @@ -133,13 +133,14 @@ public class CrashRecoveryTest { @Before public void setUp() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); MockitoAnnotations.initMocks(this); new File(InstrumentationRegistry.getContext().getFilesDir(), "package-watchdog.xml").delete(); adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG, - Manifest.permission.WRITE_DEVICE_CONFIG); + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG); mTestLooper = new TestLooper(); + mTestExecutor = mTestLooper.getNewExecutor(); mSpyContext = spy(InstrumentationRegistry.getContext()); when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager); when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> { @@ -224,39 +225,45 @@ public class CrashRecoveryTest { PackageWatchdog watchdog = createWatchdog(); RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(1); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(1); for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { watchdog.noteBoot(); } - verify(rescuePartyObserver).executeBootLoopMitigation(1); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver).onExecuteBootLoopMitigation(1); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(2); watchdog.noteBoot(); - verify(rescuePartyObserver).executeBootLoopMitigation(2); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver).onExecuteBootLoopMitigation(2); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(3); watchdog.noteBoot(); - verify(rescuePartyObserver).executeBootLoopMitigation(3); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(4); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver).onExecuteBootLoopMitigation(3); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(4); watchdog.noteBoot(); - verify(rescuePartyObserver).executeBootLoopMitigation(4); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(5); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver).onExecuteBootLoopMitigation(4); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(5); watchdog.noteBoot(); - verify(rescuePartyObserver).executeBootLoopMitigation(5); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(6); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver).onExecuteBootLoopMitigation(5); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(6); watchdog.noteBoot(); - verify(rescuePartyObserver).executeBootLoopMitigation(6); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(7); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver).onExecuteBootLoopMitigation(6); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(7); } @Test @@ -265,14 +272,15 @@ public class CrashRecoveryTest { RollbackPackageHealthObserver rollbackObserver = setUpRollbackPackageHealthObserver(watchdog); - verify(rollbackObserver, never()).executeBootLoopMitigation(1); + verify(rollbackObserver, never()).onExecuteBootLoopMitigation(1); for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { watchdog.noteBoot(); } - verify(rollbackObserver).executeBootLoopMitigation(1); - verify(rollbackObserver, never()).executeBootLoopMitigation(2); + mTestLooper.dispatchAll(); + verify(rollbackObserver).onExecuteBootLoopMitigation(1); + verify(rollbackObserver, never()).onExecuteBootLoopMitigation(2); // Update the list of available rollbacks after executing bootloop mitigation once when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH, @@ -280,130 +288,61 @@ public class CrashRecoveryTest { watchdog.noteBoot(); - verify(rollbackObserver).executeBootLoopMitigation(2); - verify(rollbackObserver, never()).executeBootLoopMitigation(3); - - // Update the list of available rollbacks after executing bootloop mitigation once - when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL)); - - watchdog.noteBoot(); - - verify(rollbackObserver, never()).executeBootLoopMitigation(3); - } - - @Test - @DisableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS) - public void testBootLoopWithRescuePartyAndRollbackObserver() throws Exception { - PackageWatchdog watchdog = createWatchdog(); - RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog); - RollbackPackageHealthObserver rollbackObserver = - setUpRollbackPackageHealthObserver(watchdog); - - verify(rescuePartyObserver, never()).executeBootLoopMitigation(1); - verify(rollbackObserver, never()).executeBootLoopMitigation(1); - for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { - watchdog.noteBoot(); - } - verify(rescuePartyObserver).executeBootLoopMitigation(1); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); - verify(rollbackObserver, never()).executeBootLoopMitigation(1); - - watchdog.noteBoot(); - - verify(rescuePartyObserver).executeBootLoopMitigation(2); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); - verify(rollbackObserver, never()).executeBootLoopMitigation(2); - - watchdog.noteBoot(); + mTestLooper.dispatchAll(); + verify(rollbackObserver).onExecuteBootLoopMitigation(2); + verify(rollbackObserver, never()).onExecuteBootLoopMitigation(3); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); - verify(rollbackObserver).executeBootLoopMitigation(1); - verify(rollbackObserver, never()).executeBootLoopMitigation(2); // Update the list of available rollbacks after executing bootloop mitigation once - when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH, - ROLLBACK_INFO_MANUAL)); - - watchdog.noteBoot(); - - verify(rescuePartyObserver).executeBootLoopMitigation(3); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(4); - verify(rollbackObserver, never()).executeBootLoopMitigation(2); - - watchdog.noteBoot(); - - verify(rescuePartyObserver).executeBootLoopMitigation(4); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(5); - verify(rollbackObserver, never()).executeBootLoopMitigation(2); - - watchdog.noteBoot(); - - verify(rescuePartyObserver).executeBootLoopMitigation(5); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(6); - verify(rollbackObserver, never()).executeBootLoopMitigation(2); - - watchdog.noteBoot(); - - verify(rescuePartyObserver, never()).executeBootLoopMitigation(6); - verify(rollbackObserver).executeBootLoopMitigation(2); - verify(rollbackObserver, never()).executeBootLoopMitigation(3); - // Update the list of available rollbacks after executing bootloop mitigation when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL)); watchdog.noteBoot(); - verify(rescuePartyObserver).executeBootLoopMitigation(6); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(7); - verify(rollbackObserver, never()).executeBootLoopMitigation(3); - - moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1); - Mockito.reset(rescuePartyObserver); - - for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { - watchdog.noteBoot(); - } - verify(rescuePartyObserver).executeBootLoopMitigation(1); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); + mTestLooper.dispatchAll(); + verify(rollbackObserver, never()).onExecuteBootLoopMitigation(3); } @Test - @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS) public void testBootLoopWithRescuePartyAndRollbackObserverNoFlags() throws Exception { PackageWatchdog watchdog = createWatchdog(); RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog); RollbackPackageHealthObserver rollbackObserver = setUpRollbackPackageHealthObserver(watchdog); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(1); - verify(rollbackObserver, never()).executeBootLoopMitigation(1); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(1); + verify(rollbackObserver, never()).onExecuteBootLoopMitigation(1); for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { watchdog.noteBoot(); } - verify(rescuePartyObserver).executeBootLoopMitigation(1); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); - verify(rollbackObserver, never()).executeBootLoopMitigation(1); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver).onExecuteBootLoopMitigation(1); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(2); + verify(rollbackObserver, never()).onExecuteBootLoopMitigation(1); watchdog.noteBoot(); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); - verify(rollbackObserver).executeBootLoopMitigation(1); - verify(rollbackObserver, never()).executeBootLoopMitigation(2); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(2); + verify(rollbackObserver).onExecuteBootLoopMitigation(1); + verify(rollbackObserver, never()).onExecuteBootLoopMitigation(2); // Update the list of available rollbacks after executing bootloop mitigation once when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL)); watchdog.noteBoot(); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); - verify(rollbackObserver).executeBootLoopMitigation(2); - verify(rollbackObserver, never()).executeBootLoopMitigation(3); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(2); + verify(rollbackObserver).onExecuteBootLoopMitigation(2); + verify(rollbackObserver, never()).onExecuteBootLoopMitigation(3); // Update the list of available rollbacks after executing bootloop mitigation when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL)); watchdog.noteBoot(); - verify(rescuePartyObserver).executeBootLoopMitigation(2); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); - verify(rollbackObserver, never()).executeBootLoopMitigation(3); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver).onExecuteBootLoopMitigation(2); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(3); + verify(rollbackObserver, never()).onExecuteBootLoopMitigation(3); moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1); Mockito.reset(rescuePartyObserver); @@ -411,85 +350,12 @@ public class CrashRecoveryTest { for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { watchdog.noteBoot(); } - verify(rescuePartyObserver).executeBootLoopMitigation(1); - verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); - } - - @Test - @DisableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS) - public void testCrashLoopWithRescuePartyAndRollbackObserver() throws Exception { - PackageWatchdog watchdog = createWatchdog(); - RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog); - RollbackPackageHealthObserver rollbackObserver = - setUpRollbackPackageHealthObserver(watchdog); - VersionedPackage versionedPackageA = new VersionedPackage(APP_A, VERSION_CODE); - - when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).then(inv -> { - ApplicationInfo info = new ApplicationInfo(); - info.flags |= ApplicationInfo.FLAG_PERSISTENT - | ApplicationInfo.FLAG_SYSTEM; - return info; - }); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: SCOPED_DEVICE_CONFIG_RESET - verify(rescuePartyObserver).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - verify(rescuePartyObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - verify(rollbackObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: ALL_DEVICE_CONFIG_RESET - verify(rescuePartyObserver).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - verify(rescuePartyObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 3); - verify(rollbackObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: WARM_REBOOT - verify(rescuePartyObserver).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 3); - verify(rescuePartyObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 4); - verify(rollbackObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: Low impact rollback - verify(rollbackObserver).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - verify(rescuePartyObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 4); - - // update available rollbacks to mock rollbacks being applied after the call to - // rollbackObserver.execute - when(mRollbackManager.getAvailableRollbacks()).thenReturn( - List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL)); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD reached. No more mitigations applied - verify(rescuePartyObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 4); - verify(rollbackObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); + mTestLooper.dispatchAll(); + verify(rescuePartyObserver).onExecuteBootLoopMitigation(1); + verify(rescuePartyObserver, never()).onExecuteBootLoopMitigation(2); } @Test - @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS) public void testCrashLoopWithRescuePartyAndRollbackObserverEnableDeprecateFlagReset() throws Exception { PackageWatchdog watchdog = createWatchdog(); @@ -510,24 +376,24 @@ public class CrashRecoveryTest { Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH); // Mitigation: WARM_REBOOT - verify(rescuePartyObserver).execute(versionedPackageA, + verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageA, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - verify(rescuePartyObserver, never()).execute(versionedPackageA, + verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA, PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - verify(rollbackObserver, never()).execute(versionedPackageA, + verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH); // Mitigation: Low impact rollback - verify(rollbackObserver).execute(versionedPackageA, + verify(rollbackObserver).onExecuteHealthCheckMitigation(versionedPackageA, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - verify(rescuePartyObserver, never()).execute(versionedPackageA, + verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA, PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); // update available rollbacks to mock rollbacks being applied after the call to - // rollbackObserver.execute + // rollbackObserver.onExecuteHealthCheckMitigation when(mRollbackManager.getAvailableRollbacks()).thenReturn( List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL)); @@ -535,130 +401,13 @@ public class CrashRecoveryTest { Arrays.asList(versionedPackageA), PackageWatchdog.FAILURE_REASON_APP_CRASH); // DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD reached. No more mitigations applied - verify(rescuePartyObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - verify(rollbackObserver, never()).execute(versionedPackageA, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - } - - @Test - @DisableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS) - public void testCrashLoopSystemUIWithRescuePartyAndRollbackObserver() throws Exception { - PackageWatchdog watchdog = createWatchdog(); - RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog); - RollbackPackageHealthObserver rollbackObserver = - setUpRollbackPackageHealthObserver(watchdog); - String systemUi = "com.android.systemui"; - VersionedPackage versionedPackageUi = new VersionedPackage( - systemUi, VERSION_CODE); - RollbackInfo rollbackInfoUi = getRollbackInfo(systemUi, VERSION_CODE, 1, - PackageManager.ROLLBACK_USER_IMPACT_LOW); - when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW, - ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL, rollbackInfoUi)); - - when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).then(inv -> { - ApplicationInfo info = new ApplicationInfo(); - info.flags |= ApplicationInfo.FLAG_PERSISTENT - | ApplicationInfo.FLAG_SYSTEM; - return info; - }); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: SCOPED_DEVICE_CONFIG_RESET - verify(rescuePartyObserver).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - verify(rollbackObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: ALL_DEVICE_CONFIG_RESET - verify(rescuePartyObserver).execute(versionedPackageUi, + verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA, PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 3); - verify(rollbackObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: WARM_REBOOT - verify(rescuePartyObserver).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 3); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 4); - verify(rollbackObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: Low impact rollback - verify(rollbackObserver).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 4); - verify(rollbackObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - - // update available rollbacks to mock rollbacks being applied after the call to - // rollbackObserver.execute - when(mRollbackManager.getAvailableRollbacks()).thenReturn( - List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL)); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: RESET_SETTINGS_UNTRUSTED_DEFAULTS - verify(rescuePartyObserver).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 4); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 5); - verify(rollbackObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: RESET_SETTINGS_UNTRUSTED_CHANGES - verify(rescuePartyObserver).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 5); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 6); - verify(rollbackObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: RESET_SETTINGS_TRUSTED_DEFAULTS - verify(rescuePartyObserver).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 6); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 7); - verify(rollbackObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); - - // Mitigation: Factory reset. High impact rollbacks are performed only for boot loops. - verify(rescuePartyObserver).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 7); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, - PackageWatchdog.FAILURE_REASON_APP_CRASH, 8); - verify(rollbackObserver, never()).execute(versionedPackageUi, + verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageA, PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); } @Test - @EnableFlags(Flags.FLAG_DEPRECATE_FLAGS_AND_SETTINGS_RESETS) public void testCrashLoopSystemUIWithRescuePartyAndRollbackObserverEnableDeprecateFlagReset() throws Exception { PackageWatchdog watchdog = createWatchdog(); @@ -685,26 +434,26 @@ public class CrashRecoveryTest { Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); // Mitigation: WARM_REBOOT - verify(rescuePartyObserver).execute(versionedPackageUi, + verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageUi, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, + verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi, PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - verify(rollbackObserver, never()).execute(versionedPackageUi, + verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); // Mitigation: Low impact rollback - verify(rollbackObserver).execute(versionedPackageUi, + verify(rollbackObserver).onExecuteHealthCheckMitigation(versionedPackageUi, PackageWatchdog.FAILURE_REASON_APP_CRASH, 1); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, + verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi, PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - verify(rollbackObserver, never()).execute(versionedPackageUi, + verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi, PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); // update available rollbacks to mock rollbacks being applied after the call to - // rollbackObserver.execute + // rollbackObserver.onExecuteHealthCheckMitigation when(mRollbackManager.getAvailableRollbacks()).thenReturn( List.of(ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL)); @@ -712,30 +461,40 @@ public class CrashRecoveryTest { Arrays.asList(versionedPackageUi), PackageWatchdog.FAILURE_REASON_APP_CRASH); // Mitigation: Factory reset. High impact rollbacks are performed only for boot loops. - verify(rescuePartyObserver).execute(versionedPackageUi, + verify(rescuePartyObserver).onExecuteHealthCheckMitigation(versionedPackageUi, PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); - verify(rescuePartyObserver, never()).execute(versionedPackageUi, + verify(rescuePartyObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi, PackageWatchdog.FAILURE_REASON_APP_CRASH, 3); - verify(rollbackObserver, never()).execute(versionedPackageUi, + verify(rollbackObserver, never()).onExecuteHealthCheckMitigation(versionedPackageUi, PackageWatchdog.FAILURE_REASON_APP_CRASH, 2); } RollbackPackageHealthObserver setUpRollbackPackageHealthObserver(PackageWatchdog watchdog) { RollbackPackageHealthObserver rollbackObserver = - spy(new RollbackPackageHealthObserver(mSpyContext, mApexManager)); + spy(new RollbackPackageHealthObserver(mSpyContext)); when(mSpyContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager); when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW, ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL)); when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager); - - watchdog.registerHealthObserver(rollbackObserver); + 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(mTestExecutor, rollbackObserver); return rollbackObserver; } RescuePartyObserver setUpRescuePartyObserver(PackageWatchdog watchdog) { setCrashRecoveryPropRescueBootCount(0); RescuePartyObserver rescuePartyObserver = spy(RescuePartyObserver.getInstance(mSpyContext)); assertFalse(RescueParty.isRebootPropertySet()); - watchdog.registerHealthObserver(rescuePartyObserver); + watchdog.registerHealthObserver(mTestExecutor, rescuePartyObserver); return rescuePartyObserver; } @@ -775,7 +534,7 @@ public class CrashRecoveryTest { Handler handler = new Handler(mTestLooper.getLooper()); PackageWatchdog watchdog = new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller, - mConnectivityModuleConnector, mTestClock); + mTestClock); mockCrashRecoveryProperties(watchdog); // Verify controller is not automatically started @@ -787,8 +546,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; @@ -1001,11 +762,9 @@ public class CrashRecoveryTest { triggerFailureCount = 1; } for (int i = 0; i < triggerFailureCount; i++) { - watchdog.onPackageFailure(packages, failureReason); + watchdog.notifyPackageFailure(packages, failureReason); } mTestLooper.dispatchAll(); - if (Flags.recoverabilityDetection()) { - moveTimeForwardAndDispatch(watchdog.DEFAULT_MITIGATION_WINDOW_MS); - } + moveTimeForwardAndDispatch(watchdog.DEFAULT_MITIGATION_WINDOW_MS); } } diff --git a/tests/PackageWatchdog/src/com/android/server/ExplicitHealthCheckServiceTest.java b/tests/PackageWatchdog/src/com/android/server/ExplicitHealthCheckServiceTest.java index 2fbfeba47b13..055e159ff0b6 100644 --- a/tests/PackageWatchdog/src/com/android/server/ExplicitHealthCheckServiceTest.java +++ b/tests/PackageWatchdog/src/com/android/server/ExplicitHealthCheckServiceTest.java @@ -16,6 +16,8 @@ package com.android.server; +import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; @@ -50,7 +52,7 @@ public class ExplicitHealthCheckServiceTest { IBinder binder = mExplicitHealthCheckService.onBind(new Intent()); CountDownLatch countDownLatch = new CountDownLatch(1); RemoteCallback callback = new RemoteCallback(result -> { - assertThat(result.get(ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE)) + assertThat(result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE)) .isEqualTo(PACKAGE_NAME); countDownLatch.countDown(); }); diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index 5b178250a4c9..b8274841f65b 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -19,6 +19,7 @@ package com.android.server; import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS; import static com.google.common.truth.Truth.assertThat; @@ -46,6 +47,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; @@ -85,6 +89,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Supplier; @@ -111,8 +116,12 @@ 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 Executor mTestExecutor; private Context mSpyContext; // Keep track of all created watchdogs to apply device config changes private List<PackageWatchdog> mAllocatedWatchdogs; @@ -141,13 +150,14 @@ public class PackageWatchdogTest { @Before public void setUp() throws Exception { - mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); MockitoAnnotations.initMocks(this); new File(InstrumentationRegistry.getContext().getFilesDir(), "package-watchdog.xml").delete(); adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG, - Manifest.permission.WRITE_DEVICE_CONFIG); + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG); mTestLooper = new TestLooper(); + mTestExecutor = mTestLooper.getNewExecutor(); mSpyContext = spy(InstrumentationRegistry.getContext()); when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager); when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> { @@ -219,7 +229,8 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); @@ -235,8 +246,10 @@ public class PackageWatchdogTest { TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer1); + watchdog.registerHealthObserver(mTestExecutor, observer2); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A, APP_B), SHORT_DURATION, observer2); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE), new VersionedPackage(APP_B, VERSION_CODE)), @@ -253,7 +266,8 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer); watchdog.unregisterHealthObserver(observer); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), @@ -269,8 +283,10 @@ public class PackageWatchdogTest { TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer1); + watchdog.registerHealthObserver(mTestExecutor, observer2); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer2); watchdog.unregisterHealthObserver(observer2); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), @@ -287,7 +303,8 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer); moveTimeForwardAndDispatch(SHORT_DURATION); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), @@ -303,8 +320,10 @@ public class PackageWatchdogTest { TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), LONG_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer1); + watchdog.registerHealthObserver(mTestExecutor, observer2); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), LONG_DURATION, observer2); moveTimeForwardAndDispatch(SHORT_DURATION); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), @@ -323,13 +342,14 @@ public class PackageWatchdogTest { TestObserver observer = new TestObserver(OBSERVER_NAME_1); // Start observing APP_A - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer); // Then advance time half-way moveTimeForwardAndDispatch(SHORT_DURATION / 2); // Start observing APP_A again - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer); // Then advance time such that it should have expired were it not for the second observation moveTimeForwardAndDispatch((SHORT_DURATION / 2) + 1); @@ -351,15 +371,17 @@ public class PackageWatchdogTest { TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); - watchdog1.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); - watchdog1.startObservingHealth(observer2, Arrays.asList(APP_A, APP_B), SHORT_DURATION); + watchdog1.registerHealthObserver(mTestExecutor, observer1); + watchdog1.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer1); + watchdog1.registerHealthObserver(mTestExecutor, observer2); + watchdog1.startExplicitHealthCheck(Arrays.asList(APP_A, APP_B), SHORT_DURATION, observer2); // Then advance time and run IO Handler so file is saved mTestLooper.dispatchAll(); // Then start a new watchdog PackageWatchdog watchdog2 = createWatchdog(); // Then resume observer1 and observer2 - watchdog2.registerHealthObserver(observer1); - watchdog2.registerHealthObserver(observer2); + watchdog2.registerHealthObserver(mTestExecutor, observer1); + watchdog2.registerHealthObserver(mTestExecutor, observer2); raiseFatalFailureAndDispatch(watchdog2, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE), new VersionedPackage(APP_B, VERSION_CODE)), @@ -367,6 +389,7 @@ public class PackageWatchdogTest { // We should receive failed packages as expected to ensure observers are persisted and // resumed correctly + mTestLooper.dispatchAll(); assertThat(observer1.mHealthCheckFailedPackages).containsExactly(APP_A); assertThat(observer2.mHealthCheckFailedPackages).containsExactly(APP_A, APP_B); } @@ -380,12 +403,14 @@ public class PackageWatchdogTest { TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); - watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer2); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer2); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer1); // Then fail APP_A below the threshold for (int i = 0; i < watchdog.getTriggerFailureCount() - 1; i++) { - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); } @@ -407,9 +432,10 @@ public class PackageWatchdogTest { TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); - - watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_B), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer2); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer2); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_B), SHORT_DURATION, observer1); // Then fail APP_C (not observed) above the threshold raiseFatalFailureAndDispatch(watchdog, @@ -441,7 +467,8 @@ public class PackageWatchdogTest { } }; - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer); // Then fail APP_A (different version) above the threshold raiseFatalFailureAndDispatch(watchdog, @@ -452,57 +479,6 @@ public class PackageWatchdogTest { assertThat(observer.mHealthCheckFailedPackages).isEmpty(); } - - /** - * Test package failure and notifies only least impact observers. - */ - @Test - public void testPackageFailureNotifyAllDifferentImpacts() throws Exception { - mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); - PackageWatchdog watchdog = createWatchdog(); - TestObserver observerNone = new TestObserver(OBSERVER_NAME_1, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_0); - TestObserver observerHigh = new TestObserver(OBSERVER_NAME_2, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); - TestObserver observerMid = new TestObserver(OBSERVER_NAME_3, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); - TestObserver observerLow = new TestObserver(OBSERVER_NAME_4, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); - - // Start observing for all impact observers - watchdog.startObservingHealth(observerNone, Arrays.asList(APP_A, APP_B, APP_C, APP_D), - SHORT_DURATION); - watchdog.startObservingHealth(observerHigh, Arrays.asList(APP_A, APP_B, APP_C), - SHORT_DURATION); - watchdog.startObservingHealth(observerMid, Arrays.asList(APP_A, APP_B), - SHORT_DURATION); - watchdog.startObservingHealth(observerLow, Arrays.asList(APP_A), - SHORT_DURATION); - - // Then fail all apps above the threshold - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE), - new VersionedPackage(APP_B, VERSION_CODE), - new VersionedPackage(APP_C, VERSION_CODE), - new VersionedPackage(APP_D, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - - // Verify least impact observers are notifed of package failures - List<String> observerNonePackages = observerNone.mMitigatedPackages; - List<String> observerHighPackages = observerHigh.mMitigatedPackages; - List<String> observerMidPackages = observerMid.mMitigatedPackages; - List<String> observerLowPackages = observerLow.mMitigatedPackages; - - // APP_D failure observed by only observerNone is not caught cos its impact is none - assertThat(observerNonePackages).isEmpty(); - // APP_C failure is caught by observerHigh cos it's the lowest impact observer - assertThat(observerHighPackages).containsExactly(APP_C); - // APP_B failure is caught by observerMid cos it's the lowest impact observer - assertThat(observerMidPackages).containsExactly(APP_B); - // APP_A failure is caught by observerLow cos it's the lowest impact observer - assertThat(observerLowPackages).containsExactly(APP_A); - } - @Test public void testPackageFailureNotifyAllDifferentImpactsRecoverability() throws Exception { PackageWatchdog watchdog = createWatchdog(); @@ -516,14 +492,17 @@ public class PackageWatchdogTest { PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); // Start observing for all impact observers - watchdog.startObservingHealth(observerNone, Arrays.asList(APP_A, APP_B, APP_C, APP_D), - SHORT_DURATION); - watchdog.startObservingHealth(observerHigh, Arrays.asList(APP_A, APP_B, APP_C), - SHORT_DURATION); - watchdog.startObservingHealth(observerMid, Arrays.asList(APP_A, APP_B), - SHORT_DURATION); - watchdog.startObservingHealth(observerLow, Arrays.asList(APP_A), - SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observerNone); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A, APP_B, APP_C, APP_D), + SHORT_DURATION, observerNone); + watchdog.registerHealthObserver(mTestExecutor, observerHigh); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A, APP_B, APP_C), SHORT_DURATION, + observerHigh); + watchdog.registerHealthObserver(mTestExecutor, observerMid); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A, APP_B), SHORT_DURATION, + observerMid); + watchdog.registerHealthObserver(mTestExecutor, observerLow); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observerLow); // Then fail all apps above the threshold raiseFatalFailureAndDispatch(watchdog, @@ -549,82 +528,6 @@ public class PackageWatchdogTest { assertThat(observerLowPackages).containsExactly(APP_A); } - /** - * Test package failure and least impact observers are notified successively. - * State transistions: - * - * <ul> - * <li>(observer1:low, observer2:mid) -> {observer1} - * <li>(observer1:high, observer2:mid) -> {observer2} - * <li>(observer1:high, observer2:none) -> {observer1} - * <li>(observer1:none, observer2:none) -> {} - * <ul> - */ - @Test - public void testPackageFailureNotifyLeastImpactSuccessively() throws Exception { - mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); - PackageWatchdog watchdog = createWatchdog(); - TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); - TestObserver observerSecond = new TestObserver(OBSERVER_NAME_2, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); - - // Start observing for observerFirst and observerSecond with failure handling - watchdog.startObservingHealth(observerFirst, Arrays.asList(APP_A), LONG_DURATION); - watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION); - - // Then fail APP_A above the threshold - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - - // Verify only observerFirst is notifed - assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A); - assertThat(observerSecond.mMitigatedPackages).isEmpty(); - - // After observerFirst handles failure, next action it has is high impact - observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; - observerFirst.mMitigatedPackages.clear(); - observerSecond.mMitigatedPackages.clear(); - - // Then fail APP_A again above the threshold - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - - // Verify only observerSecond is notifed cos it has least impact - assertThat(observerSecond.mMitigatedPackages).containsExactly(APP_A); - assertThat(observerFirst.mMitigatedPackages).isEmpty(); - - // After observerSecond handles failure, it has no further actions - observerSecond.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - observerFirst.mMitigatedPackages.clear(); - observerSecond.mMitigatedPackages.clear(); - - // Then fail APP_A again above the threshold - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - - // Verify only observerFirst is notifed cos it has the only action - assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A); - assertThat(observerSecond.mMitigatedPackages).isEmpty(); - - // After observerFirst handles failure, it too has no further actions - observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; - observerFirst.mMitigatedPackages.clear(); - observerSecond.mMitigatedPackages.clear(); - - // Then fail APP_A again above the threshold - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - - // Verify no observer is notified cos no actions left - assertThat(observerFirst.mMitigatedPackages).isEmpty(); - assertThat(observerSecond.mMitigatedPackages).isEmpty(); - } - @Test public void testPackageFailureNotifyLeastImpactSuccessivelyRecoverability() throws Exception { PackageWatchdog watchdog = createWatchdog(); @@ -634,8 +537,10 @@ public class PackageWatchdogTest { PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); // Start observing for observerFirst and observerSecond with failure handling - watchdog.startObservingHealth(observerFirst, Arrays.asList(APP_A), LONG_DURATION); - watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observerFirst); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), LONG_DURATION, observerFirst); + watchdog.registerHealthObserver(mTestExecutor, observerSecond); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), LONG_DURATION, observerSecond); // Then fail APP_A above the threshold raiseFatalFailureAndDispatch(watchdog, @@ -689,32 +594,6 @@ public class PackageWatchdogTest { assertThat(observerSecond.mMitigatedPackages).isEmpty(); } - /** - * Test package failure and notifies only one observer even with observer impact tie. - */ - @Test - public void testPackageFailureNotifyOneSameImpact() throws Exception { - mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); - PackageWatchdog watchdog = createWatchdog(); - TestObserver observer1 = new TestObserver(OBSERVER_NAME_1, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); - TestObserver observer2 = new TestObserver(OBSERVER_NAME_2, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); - - // Start observing for observer1 and observer2 with failure handling - watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); - - // Then fail APP_A above the threshold - raiseFatalFailureAndDispatch(watchdog, - Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), - PackageWatchdog.FAILURE_REASON_UNKNOWN); - - // Verify only one observer is notifed - assertThat(observer1.mMitigatedPackages).containsExactly(APP_A); - assertThat(observer2.mMitigatedPackages).isEmpty(); - } - @Test public void testPackageFailureNotifyOneSameImpactRecoverabilityDetection() throws Exception { PackageWatchdog watchdog = createWatchdog(); @@ -724,8 +603,10 @@ public class PackageWatchdogTest { PackageHealthObserverImpact.USER_IMPACT_LEVEL_50); // Start observing for observer1 and observer2 with failure handling - watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer2); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer2); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer1); // Then fail APP_A above the threshold raiseFatalFailureAndDispatch(watchdog, @@ -755,8 +636,10 @@ public class PackageWatchdogTest { // Start observing with explicit health checks for APP_A and APP_B respectively // with observer1 and observer2 controller.setSupportedPackages(Arrays.asList(APP_A, APP_B)); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer2, Arrays.asList(APP_B), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer1); + watchdog.registerHealthObserver(mTestExecutor, observer2); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_B), SHORT_DURATION, observer2); // Run handler so requests are dispatched to the controller mTestLooper.dispatchAll(); @@ -772,7 +655,8 @@ public class PackageWatchdogTest { // Observer3 didn't exist when we got the explicit health check above, so // it starts out with a non-passing explicit health check and has to wait for a pass // otherwise it would be notified of APP_A failure on expiry - watchdog.startObservingHealth(observer3, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer3); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer3); // Then expire observers moveTimeForwardAndDispatch(SHORT_DURATION); @@ -802,8 +686,9 @@ public class PackageWatchdogTest { // Start observing with explicit health checks for APP_A and APP_B controller.setSupportedPackages(Arrays.asList(APP_A, APP_B, APP_C)); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer, Arrays.asList(APP_B), LONG_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_B), LONG_DURATION, observer); // Run handler so requests are dispatched to the controller mTestLooper.dispatchAll(); @@ -839,7 +724,7 @@ public class PackageWatchdogTest { // Then set new supported packages controller.setSupportedPackages(Arrays.asList(APP_C)); // Start observing APP_A and APP_C; only APP_C has support for explicit health checks - watchdog.startObservingHealth(observer, Arrays.asList(APP_A, APP_C), SHORT_DURATION); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A, APP_C), SHORT_DURATION, observer); // Run handler so requests/cancellations are dispatched to the controller mTestLooper.dispatchAll(); @@ -870,7 +755,8 @@ public class PackageWatchdogTest { // package observation duration == LONG_DURATION // health check duration == SHORT_DURATION (set by default in the TestController) controller.setSupportedPackages(Arrays.asList(APP_A)); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), LONG_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), LONG_DURATION, observer); // Then APP_A has exceeded health check duration moveTimeForwardAndDispatch(SHORT_DURATION); @@ -901,7 +787,8 @@ public class PackageWatchdogTest { // package observation duration == SHORT_DURATION / 2 // health check duration == SHORT_DURATION (set by default in the TestController) controller.setSupportedPackages(Arrays.asList(APP_A)); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION / 2); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION / 2, observer); // Forward time to expire the observation duration moveTimeForwardAndDispatch(SHORT_DURATION / 2); @@ -966,33 +853,14 @@ public class PackageWatchdogTest { } @Test - public void testNetworkStackFailure() { - mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); - final PackageWatchdog wd = createWatchdog(); - - // Start observing with failure handling - TestObserver observer = new TestObserver(OBSERVER_NAME_1, - PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); - wd.startObservingHealth(observer, Collections.singletonList(APP_A), SHORT_DURATION); - - // Notify of NetworkStack failure - mConnectivityModuleCallbackCaptor.getValue().onNetworkStackFailure(APP_A); - - // Run handler so package failures are dispatched to observers - mTestLooper.dispatchAll(); - - // Verify the NetworkStack observer is notified - assertThat(observer.mMitigatedPackages).containsExactly(APP_A); - } - - @Test + @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_CRASHRECOVERY) public void testNetworkStackFailureRecoverabilityDetection() { final PackageWatchdog wd = createWatchdog(); // Start observing with failure handling TestObserver observer = new TestObserver(OBSERVER_NAME_1, PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); - wd.startObservingHealth(observer, Collections.singletonList(APP_A), SHORT_DURATION); + wd.startExplicitHealthCheck(Collections.singletonList(APP_A), SHORT_DURATION, observer); // Notify of NetworkStack failure mConnectivityModuleCallbackCaptor.getValue().onNetworkStackFailure(APP_A); @@ -1013,17 +881,18 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer); // Fail APP_A below the threshold which should not trigger package failures for (int i = 0; i < PackageWatchdog.DEFAULT_TRIGGER_FAILURE_COUNT - 1; i++) { - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); } mTestLooper.dispatchAll(); assertThat(observer.mHealthCheckFailedPackages).isEmpty(); // One more to trigger the package failure - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); mTestLooper.dispatchAll(); assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_A); @@ -1041,11 +910,12 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A, APP_B), Long.MAX_VALUE); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A, APP_B), Long.MAX_VALUE, observer); + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_DURATION_MS + 1); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); mTestLooper.dispatchAll(); @@ -1053,10 +923,10 @@ public class PackageWatchdogTest { // DEFAULT_TRIGGER_FAILURE_DURATION_MS. assertThat(observer.mHealthCheckFailedPackages).isEmpty(); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_DURATION_MS - 1); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); mTestLooper.dispatchAll(); @@ -1066,15 +936,16 @@ public class PackageWatchdogTest { } /** - * Test default monitoring duration is used when PackageWatchdog#startObservingHealth is offered - * an invalid durationMs. + * Test default monitoring duration is used when PackageWatchdog#startExplicitHealthCheck is + * offered an invalid durationMs. */ @Test public void testInvalidMonitoringDuration_beforeExpiry() { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), -1); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), -1, observer); // Note: Don't move too close to the expiration time otherwise the handler will be thrashed // by PackageWatchdog#scheduleNextSyncStateLocked which keeps posting runnables with very // small timeouts. @@ -1088,15 +959,16 @@ public class PackageWatchdogTest { } /** - * Test default monitoring duration is used when PackageWatchdog#startObservingHealth is offered - * an invalid durationMs. + * Test default monitoring duration is used when PackageWatchdog#startExplicitHealthCheck is + * offered an invalid durationMs. */ @Test public void testInvalidMonitoringDuration_afterExpiry() { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), -1); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), -1, observer); moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_OBSERVING_DURATION_MS + 1); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), @@ -1118,19 +990,20 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, Arrays.asList(APP_A), Long.MAX_VALUE); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), Long.MAX_VALUE, observer); // Raise 2 failures at t=0 and t=900 respectively - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); moveTimeForwardAndDispatch(900); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); // Raise 2 failures at t=1100 moveTimeForwardAndDispatch(200); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); - watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.notifyPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); mTestLooper.dispatchAll(); @@ -1145,8 +1018,10 @@ public class PackageWatchdogTest { TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); TestObserver observer2 = new TestObserver(OBSERVER_NAME_2); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); - watchdog.startObservingHealth(observer2, Arrays.asList(APP_B), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer1); + watchdog.registerHealthObserver(mTestExecutor, observer2); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_B), SHORT_DURATION, observer2); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_APP_CRASH); @@ -1165,7 +1040,8 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(); TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_A), SHORT_DURATION, observer1); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_NATIVE_CRASH); @@ -1185,7 +1061,8 @@ public class PackageWatchdogTest { persistentObserver.setPersistent(true); persistentObserver.setMayObservePackages(true); - watchdog.startObservingHealth(persistentObserver, Arrays.asList(APP_B), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, persistentObserver); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_B), SHORT_DURATION, persistentObserver); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); @@ -1203,35 +1080,23 @@ public class PackageWatchdogTest { persistentObserver.setPersistent(true); persistentObserver.setMayObservePackages(false); - watchdog.startObservingHealth(persistentObserver, Arrays.asList(APP_B), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, persistentObserver); + watchdog.startExplicitHealthCheck(Arrays.asList(APP_B), SHORT_DURATION, persistentObserver); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); assertThat(persistentObserver.mHealthCheckFailedPackages).isEmpty(); } - - /** Ensure that boot loop mitigation is done when the number of boots meets the threshold. */ - @Test - public void testBootLoopDetection_meetsThreshold() { - mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); - PackageWatchdog watchdog = createWatchdog(); - TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); - watchdog.registerHealthObserver(bootObserver); - for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { - watchdog.noteBoot(); - } - assertThat(bootObserver.mitigatedBootLoop()).isTrue(); - } - @Test public void testBootLoopDetection_meetsThresholdRecoverability() { PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); - watchdog.registerHealthObserver(bootObserver); + watchdog.registerHealthObserver(mTestExecutor, bootObserver); for (int i = 0; i < 15; i++) { watchdog.noteBoot(); } + mTestLooper.dispatchAll(); assertThat(bootObserver.mitigatedBootLoop()).isTrue(); } @@ -1243,10 +1108,11 @@ public class PackageWatchdogTest { public void testBootLoopDetection_doesNotMeetThreshold() { PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); - watchdog.registerHealthObserver(bootObserver); + watchdog.registerHealthObserver(mTestExecutor, bootObserver); for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; i++) { watchdog.noteBoot(); } + mTestLooper.dispatchAll(); assertThat(bootObserver.mitigatedBootLoop()).isFalse(); } @@ -1259,33 +1125,14 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); - watchdog.registerHealthObserver(bootObserver); + watchdog.registerHealthObserver(mTestExecutor, bootObserver); for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; i++) { watchdog.noteBoot(); } + mTestLooper.dispatchAll(); assertThat(bootObserver.mitigatedBootLoop()).isFalse(); } - /** - * Ensure that boot loop mitigation is done for the observer with the lowest user impact - */ - @Test - public void testBootLoopMitigationDoneForLowestUserImpact() { - mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); - PackageWatchdog watchdog = createWatchdog(); - TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1); - bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); - TestObserver bootObserver2 = new TestObserver(OBSERVER_NAME_2); - bootObserver2.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); - watchdog.registerHealthObserver(bootObserver1); - watchdog.registerHealthObserver(bootObserver2); - for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { - watchdog.noteBoot(); - } - assertThat(bootObserver1.mitigatedBootLoop()).isTrue(); - assertThat(bootObserver2.mitigatedBootLoop()).isFalse(); - } - @Test public void testBootLoopMitigationDoneForLowestUserImpactRecoverability() { PackageWatchdog watchdog = createWatchdog(); @@ -1293,47 +1140,22 @@ public class PackageWatchdogTest { bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); TestObserver bootObserver2 = new TestObserver(OBSERVER_NAME_2); bootObserver2.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); - watchdog.registerHealthObserver(bootObserver1); - watchdog.registerHealthObserver(bootObserver2); + watchdog.registerHealthObserver(mTestExecutor, bootObserver1); + watchdog.registerHealthObserver(mTestExecutor, bootObserver2); for (int i = 0; i < 15; i++) { watchdog.noteBoot(); } + mTestLooper.dispatchAll(); assertThat(bootObserver1.mitigatedBootLoop()).isTrue(); assertThat(bootObserver2.mitigatedBootLoop()).isFalse(); } - /** - * Ensure that the correct mitigation counts are sent to the boot loop observer. - */ - @Test - public void testMultipleBootLoopMitigation() { - mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); - PackageWatchdog watchdog = createWatchdog(); - TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); - watchdog.registerHealthObserver(bootObserver); - for (int i = 0; i < 4; i++) { - for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; j++) { - watchdog.noteBoot(); - } - } - - moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1); - - for (int i = 0; i < 4; i++) { - for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; j++) { - watchdog.noteBoot(); - } - } - - assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4)); - } - @Test public void testMultipleBootLoopMitigationRecoverabilityLowImpact() { PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); - watchdog.registerHealthObserver(bootObserver); + watchdog.registerHealthObserver(mTestExecutor, bootObserver); for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; j++) { watchdog.noteBoot(); } @@ -1349,7 +1171,7 @@ public class PackageWatchdogTest { for (int i = 0; i < 4; i++) { watchdog.noteBoot(); } - + mTestLooper.dispatchAll(); assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4)); } @@ -1361,7 +1183,8 @@ public class PackageWatchdogTest { public void testNullFailedPackagesList() { PackageWatchdog watchdog = createWatchdog(); TestObserver observer1 = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer1, List.of(APP_A), LONG_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer1); + watchdog.startExplicitHealthCheck(List.of(APP_A), LONG_DURATION, observer1); raiseFatalFailureAndDispatch(watchdog, null, PackageWatchdog.FAILURE_REASON_APP_CRASH); assertThat(observer1.mMitigatedPackages).isEmpty(); @@ -1379,18 +1202,18 @@ public class PackageWatchdogTest { PackageWatchdog watchdog = createWatchdog(testController, true); TestObserver testObserver1 = new TestObserver(OBSERVER_NAME_1); - watchdog.registerHealthObserver(testObserver1); - watchdog.startObservingHealth(testObserver1, List.of(APP_A), LONG_DURATION); + watchdog.registerHealthObserver(mTestExecutor, testObserver1); + watchdog.startExplicitHealthCheck(List.of(APP_A), LONG_DURATION, testObserver1); mTestLooper.dispatchAll(); TestObserver testObserver2 = new TestObserver(OBSERVER_NAME_2); - watchdog.registerHealthObserver(testObserver2); - watchdog.startObservingHealth(testObserver2, List.of(APP_B), LONG_DURATION); + watchdog.registerHealthObserver(mTestExecutor, testObserver2); + watchdog.startExplicitHealthCheck(List.of(APP_B), LONG_DURATION, testObserver2); mTestLooper.dispatchAll(); TestObserver testObserver3 = new TestObserver(OBSERVER_NAME_3); - watchdog.registerHealthObserver(testObserver3); - watchdog.startObservingHealth(testObserver3, List.of(APP_C), LONG_DURATION); + watchdog.registerHealthObserver(mTestExecutor, testObserver3); + watchdog.startExplicitHealthCheck(List.of(APP_C), LONG_DURATION, testObserver3); mTestLooper.dispatchAll(); watchdog.unregisterHealthObserver(testObserver1); @@ -1422,15 +1245,16 @@ public class PackageWatchdogTest { public void testFailureHistoryIsPreserved() { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, List.of(APP_A), SHORT_DURATION); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(List.of(APP_A), SHORT_DURATION, observer); for (int i = 0; i < PackageWatchdog.DEFAULT_TRIGGER_FAILURE_COUNT - 1; i++) { - watchdog.onPackageFailure(List.of(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.notifyPackageFailure(List.of(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); } mTestLooper.dispatchAll(); assertThat(observer.mMitigatedPackages).isEmpty(); - watchdog.startObservingHealth(observer, List.of(APP_A), LONG_DURATION); - watchdog.onPackageFailure(List.of(new VersionedPackage(APP_A, VERSION_CODE)), + watchdog.startExplicitHealthCheck(List.of(APP_A), LONG_DURATION, observer); + watchdog.notifyPackageFailure(List.of(new VersionedPackage(APP_A, VERSION_CODE)), PackageWatchdog.FAILURE_REASON_UNKNOWN); mTestLooper.dispatchAll(); assertThat(observer.mMitigatedPackages).isEqualTo(List.of(APP_A)); @@ -1444,8 +1268,9 @@ public class PackageWatchdogTest { public void testMitigationSlidingWindow() { PackageWatchdog watchdog = createWatchdog(); TestObserver observer = new TestObserver(OBSERVER_NAME_1); - watchdog.startObservingHealth(observer, List.of(APP_A), - PackageWatchdog.DEFAULT_OBSERVING_DURATION_MS * 2); + watchdog.registerHealthObserver(mTestExecutor, observer); + watchdog.startExplicitHealthCheck(List.of(APP_A), + PackageWatchdog.DEFAULT_OBSERVING_DURATION_MS * 2, observer); raiseFatalFailureAndDispatch(watchdog, Arrays.asList(new VersionedPackage(APP_A, @@ -1728,12 +1553,10 @@ public class PackageWatchdogTest { triggerFailureCount = 1; } for (int i = 0; i < triggerFailureCount; i++) { - watchdog.onPackageFailure(packages, failureReason); + watchdog.notifyPackageFailure(packages, failureReason); } mTestLooper.dispatchAll(); - if (Flags.recoverabilityDetection()) { - moveTimeForwardAndDispatch(watchdog.DEFAULT_MITIGATION_WINDOW_MS); - } + moveTimeForwardAndDispatch(watchdog.DEFAULT_MITIGATION_WINDOW_MS); } private PackageWatchdog createWatchdog() { @@ -1746,7 +1569,7 @@ public class PackageWatchdogTest { Handler handler = new Handler(mTestLooper.getLooper()); PackageWatchdog watchdog = new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller, - mConnectivityModuleConnector, mTestClock); + mTestClock); mockCrashRecoveryProperties(watchdog); // Verify controller is not automatically started @@ -1758,8 +1581,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; @@ -1859,12 +1684,12 @@ public class PackageWatchdogTest { return mImpact; } - public boolean execute(VersionedPackage versionedPackage, int failureReason, - int mitigationCount) { + public int onExecuteHealthCheckMitigation(VersionedPackage versionedPackage, + int failureReason, int mitigationCount) { mMitigatedPackages.add(versionedPackage.getPackageName()); mMitigationCounts.add(mitigationCount); mLastFailureReason = failureReason; - return true; + return MITIGATION_RESULT_SUCCESS; } public String getUniqueIdentifier() { @@ -1883,10 +1708,10 @@ public class PackageWatchdogTest { return mImpact; } - public boolean executeBootLoopMitigation(int level) { + public int onExecuteBootLoopMitigation(int level) { mMitigatedBootLoop = true; mBootMitigationCounts.add(level); - return true; + return MITIGATION_RESULT_SUCCESS; } public boolean mitigatedBootLoop() { diff --git a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java index 060133df0a40..e7e3d10c958b 100644 --- a/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java +++ b/tests/PlatformCompatGating/src/com/android/tests/gating/PlatformCompatPermissionsTest.java @@ -81,7 +81,8 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.reportChange(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -90,7 +91,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChange(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.reportChange(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -99,7 +101,7 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChangeByPackageName(1, packageName, 0); + mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid()); } @Test @@ -108,7 +110,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.reportChangeByPackageName(1, packageName, 0); + mPlatformCompat.reportChangeByPackageName(1, packageName, Process.myUid()); } @Test @@ -133,7 +135,8 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -143,7 +146,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -152,7 +156,8 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabled(1, mPackageManager.getApplicationInfo(packageName, 0)); + mPlatformCompat.isChangeEnabled(1, + mPackageManager.getApplicationInfo(packageName, Process.myUid())); } @Test @@ -161,7 +166,7 @@ public final class PlatformCompatPermissionsTest { thrown.expect(SecurityException.class); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test @@ -171,7 +176,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test @@ -180,7 +185,7 @@ public final class PlatformCompatPermissionsTest { mUiAutomation.adoptShellPermissionIdentity(READ_COMPAT_CHANGE_CONFIG, LOG_COMPAT_CHANGE); final String packageName = mContext.getPackageName(); - mPlatformCompat.isChangeEnabledByPackageName(1, packageName, 0); + mPlatformCompat.isChangeEnabledByPackageName(1, packageName, Process.myUid()); } @Test diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java index 314e95229d29..a6aa877c9097 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java @@ -82,7 +82,8 @@ public class NetworkStagedRollbackTest { Manifest.permission.DELETE_PACKAGES, Manifest.permission.TEST_MANAGE_ROLLBACKS, Manifest.permission.FORCE_STOP_PACKAGES, - Manifest.permission.WRITE_DEVICE_CONFIG); + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG); } /** diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java index 4cddcfeb91dc..32deb2e8fdfc 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java @@ -70,7 +70,8 @@ public class StagedRollbackTest { Manifest.permission.DELETE_PACKAGES, Manifest.permission.TEST_MANAGE_ROLLBACKS, Manifest.permission.FORCE_STOP_PACKAGES, - Manifest.permission.WRITE_DEVICE_CONFIG); + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG); } /** 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/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/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogImplTest.java index 8913e8c1996e..05308464cb9b 100644 --- a/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/LegacyProtoLogImplTest.java @@ -89,7 +89,7 @@ public class LegacyProtoLogImplTest { //noinspection ResultOfMethodCallIgnored mFile.delete(); mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename, - 1024 * 1024, mReader, 1024, () -> {}); + 1024 * 1024, mReader, 1024, (instance) -> {}); } @After diff --git a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java index 6f3deab1d4fa..ed256e72b415 100644 --- a/tests/Tracing/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProcessedPerfettoProtoLogImplTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -40,7 +41,7 @@ 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 android.tracing.perfetto.DataSourceParams; import androidx.test.platform.app.InstrumentationRegistry; @@ -58,6 +59,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; +import org.mockito.stubbing.Answer; import perfetto.protos.Protolog; import perfetto.protos.ProtologCommon; @@ -74,7 +76,7 @@ import java.util.concurrent.atomic.AtomicInteger; @SuppressWarnings("ConstantConditions") @Presubmit @RunWith(JUnit4.class) -public class PerfettoProtoLogImplTest { +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() @@ -94,14 +96,14 @@ public class PerfettoProtoLogImplTest { ); private static ProtoLogConfigurationService sProtoLogConfigurationService; + private static ProtoLogDataSource sTestDataSource; private static PerfettoProtoLogImpl sProtoLog; private static Protolog.ProtoLogViewerConfig.Builder sViewerConfigBuilder; - private static Runnable sCacheUpdater; + private static ProtoLogCacheUpdater sCacheUpdater; private static ProtoLogViewerConfigReader sReader; - public PerfettoProtoLogImplTest() throws IOException { - } + public ProcessedPerfettoProtoLogImplTest() throws IOException { } @BeforeClass public static void setUp() throws Exception { @@ -151,37 +153,38 @@ public class PerfettoProtoLogImplTest { ViewerConfigInputStreamProvider viewerConfigInputStreamProvider = Mockito.mock( ViewerConfigInputStreamProvider.class); Mockito.when(viewerConfigInputStreamProvider.getInputStream()) - .thenAnswer(it -> new ProtoInputStream(sViewerConfigBuilder.build().toByteArray())); + .thenAnswer(it -> new AutoClosableProtoInputStream( + sViewerConfigBuilder.build().toByteArray())); - sCacheUpdater = () -> {}; + sCacheUpdater = (instance) -> {}; sReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider)); + sTestDataSource = new ProtoLogDataSource(TEST_PROTOLOG_DATASOURCE_NAME); + DataSourceParams params = + new DataSourceParams.Builder() + .setBufferExhaustedPolicy( + DataSourceParams + .PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP) + .build(); + sTestDataSource.register(params); + busyWaitForDataSourceRegistration(TEST_PROTOLOG_DATASOURCE_NAME); - 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 ProtoInputStream(sViewerConfigBuilder.build().toByteArray()); + return new AutoClosableProtoInputStream(sViewerConfigBuilder.build().toByteArray()); }); }; sProtoLogConfigurationService = - new ProtoLogConfigurationServiceImpl(dataSourceBuilder, tracer); - - if (android.tracing.Flags.clientSideProtoLogging()) { - sProtoLog = new PerfettoProtoLogImpl( - MOCK_VIEWER_CONFIG_FILE, sReader, () -> sCacheUpdater.run(), - TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService); - } else { - sProtoLog = new PerfettoProtoLogImpl( - viewerConfigInputStreamProvider, sReader, () -> sCacheUpdater.run(), - TestProtoLogGroup.values(), dataSourceBuilder, sProtoLogConfigurationService); - } + new ProtoLogConfigurationServiceImpl(sTestDataSource, tracer); - busyWaitForDataSourceRegistration(TEST_PROTOLOG_DATASOURCE_NAME); + sProtoLog = new ProcessedPerfettoProtoLogImpl(sTestDataSource, + MOCK_VIEWER_CONFIG_FILE, viewerConfigInputStreamProvider, sReader, + (instance) -> sCacheUpdater.update(instance), TestProtoLogGroup.values(), + sProtoLogConfigurationService); + sProtoLog.enable(); } @Before @@ -398,18 +401,17 @@ public class PerfettoProtoLogImplTest { } @Test - public void log_logcatEnabledNoMessage() { + 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, - new Object[]{5}); - - verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( - LogLevel.INFO), eq("UNKNOWN MESSAGE args = (5)")); - verify(sReader).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 @@ -539,16 +541,12 @@ public class PerfettoProtoLogImplTest { PerfettoTraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() .enableProtoLog(TEST_PROTOLOG_DATASOURCE_NAME) .build(); - long before; - long after; try { traceMonitor.start(); - before = SystemClock.elapsedRealtimeNanos(); sProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, 0b01100100, new Object[]{"test", 1, 0.1, true}); - after = SystemClock.elapsedRealtimeNanos(); } finally { traceMonitor.stop(mWriter); } @@ -606,14 +604,15 @@ 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); - sCacheUpdater = cacheUpdateCallCount::incrementAndGet; + sCacheUpdater = (instance) -> cacheUpdateCallCount.incrementAndGet(); PerfettoTraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder() .enableProtoLog(true, @@ -867,6 +866,39 @@ public class PerfettoProtoLogImplTest { .isEqualTo("This message should also be logged 567"); } + @Test + public void enablesLogGroupAfterLoadingConfig() { + sProtoLog.stopLoggingToLogcat( + new String[] { TestProtoLogGroup.TEST_GROUP.name() }, (msg) -> {}); + Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isFalse(); + + doAnswer((Answer<Void>) invocation -> { + // logToLogcat is still false before we laod the viewer config + Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isFalse(); + return null; + }).when(sReader).unloadViewerConfig(any(), any()); + + sProtoLog.startLoggingToLogcat( + new String[] { TestProtoLogGroup.TEST_GROUP.name() }, (msg) -> {}); + Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isTrue(); + } + + @Test + public void disablesLogGroupBeforeUnloadingConfig() { + sProtoLog.startLoggingToLogcat( + new String[] { TestProtoLogGroup.TEST_GROUP.name() }, (msg) -> {}); + Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isTrue(); + + doAnswer((Answer<Void>) invocation -> { + // Already set logToLogcat to false by the time we unload the config + Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isFalse(); + return null; + }).when(sReader).unloadViewerConfig(any(), any()); + sProtoLog.stopLoggingToLogcat( + new String[] { TestProtoLogGroup.TEST_GROUP.name() }, (msg) -> {}); + Truth.assertThat(TestProtoLogGroup.TEST_GROUP.isLogToLogcat()).isFalse(); + } + private enum TestProtoLogGroup implements IProtoLogGroup { TEST_GROUP(true, true, false, "TEST_TAG"); diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java index be0c7daebb57..e62a89b17927 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java @@ -19,6 +19,7 @@ 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.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.times; @@ -157,13 +158,15 @@ public class ProtoLogCommandHandlerTest { cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "enable", "MY_GROUP" }); - Mockito.verify(mProtoLogConfigurationService).enableProtoLogToLogcat("MY_GROUP"); + Mockito.verify(mProtoLogConfigurationService) + .enableProtoLogToLogcat(Mockito.any(), eq("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"); + .enableProtoLogToLogcat(Mockito.any(), + eq("MY_GROUP"), eq("MY_OTHER_GROUP")); } @Test @@ -173,13 +176,15 @@ public class ProtoLogCommandHandlerTest { cmdHandler.exec(mMockBinder, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err, new String[] { "logcat", "disable", "MY_GROUP" }); - Mockito.verify(mProtoLogConfigurationService).disableProtoLogToLogcat("MY_GROUP"); + Mockito.verify(mProtoLogConfigurationService) + .disableProtoLogToLogcat(Mockito.any(), eq("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"); + .disableProtoLogToLogcat(Mockito.any(), + eq("MY_GROUP"), eq("MY_OTHER_GROUP")); } @Test diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java index a3d03a8278ed..1f3f91ebf557 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java @@ -38,6 +38,8 @@ import android.tools.traces.io.ResultReader; import android.tools.traces.io.ResultWriter; import android.tools.traces.monitors.PerfettoTraceMonitor; +import com.android.internal.protolog.IProtoLogConfigurationService.RegisterClientArgs; + import com.google.common.truth.Truth; import com.google.protobuf.InvalidProtocolBufferException; @@ -60,6 +62,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.PrintWriter; import java.util.List; /** @@ -152,10 +155,9 @@ public class ProtoLogConfigurationServiceTest { 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)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { true }; service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); @@ -167,11 +169,11 @@ public class ProtoLogConfigurationServiceTest { 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()); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { true }; + args.viewerConfigFile = mViewerConfigFile.getAbsolutePath(); + service.registerClient(mMockClient, args); service.registerClient(mSecondMockClient, args); @@ -204,11 +206,11 @@ public class ProtoLogConfigurationServiceTest { 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()); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { true }; + args.viewerConfigFile = mViewerConfigFile.getAbsolutePath(); + service.registerClient(mMockClient, args); service.registerClient(mSecondMockClient, args); @@ -227,13 +229,13 @@ public class ProtoLogConfigurationServiceTest { public void sendEnableLoggingToLogcatToClient() throws RemoteException { final var service = new ProtoLogConfigurationServiceImpl(); - final var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs() - .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs - .GroupConfig(TEST_GROUP, false)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { false }; service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); - service.enableProtoLogToLogcat(TEST_GROUP); + service.enableProtoLogToLogcat(Mockito.mock(PrintWriter.class), TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); Mockito.verify(mMockClient).toggleLogcat(eq(true), @@ -244,14 +246,13 @@ public class ProtoLogConfigurationServiceTest { 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)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { true }; service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); - service.disableProtoLogToLogcat(TEST_GROUP); + service.disableProtoLogToLogcat(Mockito.mock(PrintWriter.class), TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); Mockito.verify(mMockClient).toggleLogcat(eq(false), @@ -262,14 +263,14 @@ public class ProtoLogConfigurationServiceTest { 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)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { false }; + service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); - service.enableProtoLogToLogcat(OTHER_TEST_GROUP); + service.enableProtoLogToLogcat(Mockito.mock(PrintWriter.class), OTHER_TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); Mockito.verify(mMockClient, never()).toggleLogcat(anyBoolean(), any()); @@ -280,13 +281,13 @@ public class ProtoLogConfigurationServiceTest { final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); Truth.assertThat(service.getGroups()).asList().doesNotContain(TEST_GROUP); - service.enableProtoLogToLogcat(TEST_GROUP); + service.enableProtoLogToLogcat(Mockito.mock(PrintWriter.class), TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); - final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = - new ProtoLogConfigurationServiceImpl.RegisterClientArgs() - .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs - .GroupConfig(TEST_GROUP, false)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { false }; + service.registerClient(mMockClient, args); Mockito.verify(mMockClient).toggleLogcat(eq(true), diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java index 9d56a92fad52..3d1e208189b0 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogTest.java @@ -16,6 +16,8 @@ package com.android.internal.protolog; +import static org.junit.Assert.assertThrows; + import android.platform.test.annotations.Presubmit; import com.android.internal.protolog.common.IProtoLogGroup; @@ -44,8 +46,29 @@ public class ProtoLogTest { .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; diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java index 28d7b42764c4..72b1780ceb06 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java @@ -19,9 +19,12 @@ 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 android.util.proto.ProtoInputStream; +import com.google.common.truth.Truth; + +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,6 +32,9 @@ import org.junit.runners.JUnit4; import perfetto.protos.ProtologCommon; +import java.io.File; +import java.io.IOException; + @Presubmit @RunWith(JUnit4.class) public class ProtoLogViewerConfigReaderTest { @@ -83,7 +89,7 @@ public class ProtoLogViewerConfigReaderTest { ).build().toByteArray(); private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider = - () -> new ProtoInputStream(TEST_VIEWER_CONFIG); + () -> new AutoClosableProtoInputStream(TEST_VIEWER_CONFIG); private ProtoLogViewerConfigReader mConfig; @@ -121,4 +127,47 @@ public class ProtoLogViewerConfigReaderTest { 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(); + } + + @Test + public void testMessageHashIsAvailableInFile() throws IOException { + Truth.assertThat(mConfig.messageHashIsAvailableInFile(1)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(2)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(3)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(4)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(5)).isTrue(); + Truth.assertThat(mConfig.messageHashIsAvailableInFile(6)).isFalse(); + } } diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java index ce519b7a1576..49249333b72b 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtologDataSourceTest.java @@ -67,9 +67,6 @@ public class ProtologDataSourceTest { @Test public void allEnabledTraceMode() { - final ProtoLogDataSource ds = - new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {}); - final ProtoLogDataSource.TlsState tlsState = createTlsState( DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig( ProtologConfig.ProtoLogConfig.newBuilder() @@ -154,8 +151,7 @@ public class ProtologDataSourceTest { private ProtoLogDataSource.TlsState createTlsState( DataSourceConfigOuterClass.DataSourceConfig config) { - final ProtoLogDataSource ds = - Mockito.spy(new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {})); + final ProtoLogDataSource ds = Mockito.spy(new ProtoLogDataSource()); ProtoInputStream configStream = new ProtoInputStream(config.toByteArray()); final ProtoLogDataSource.Instance dsInstance = Mockito.spy( diff --git a/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java index e2099e652c49..635e5de935c7 100644 --- a/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java +++ b/tests/UsbManagerTests/lib/src/com/android/server/usblib/UsbManagerTestLib.java @@ -18,19 +18,27 @@ package com.android.server.usblib; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbManager; +import android.hardware.usb.flags.Flags; import android.os.Binder; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.concurrent.atomic.AtomicInteger; /** @@ -43,13 +51,36 @@ public class UsbManagerTestLib { private UsbManager mUsbManagerSys; private UsbManager mUsbManagerMock; - @Mock private android.hardware.usb.IUsbManager mMockUsbService; + @Mock + private android.hardware.usb.IUsbManager mMockUsbService; + private TestParcelFileDescriptor mTestParcelFileDescriptor = new TestParcelFileDescriptor( + new ParcelFileDescriptor(new FileDescriptor())); + @Mock + private UsbAccessory mMockUsbAccessory; /** * Counter for tracking UsbOperation operations. */ private static final AtomicInteger sUsbOperationCount = new AtomicInteger(); + private class TestParcelFileDescriptor extends ParcelFileDescriptor { + + private final AtomicInteger mCloseCount = new AtomicInteger(); + + TestParcelFileDescriptor(ParcelFileDescriptor wrapped) { + super(wrapped); + } + + @Override + public void close() { + int unused = mCloseCount.incrementAndGet(); + } + + public void clearCloseCount() { + mCloseCount.set(0); + } + } + public UsbManagerTestLib(Context context) { MockitoAnnotations.initMocks(this); mContext = context; @@ -74,6 +105,34 @@ public class UsbManagerTestLib { mUsbManagerSys.setCurrentFunctions(functions); } + private InputStream openAccessoryInputStream(UsbAccessory accessory) { + try { + when(mMockUsbService.openAccessory(accessory)).thenReturn(mTestParcelFileDescriptor); + } catch (RemoteException remEx) { + Log.w(TAG, "RemoteException"); + } + + if (Flags.enableAccessoryStreamApi()) { + return mUsbManagerMock.openAccessoryInputStream(accessory); + } + + throw new UnsupportedOperationException("Stream APIs not available"); + } + + private OutputStream openAccessoryOutputStream(UsbAccessory accessory) { + try { + when(mMockUsbService.openAccessory(accessory)).thenReturn(mTestParcelFileDescriptor); + } catch (RemoteException remEx) { + Log.w(TAG, "RemoteException"); + } + + if (Flags.enableAccessoryStreamApi()) { + return mUsbManagerMock.openAccessoryOutputStream(accessory); + } + + throw new UnsupportedOperationException("Stream APIs not available"); + } + private void testSetGetCurrentFunctions_Matched(long functions) { setCurrentFunctions(functions); assertEquals("CurrentFunctions mismatched: ", functions, getCurrentFunctions()); @@ -94,7 +153,7 @@ public class UsbManagerTestLib { try { setCurrentFunctions(functions); - verify(mMockUsbService).setCurrentFunctions(eq(functions), operationId); + verify(mMockUsbService).setCurrentFunctions(eq(functions), eq(operationId)); } catch (RemoteException remEx) { Log.w(TAG, "RemoteException"); } @@ -118,7 +177,7 @@ public class UsbManagerTestLib { int operationId = sUsbOperationCount.incrementAndGet() + Binder.getCallingUid(); setCurrentFunctions(functions); - verify(mMockUsbService).setCurrentFunctions(eq(functions), operationId); + verify(mMockUsbService).setCurrentFunctions(eq(functions), eq(operationId)); } public void testGetCurrentFunctions_shouldMatched() { @@ -138,4 +197,47 @@ public class UsbManagerTestLib { testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_RNDIS); testSetCurrentFunctionsMock_Matched(UsbManager.FUNCTION_NCM); } + + public void testParcelFileDescriptorClosedWhenAllOpenStreamsAreClosed() { + mTestParcelFileDescriptor.clearCloseCount(); + try { + try (InputStream ignored = openAccessoryInputStream(mMockUsbAccessory)) { + //noinspection EmptyTryBlock + try (OutputStream ignored2 = openAccessoryOutputStream(mMockUsbAccessory)) { + // do nothing + } + } + + // ParcelFileDescriptor is closed only once. + assertEquals(mTestParcelFileDescriptor.mCloseCount.get(), 1); + mTestParcelFileDescriptor.clearCloseCount(); + } catch (IOException e) { + // do nothing + } + } + + public void testOnlyOneOpenInputStreamAllowed() { + try { + //noinspection EmptyTryBlock + try (InputStream ignored = openAccessoryInputStream(mMockUsbAccessory)) { + assertThrows(IllegalStateException.class, + () -> openAccessoryInputStream(mMockUsbAccessory)); + } + } catch (IOException e) { + // do nothing + } + } + + public void testOnlyOneOpenOutputStreamAllowed() { + try { + //noinspection EmptyTryBlock + try (OutputStream ignored = openAccessoryOutputStream(mMockUsbAccessory)) { + assertThrows(IllegalStateException.class, + () -> openAccessoryOutputStream(mMockUsbAccessory)); + } + } catch (IOException e) { + // do nothing + } + } + } diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java index 8b21763b4a24..40fd0b431451 100644 --- a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java +++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbManagerApiTest.java @@ -18,17 +18,21 @@ package com.android.server.usbtest; import android.content.Context; import android.hardware.usb.UsbManager; +import android.hardware.usb.flags.Flags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import org.junit.Ignore; +import com.android.server.usblib.UsbManagerTestLib; + +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import com.android.server.usblib.UsbManagerTestLib; - /** * Unit tests for {@link android.hardware.usb.UsbManager}. * Note: MUST claimed MANAGE_USB permission in Manifest @@ -41,6 +45,9 @@ public class UsbManagerApiTest { private final UsbManagerTestLib mUsbManagerTestLib = new UsbManagerTestLib(mContext = InstrumentationRegistry.getContext()); + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); /** * Verify NO SecurityException * Go through System Server @@ -92,4 +99,23 @@ public class UsbManagerApiTest { public void testUsbApi_SetCurrentFunctions_shouldMatched() { mUsbManagerTestLib.testSetCurrentFunctions_shouldMatched(); } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API) + public void testUsbApi_closesParcelFileDescriptorAfterAllStreamsClosed() { + mUsbManagerTestLib.testParcelFileDescriptorClosedWhenAllOpenStreamsAreClosed(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API) + public void testUsbApi_callingOpenAccessoryInputStreamTwiceThrowsException() { + mUsbManagerTestLib.testOnlyOneOpenInputStreamAllowed(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_ACCESSORY_STREAM_API) + public void testUsbApi_callingOpenAccessoryOutputStreamTwiceThrowsException() { + mUsbManagerTestLib.testOnlyOneOpenOutputStreamAllowed(); + } + } 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 deleted file mode 100644 index d2e1f815e8dc..000000000000 --- a/tests/broadcasts/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Bug component: 316181 -include platform/frameworks/base:/BROADCASTS_OWNERS diff --git a/tests/broadcasts/unit/Android.bp b/tests/broadcasts/unit/Android.bp index 47166a713580..9e15ac41d84b 100644 --- a/tests/broadcasts/unit/Android.bp +++ b/tests/broadcasts/unit/Android.bp @@ -1,4 +1,3 @@ -// // Copyright (C) 2024 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,7 +12,6 @@ // 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 @@ -38,6 +36,7 @@ android_test { "truth", "flag-junit", "android.app.flags-aconfig-java", + "junit-params", ], certificate: "platform", platform_apis: true, diff --git a/tests/broadcasts/unit/AndroidManifest.xml b/tests/broadcasts/unit/AndroidManifest.xml index e9c5248e4d98..61eb230f7957 100644 --- a/tests/broadcasts/unit/AndroidManifest.xml +++ b/tests/broadcasts/unit/AndroidManifest.xml @@ -22,6 +22,6 @@ </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" - android:targetPackage="com.android.broadcasts.unit" - android:label="Broadcasts Unit Tests"/> + android:targetPackage="com.android.broadcasts.unit" + android:label="Broadcasts Unit Tests"/> </manifest>
\ No newline at end of file diff --git a/tests/broadcasts/unit/OWNERS b/tests/broadcasts/unit/OWNERS new file mode 100644 index 000000000000..f1e450b7e5f9 --- /dev/null +++ b/tests/broadcasts/unit/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 316181 +include platform/frameworks/base:/BROADCASTS_OWNERS
\ No newline at end of file diff --git a/tests/broadcasts/unit/TEST_MAPPING b/tests/broadcasts/unit/TEST_MAPPING index 0e824c54e92e..b920e2586c86 100644 --- a/tests/broadcasts/unit/TEST_MAPPING +++ b/tests/broadcasts/unit/TEST_MAPPING @@ -1,15 +1,7 @@ { "postsubmit": [ { - "name": "BroadcastUnitTests", - "options": [ - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - }, - { - "exclude-annotation": "org.junit.Ignore" - } - ] + "name": "BroadcastUnitTests" } ] } diff --git a/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java index b7c412dea999..15a580c9e8f7 100644 --- a/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java +++ b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java @@ -13,246 +13,236 @@ * 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; +package android.app; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; -import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.content.Intent; import android.content.IntentFilter; -import android.os.BatteryManager; -import android.os.Bundle; -import android.os.SystemProperties; +import android.media.AudioManager; +import android.os.IpcDataCache; +import android.os.RemoteException; +import android.platform.test.annotations.DisableFlags; 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.dx.mockito.inline.extended.ExtendedMockito; +import com.android.internal.annotations.Keep; import com.android.modules.utils.testing.ExtendedMockitoRule; -import org.junit.After; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + 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; +import org.mockito.Mock; -@EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE) -@RunWith(AndroidJUnit4.class) -@SmallTest +@RunWith(JUnitParamsRunner.class) public class BroadcastStickyCacheTest { - @ClassRule - public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); - @Rule - public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Rule public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) - .mockStatic(SystemProperties.class) + .mockStatic(IpcDataCache.class) + .mockStatic(ActivityManager.class) .build(); - private static final String PROP_KEY_BATTERY_CHANGED = BroadcastStickyCache.getKey( - ACTION_BATTERY_CHANGED); + @Mock + private IActivityManager mActivityManagerMock; - private final TestSystemProps mTestSystemProps = new TestSystemProps(); + @Mock + private IApplicationThread mIApplicationThreadMock; + + @Keep + private static Object stickyBroadcastList() { + return BroadcastStickyCache.STICKY_BROADCAST_ACTIONS; + } @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())); - } + BroadcastStickyCache.clearCacheForTest(); - @After - public void tearDown() { - mTestSystemProps.clear(); - BroadcastStickyCache.clearForTest(); + doNothing().when(() -> IpcDataCache.invalidateCache(anyString(), anyString())); } @Test - public void testUseCache_nullFilter() { - assertThat(BroadcastStickyCache.useCache(null)).isEqualTo(false); + @DisableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE) + public void useCache_flagDisabled_returnsFalse() { + assertFalse(BroadcastStickyCache.useCache(new IntentFilter(Intent.ACTION_BATTERY_CHANGED))); } @Test - public void testUseCache_noActions() { - final IntentFilter filter = new IntentFilter(); - assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false); + @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE) + public void useCache_nullFilter_returnsFalse() { + assertFalse(BroadcastStickyCache.useCache(null)); } @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); + @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE) + public void useCache_filterWithoutAction_returnsFalse() { + assertFalse(BroadcastStickyCache.useCache(new IntentFilter())); } @Test - public void testUseCache_valueNotSet() { - final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED); - assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false); + @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE) + public void useCache_filterWithoutStickyBroadcastAction_returnsFalse() { + assertFalse(BroadcastStickyCache.useCache(new IntentFilter(Intent.ACTION_BOOT_COMPLETED))); } @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); - } + @DisableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE) + public void invalidateCache_flagDisabled_cacheNotInvalidated() { + final String apiName = BroadcastStickyCache.sActionApiNameMap.get( + AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); - @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); - } + BroadcastStickyCache.invalidateCache( + AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); - @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); + ExtendedMockito.verify( + () -> IpcDataCache.invalidateCache(eq(IpcDataCache.MODULE_SYSTEM), eq(apiName)), + times(0)); } @Test - public void testIncrementVersion_propExists() { - SystemProperties.set(PROP_KEY_BATTERY_CHANGED, String.valueOf(100)); + @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE) + public void invalidateCache_broadcastNotSticky_cacheNotInvalidated() { + BroadcastStickyCache.invalidateCache(Intent.ACTION_AIRPLANE_MODE_CHANGED); - 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); + ExtendedMockito.verify( + () -> IpcDataCache.invalidateCache(eq(IpcDataCache.MODULE_SYSTEM), anyString()), + times(0)); } @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); - } + @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE) + public void invalidateCache_withStickyBroadcast_cacheInvalidated() { + final String apiName = BroadcastStickyCache.sActionApiNameMap.get( + Intent.ACTION_BATTERY_CHANGED); - @Test - public void testIncrementVersionIfExists_propExists() { - BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED); + BroadcastStickyCache.invalidateCache(Intent.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); + ExtendedMockito.verify( + () -> IpcDataCache.invalidateCache(eq(IpcDataCache.MODULE_SYSTEM), eq(apiName)), + times(1)); } @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); - } + public void invalidateAllCaches_cacheInvalidated() { + BroadcastStickyCache.invalidateAllCaches(); - private void assertEquals(Intent actualIntent, Intent expectedIntent) { - assertThat(actualIntent.getAction()).isEqualTo(expectedIntent.getAction()); - assertEquals(actualIntent.getExtras(), expectedIntent.getExtras()); + for (int i = BroadcastStickyCache.sActionApiNameMap.size() - 1; i > -1; i--) { + final String apiName = BroadcastStickyCache.sActionApiNameMap.valueAt(i); + ExtendedMockito.verify(() -> IpcDataCache.invalidateCache(anyString(), + eq(apiName)), times(1)); + } } - private void assertEquals(Bundle actualExtras, Bundle expectedExtras) { - assertWithMessage("Extras expected=%s, actual=%s", expectedExtras, actualExtras) - .that(actualExtras.kindofEquals(expectedExtras)).isTrue(); - } + @Test + @Parameters(method = "stickyBroadcastList") + public void getIntent_createNewCache_verifyRegisterReceiverIsCalled(String action) + throws RemoteException { + setActivityManagerMock(action); + final IntentFilter filter = new IntentFilter(action); + final Intent intent = queryIntent(filter); - private static final class TestSystemProps { - @GuardedBy("mSysProps") - private final ArrayMap<String, Long> mSysProps = new ArrayMap<>(); + assertNotNull(intent); + assertEquals(intent.getAction(), action); + verify(mActivityManagerMock, times(1)).registerReceiverWithFeature( + eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(), + eq(filter), anyString(), anyInt(), anyInt()); + } - public void add(String name, long value) { - synchronized (mSysProps) { - mSysProps.put(name, value); - } - } + @Test + public void getIntent_querySameValueTwice_verifyRegisterReceiverIsCalledOnce() + throws RemoteException { + setActivityManagerMock(Intent.ACTION_DEVICE_STORAGE_LOW); + final Intent intent = queryIntent(new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW)); + final Intent cachedIntent = queryIntent(new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW)); - public long get(String name, long defaultValue) { - synchronized (mSysProps) { - final int idx = mSysProps.indexOfKey(name); - return idx >= 0 ? mSysProps.valueAt(idx) : defaultValue; - } - } + assertNotNull(intent); + assertEquals(intent.getAction(), Intent.ACTION_DEVICE_STORAGE_LOW); + assertNotNull(cachedIntent); + assertEquals(cachedIntent.getAction(), Intent.ACTION_DEVICE_STORAGE_LOW); - public Handle query(String name) { - synchronized (mSysProps) { - return mSysProps.containsKey(name) ? new Handle(name) : null; - } - } + verify(mActivityManagerMock, times(1)).registerReceiverWithFeature( + eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(), + any(), anyString(), anyInt(), anyInt()); + } - public void clear() { - synchronized (mSysProps) { - mSysProps.clear(); - } - } + @Test + public void getIntent_queryActionTwiceWithNullResult_verifyRegisterReceiverIsCalledOnce() + throws RemoteException { + setActivityManagerMock(null); + final Intent intent = queryIntent(new IntentFilter(Intent.ACTION_DEVICE_STORAGE_FULL)); + final Intent cachedIntent = queryIntent( + new IntentFilter(Intent.ACTION_DEVICE_STORAGE_FULL)); - public class Handle { - private final String mName; + assertNull(intent); + assertNull(cachedIntent); - Handle(String name) { - mName = name; - } + verify(mActivityManagerMock, times(1)).registerReceiverWithFeature( + eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(), + any(), anyString(), anyInt(), anyInt()); + } - public long getLong(long defaultValue) { - return get(mName, defaultValue); - } - } + @Test + public void getIntent_querySameActionWithDifferentFilter_verifyRegisterReceiverCalledTwice() + throws RemoteException { + setActivityManagerMock(Intent.ACTION_DEVICE_STORAGE_LOW); + final IntentFilter filter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); + final Intent intent = queryIntent(filter); + + final IntentFilter newFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); + newFilter.addDataScheme("file"); + final Intent newIntent = queryIntent(newFilter); + + assertNotNull(intent); + assertEquals(intent.getAction(), Intent.ACTION_DEVICE_STORAGE_LOW); + assertNotNull(newIntent); + assertEquals(newIntent.getAction(), Intent.ACTION_DEVICE_STORAGE_LOW); + + verify(mActivityManagerMock, times(1)).registerReceiverWithFeature( + eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(), + eq(filter), anyString(), anyInt(), anyInt()); + + verify(mActivityManagerMock, times(1)).registerReceiverWithFeature( + eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(), + eq(newFilter), anyString(), anyInt(), anyInt()); + } + + private Intent queryIntent(IntentFilter filter) { + return BroadcastStickyCache.getIntent( + mIApplicationThreadMock, + "android", + "android", + filter, + "system", + 0, + 0 + ); + } + + private void setActivityManagerMock(String action) throws RemoteException { + when(ActivityManager.getService()).thenReturn(mActivityManagerMock); + when(mActivityManagerMock.registerReceiverWithFeature(any(), anyString(), + anyString(), anyString(), any(), any(), anyString(), anyInt(), + anyInt())).thenReturn(action != null ? new Intent(action) : null); } } 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/AndroidManifest.xml b/tests/graphics/SilkFX/AndroidManifest.xml index 25092b52e2b6..c293589bdbaf 100644 --- a/tests/graphics/SilkFX/AndroidManifest.xml +++ b/tests/graphics/SilkFX/AndroidManifest.xml @@ -23,13 +23,12 @@ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> <application android:label="SilkFX" - android:theme="@style/Theme.UsefulDefault"> + android:theme="@android:style/Theme.Material"> <activity android:name=".Main" android:label="SilkFX Demos" android:banner="@drawable/background1" - android:exported="true" - android:theme="@style/Theme.UsefulDefault"> + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.DEFAULT"/> diff --git a/tests/graphics/SilkFX/res/layout/activity_background_blur.xml b/tests/graphics/SilkFX/res/layout/activity_background_blur.xml index f13c0883cb01..27eca82dcb23 100644 --- a/tests/graphics/SilkFX/res/layout/activity_background_blur.xml +++ b/tests/graphics/SilkFX/res/layout/activity_background_blur.xml @@ -13,161 +13,168 @@ ~ 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" +--> +<FrameLayout + 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:id="@+id/background" - android:layout_width="390dp" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:padding="15dp" - android:orientation="vertical" + android:fitsSystemWindows="true" + android:layout_width="match_parent" + android:layout_height="match_parent" tools:context=".materials.BackgroundBlurActivity"> - <TextView - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="center_horizontal" - android:padding="10dp" - android:textColor="#ffffffff" - android:text="Hello blurry world!"/> - <LinearLayout - android:layout_width="match_parent" + android:id="@+id/background" + android:layout_width="390dp" android:layout_height="wrap_content" - android:orientation="horizontal"> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_weight="1" - android:textColor="#ffffffff" - android:text="Background blur"/> + android:layout_gravity="center" + android:padding="15dp" + android:orientation="vertical"> - <SeekBar - android:id="@+id/set_background_blur" - android:min="0" - android:max="300" - android:layout_width="160dp" - android:layout_height="wrap_content"/> - <TextView - android:id="@+id/background_blur_radius" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textColor="#ffffffff" - android:ems="3" - android:gravity="center" - android:paddingLeft="10dp" - android:paddingRight="10dp" - android:text="TODO"/> - </LinearLayout> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal"> <TextView - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_weight="1" + android:gravity="center_horizontal" + android:padding="10dp" android:textColor="#ffffffff" - android:text="Background alpha"/> + android:text="Hello blurry world!"/> - <SeekBar - android:id="@+id/set_background_alpha" - android:min="0" - android:max="100" - android:layout_width="160dp" - android:layout_height="wrap_content" /> - <TextView - android:id="@+id/background_alpha" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textColor="#ffffffff" - android:ems="3" - android:gravity="center" - android:paddingLeft="10dp" - android:paddingRight="10dp" - android:text="TODO"/> - </LinearLayout> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal"> - <TextView - android:layout_width="wrap_content" + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_weight="1" - android:textColor="#ffffffff" - android:text="Blur behind"/> + android:orientation="horizontal"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textColor="#ffffffff" + android:text="Background blur"/> - <SeekBar - android:id="@+id/set_blur_behind" - android:min="0" - android:max="300" - android:layout_width="160dp" - android:layout_height="wrap_content" /> - <TextView - android:id="@+id/blur_behind_radius" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - android:textColor="#ffffffff" - android:paddingLeft="10dp" - android:paddingRight="10dp" - android:ems="3" - android:text="TODO"/> - </LinearLayout> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal"> - <TextView - android:layout_width="wrap_content" + <SeekBar + android:id="@+id/set_background_blur" + android:min="0" + android:max="300" + android:layout_width="160dp" + android:layout_height="wrap_content"/> + <TextView + android:id="@+id/background_blur_radius" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="#ffffffff" + android:ems="3" + android:gravity="center" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:text="TODO"/> + </LinearLayout> + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_weight="1" - android:textColor="#ffffffff" - android:text="Dim amount"/> + android:orientation="horizontal"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textColor="#ffffffff" + android:text="Background alpha"/> - <SeekBar - android:id="@+id/set_dim_amount" - android:min="0" - android:max="100" - android:layout_width="160dp" - android:layout_height="wrap_content" /> - <TextView - android:id="@+id/dim_amount" - android:layout_width="wrap_content" + <SeekBar + android:id="@+id/set_background_alpha" + android:min="0" + android:max="100" + android:layout_width="160dp" + android:layout_height="wrap_content" /> + <TextView + android:id="@+id/background_alpha" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="#ffffffff" + android:ems="3" + android:gravity="center" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:text="TODO"/> + </LinearLayout> + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="center" - android:textColor="#ffffffff" - android:paddingLeft="10dp" - android:paddingRight="10dp" - android:ems="3" - android:text="TODO"/> - </LinearLayout> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:layout_marginTop="5dp" - android:orientation="vertical" - android:gravity="center"> + android:orientation="horizontal"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textColor="#ffffffff" + android:text="Blur behind"/> - <Button - android:id="@+id/toggle_blur_enabled" + <SeekBar + android:id="@+id/set_blur_behind" + android:min="0" + android:max="300" + android:layout_width="160dp" + android:layout_height="wrap_content" /> + <TextView + android:id="@+id/blur_behind_radius" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="#ffffffff" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:ems="3" + android:text="TODO"/> + </LinearLayout> + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Disable blur" - android:onClick="toggleForceBlurDisabled"/> + android:orientation="horizontal"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textColor="#ffffffff" + android:text="Dim amount"/> - <Button - android:id="@+id/toggle_battery_saving_mode" + <SeekBar + android:id="@+id/set_dim_amount" + android:min="0" + android:max="100" + android:layout_width="160dp" + android:layout_height="wrap_content" /> + <TextView + android:id="@+id/dim_amount" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center" + android:textColor="#ffffffff" + android:paddingLeft="10dp" + android:paddingRight="10dp" + android:ems="3" + android:text="TODO"/> + </LinearLayout> + + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="TODO" - android:onClick="toggleBatterySavingMode"/> - </LinearLayout> - <requestFocus/> + android:layout_gravity="center" + android:layout_marginTop="5dp" + android:orientation="vertical" + android:gravity="center"> + + <Button + android:id="@+id/toggle_blur_enabled" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Disable blur" + android:onClick="toggleForceBlurDisabled"/> -</LinearLayout> + <Button + android:id="@+id/toggle_battery_saving_mode" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="TODO" + android:onClick="toggleBatterySavingMode"/> + </LinearLayout> + <requestFocus/> + + </LinearLayout> +</FrameLayout> diff --git a/tests/graphics/SilkFX/res/layout/activity_glass.xml b/tests/graphics/SilkFX/res/layout/activity_glass.xml index aa09f276d5c8..d591fc4606b0 100644 --- a/tests/graphics/SilkFX/res/layout/activity_glass.xml +++ b/tests/graphics/SilkFX/res/layout/activity_glass.xml @@ -19,6 +19,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".MainActivity"> <ImageView @@ -300,4 +301,4 @@ </androidx.constraintlayout.widget.ConstraintLayout> -</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/tests/graphics/SilkFX/res/layout/color_mode_controls.xml b/tests/graphics/SilkFX/res/layout/color_mode_controls.xml index c0c0bab8a605..9b2b0c818a8e 100644 --- a/tests/graphics/SilkFX/res/layout/color_mode_controls.xml +++ b/tests/graphics/SilkFX/res/layout/color_mode_controls.xml @@ -19,6 +19,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_margin="8dp" android:orientation="vertical"> <TextView @@ -61,4 +62,4 @@ </LinearLayout> -</com.android.test.silkfx.common.ColorModeControls>
\ No newline at end of file +</com.android.test.silkfx.common.ColorModeControls> diff --git a/tests/graphics/SilkFX/res/layout/common_base.xml b/tests/graphics/SilkFX/res/layout/common_base.xml index c0eaf9bc1476..ce6d850af1bc 100644 --- a/tests/graphics/SilkFX/res/layout/common_base.xml +++ b/tests/graphics/SilkFX/res/layout/common_base.xml @@ -18,6 +18,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical"> <include layout="@layout/color_mode_controls" /> @@ -26,4 +27,4 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/tests/graphics/SilkFX/res/layout/hdr_glows.xml b/tests/graphics/SilkFX/res/layout/hdr_glows.xml index b6050645866a..f1e553a3df23 100644 --- a/tests/graphics/SilkFX/res/layout/hdr_glows.xml +++ b/tests/graphics/SilkFX/res/layout/hdr_glows.xml @@ -18,6 +18,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:orientation="vertical"> <include layout="@layout/color_mode_controls" /> @@ -48,4 +49,4 @@ android:layout_height="50dp" android:layout_margin="8dp" /> -</LinearLayout>
\ No newline at end of file +</LinearLayout> 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/res/values/style.xml b/tests/graphics/SilkFX/res/values/style.xml index 4dd626dfb8f5..75506978024b 100644 --- a/tests/graphics/SilkFX/res/values/style.xml +++ b/tests/graphics/SilkFX/res/values/style.xml @@ -16,21 +16,16 @@ --> <!-- Styles for immersive actions UI. --> <resources xmlns:android="http://schemas.android.com/apk/res/android"> - <style name="Theme.BackgroundBlurTheme" parent= "Theme.AppCompat.Dialog"> + <style name="Theme.BackgroundBlurTheme" parent="Theme.AppCompat.Dialog"> <item name="android:windowIsTranslucent">true</item> <item name="android:windowBlurBehindEnabled">true</item> <item name="android:backgroundDimEnabled">false</item> <item name="android:windowElevation">0dp</item> <item name="buttonStyle">@style/AppTheme.Button</item> <item name="colorAccent">#bbffffff</item> - <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> </style> <style name="AppTheme.Button" parent="Widget.AppCompat.Button"> <item name="android:textColor">#ffffffff</item> </style> - <style name="Theme.UsefulDefault" parent="android:Theme.Material"> - <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> - </style> - </resources> 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..ad7cde44bb35 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) )) ) @@ -71,6 +72,7 @@ class Main : Activity() { super.onCreate(savedInstanceState) val list = ExpandableListView(this) + list.setFitsSystemWindows(true) setContentView(list) 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 44aa4028c916..370c0048d9a9 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp @@ -38,6 +38,9 @@ android_test { ], test_suites: [ "general-tests", + // This is an equivalent of general-tests for automotive. + // It helps manage the build time on automotive branches. + "automotive-general-tests", ], sdk_version: "test_current", diff --git a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java index 07b733830bd3..0da4521fca71 100644 --- a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java +++ b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java @@ -143,6 +143,38 @@ public class VibratorManagerServicePermissionTest { } @Test + public void testStartVendorVibrationSessionWithoutVibratePermissionFails() throws Exception { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.VIBRATE_VENDOR_EFFECTS, + Manifest.permission.START_VIBRATION_SESSIONS); + expectSecurityException("VIBRATE"); + mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME, + new int[] { 1 }, ATTRS, "testVibrate", null); + } + + @Test + public void testStartVendorVibrationSessionWithoutVibrateVendorEffectsPermissionFails() + throws Exception { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.VIBRATE, + Manifest.permission.START_VIBRATION_SESSIONS); + expectSecurityException("VIBRATE"); + mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME, + new int[] { 1 }, ATTRS, "testVibrate", null); + } + + @Test + public void testStartVendorVibrationSessionWithoutStartSessionPermissionFails() + throws Exception { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.VIBRATE, + Manifest.permission.VIBRATE_VENDOR_EFFECTS); + expectSecurityException("VIBRATE"); + mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME, + new int[] { 1 }, ATTRS, "testVibrate", null); + } + + @Test public void testCancelVibrateFails() throws RemoteException { expectSecurityException("VIBRATE"); mVibratorService.cancelVibrate(/* usageFilter= */ -1, new Binder()); 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/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/OWNERS b/tests/testables/src/android/testing/OWNERS new file mode 100644 index 000000000000..f31666b43654 --- /dev/null +++ b/tests/testables/src/android/testing/OWNERS @@ -0,0 +1,2 @@ +# MessageQueue-related classes +per-file TestableLooper.java = mfasheh@google.com, shayba@google.com 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..8cd89ce89e83 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -16,6 +16,7 @@ package android.testing; import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -24,7 +25,7 @@ import android.os.MessageQueue; import android.os.TestLooperManager; import android.util.ArrayMap; -import androidx.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; import org.junit.runners.model.FrameworkMethod; @@ -33,8 +34,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayDeque; import java.util.Map; import java.util.Objects; +import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -42,6 +46,12 @@ import java.util.concurrent.atomic.AtomicBoolean; * and provide an easy annotation for use with tests. * * @see TestableLooperTest TestableLooperTest for examples. + * + * @deprecated Use {@link android.os.TestLooperManager} or {@link + * org.robolectric.shadows.ShadowLooper} instead. + * This class is not actively maintained. + * Both of the recommended alternatives allow fine control of execution. + * The Robolectric class also allows advancing time. */ public class TestableLooper { @@ -61,16 +71,38 @@ public class TestableLooper { private Handler mHandler; private TestLooperManager mQueueWrapper; + /** + * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. + */ + private static boolean isAtLeastBaklava() { + Method[] methods = TestLooperManager.class.getMethods(); + for (Method method : methods) { + if (method.getName().equals("peekWhen")) { + return true; + } + } + return false; + // TODO(shayba): delete the above, uncomment the below. + // SDK_INT has not yet ramped to Baklava in all 25Q2 builds. + // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + } + static { - try { - MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); - MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); - MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); - MESSAGE_NEXT_FIELD.setAccessible(true); - MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); - MESSAGE_WHEN_FIELD.setAccessible(true); - } catch (NoSuchFieldException e) { - throw new RuntimeException("Failed to initialize TestableLooper", e); + if (isAtLeastBaklava()) { + MESSAGE_QUEUE_MESSAGES_FIELD = null; + MESSAGE_NEXT_FIELD = null; + MESSAGE_WHEN_FIELD = null; + } else { + try { + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + } catch (NoSuchFieldException e) { + throw new RuntimeException("Failed to initialize TestableLooper", e); + } } } @@ -216,8 +248,61 @@ public class TestableLooper { } public void moveTimeForward(long milliSeconds) { + if (isAtLeastBaklava()) { + moveTimeForwardBaklava(milliSeconds); + } else { + moveTimeForwardLegacy(milliSeconds); + } + } + + private void moveTimeForwardBaklava(long milliSeconds) { + // Drain all Messages from the queue. + Queue<Message> messages = new ArrayDeque<>(); + while (true) { + Message message = mQueueWrapper.poll(); + if (message == null) { + break; + } + + // Adjust the Message's delivery time. + long newWhen = message.when - milliSeconds; + if (newWhen < 0) { + newWhen = 0; + } + message.when = newWhen; + messages.add(message); + } + + // Repost all Messages back to the queue with a new time. + while (true) { + Message message = messages.poll(); + if (message == null) { + break; + } + + Runnable callback = message.getCallback(); + Handler handler = message.getTarget(); + long when = message.getWhen(); + + // The Message cannot be re-enqueued because it is marked in use. + // Make a copy of the Message and recycle the original. + // This resets {@link Message#isInUse()} but retains all other content. + { + Message newMessage = Message.obtain(); + newMessage.copyFrom(message); + newMessage.setCallback(callback); + mQueueWrapper.recycle(message); + message = newMessage; + } + + // Send the Message back to its Handler to be re-enqueued. + handler.sendMessageAtTime(message, when); + } + } + + private void moveTimeForwardLegacy(long milliSeconds) { try { - Message msg = getMessageLinkedList(); + Message msg = (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(mLooper.getQueue()); while (msg != null) { long updatedWhen = msg.getWhen() - milliSeconds; if (updatedWhen < 0) { @@ -231,17 +316,6 @@ public class TestableLooper { } } - private Message getMessageLinkedList() { - try { - MessageQueue queue = mLooper.getQueue(); - return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); - } catch (IllegalAccessException e) { - throw new RuntimeException( - "Access failed in TestableLooper: get - MessageQueue.mMessages", - e); - } - } - private int processQueuedMessages() { int count = 0; Runnable barrierRunnable = () -> { }; diff --git a/tests/testables/tests/Android.bp b/tests/testables/tests/Android.bp index 1eb36fa5f908..f0cda535b3aa 100644 --- a/tests/testables/tests/Android.bp +++ b/tests/testables/tests/Android.bp @@ -29,13 +29,19 @@ android_test { "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", ], @@ -50,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..392bf67cb13b --- /dev/null +++ b/tests/testables/tests/AndroidTest.xml @@ -0,0 +1,52 @@ +<?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="directory-keys" value="/data/user/10/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/AnimatorTestRuleToolkitTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt new file mode 100644 index 000000000000..2eb8ba1be811 --- /dev/null +++ b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt @@ -0,0 +1,210 @@ +/* + * 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.content.pm.PackageManager +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 +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.Assume.assumeFalse +import org.junit.Before +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, + ) + + @Before + fun setUp() { + // Do not run on Automotive. + assumeFalse( + InstrumentationRegistry.getInstrumentation().context.packageManager.hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE + ) + ) + } + + @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/OWNERS b/tests/utils/testutils/java/android/os/test/OWNERS index 3a9129e1bb69..6448261102fa 100644 --- a/tests/utils/testutils/java/android/os/test/OWNERS +++ b/tests/utils/testutils/java/android/os/test/OWNERS @@ -1 +1,4 @@ per-file FakePermissionEnforcer.java = file:/tests/EnforcePermission/OWNERS + +# MessageQueue-related classes +per-file TestLooper.java = mfasheh@google.com, shayba@google.com diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index a826646f69f3..4d379e45a81a 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -18,27 +18,41 @@ package android.os.test; import static org.junit.Assert.assertTrue; +import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.os.SystemClock; +import android.os.TestLooperManager; import android.util.Log; +import androidx.test.platform.app.InstrumentationRegistry; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Queue; import java.util.concurrent.Executor; /** - * Creates a looper whose message queue can be manipulated - * This allows testing code that uses a looper to dispatch messages in a deterministic manner - * Creating a TestLooper will also install it as the looper for the current thread + * Creates a looper whose message queue can be manipulated This allows testing code that uses a + * looper to dispatch messages in a deterministic manner Creating a TestLooper will also install it + * as the looper for the current thread + * + * @deprecated Use {@link android.os.TestLooperManager} or {@link + * org.robolectric.shadows.ShadowLooper} instead. + * This class is not actively maintained. + * Both of the recommended alternatives allow fine control of execution. + * The Robolectric class also allows advancing time. */ public class TestLooper { - protected final Looper mLooper; + private final Looper mLooper; + private final TestLooperManager mTestLooperManager; + private final Clock mClock; private static final Constructor<Looper> LOOPER_CONSTRUCTOR; private static final Field THREAD_LOCAL_LOOPER_FIELD; @@ -48,24 +62,46 @@ public class TestLooper { private static final Method MESSAGE_MARK_IN_USE_METHOD; private static final String TAG = "TestLooper"; - private final Clock mClock; - private AutoDispatchThread mAutoDispatchThread; + /** + * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection. + */ + private static boolean isAtLeastBaklava() { + Method[] methods = TestLooperManager.class.getMethods(); + for (Method method : methods) { + if (method.getName().equals("peekWhen")) { + return true; + } + } + return false; + // TODO(shayba): delete the above, uncomment the below. + // SDK_INT has not yet ramped to Baklava in all 25Q2 builds. + // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA; + } + static { try { LOOPER_CONSTRUCTOR = Looper.class.getDeclaredConstructor(Boolean.TYPE); LOOPER_CONSTRUCTOR.setAccessible(true); THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal"); THREAD_LOCAL_LOOPER_FIELD.setAccessible(true); - MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); - MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); - MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); - MESSAGE_NEXT_FIELD.setAccessible(true); - MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); - MESSAGE_WHEN_FIELD.setAccessible(true); - MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); - MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + + if (isAtLeastBaklava()) { + MESSAGE_QUEUE_MESSAGES_FIELD = null; + MESSAGE_NEXT_FIELD = null; + MESSAGE_WHEN_FIELD = null; + MESSAGE_MARK_IN_USE_METHOD = null; + } else { + MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages"); + MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true); + MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next"); + MESSAGE_NEXT_FIELD.setAccessible(true); + MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when"); + MESSAGE_WHEN_FIELD.setAccessible(true); + MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse"); + MESSAGE_MARK_IN_USE_METHOD.setAccessible(true); + } } catch (NoSuchFieldException | NoSuchMethodException e) { throw new RuntimeException("Failed to initialize TestLooper", e); } @@ -93,13 +129,20 @@ 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 (isAtLeastBaklava()) { + mTestLooperManager = + InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper); + } else { + mTestLooperManager = null; + } + mClock = clock; } @@ -111,19 +154,61 @@ public class TestLooper { return new HandlerExecutor(new Handler(getLooper())); } - private Message getMessageLinkedList() { + private Message getMessageLinkedListLegacy() { try { MessageQueue queue = mLooper.getQueue(); return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue); } catch (IllegalAccessException e) { throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages", - e); + e); } } public void moveTimeForward(long milliSeconds) { + if (isAtLeastBaklava()) { + moveTimeForwardBaklava(milliSeconds); + } else { + moveTimeForwardLegacy(milliSeconds); + } + } + + private void moveTimeForwardBaklava(long milliSeconds) { + // Drain all Messages from the queue. + Queue<Message> messages = new ArrayDeque<>(); + while (true) { + Message message = mTestLooperManager.poll(); + if (message == null) { + break; + } + messages.add(message); + } + + // Repost all Messages back to the queue with a new time. + while (true) { + Message message = messages.poll(); + if (message == null) { + break; + } + + // Ugly trick to reset the Message's "in use" flag. + // This is needed because the Message cannot be re-enqueued if it's + // marked in use. + message.copyFrom(message); + + // Adjust the Message's delivery time. + long newWhen = message.getWhen() - milliSeconds; + if (newWhen < 0) { + newWhen = 0; + } + + // Send the Message back to its Handler to be re-enqueued. + message.getTarget().sendMessageAtTime(message, newWhen); + } + } + + private void moveTimeForwardLegacy(long milliSeconds) { try { - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); while (msg != null) { long updatedWhen = msg.getWhen() - milliSeconds; if (updatedWhen < 0) { @@ -141,12 +226,12 @@ public class TestLooper { return mClock.uptimeMillis(); } - private Message messageQueueNext() { + private Message messageQueueNextLegacy() { try { long now = currentTime(); Message prevMsg = null; - Message msg = getMessageLinkedList(); + Message msg = getMessageLinkedListLegacy(); if (msg != null && msg.getTarget() == null) { // Stalled by a barrier. Find the next asynchronous message in // the queue. @@ -179,18 +264,46 @@ public class TestLooper { /** * @return true if there are pending messages in the message queue */ - public synchronized boolean isIdle() { - Message messageList = getMessageLinkedList(); + public boolean isIdle() { + if (isAtLeastBaklava()) { + return isIdleBaklava(); + } else { + return isIdleLegacy(); + } + } + + private boolean isIdleBaklava() { + Long when = mTestLooperManager.peekWhen(); + return when != null && currentTime() >= when; + } + private synchronized boolean isIdleLegacy() { + Message messageList = getMessageLinkedListLegacy(); return messageList != null && currentTime() >= messageList.getWhen(); } /** * @return the next message in the Looper's message queue or null if there is none */ - public synchronized Message nextMessage() { + public Message nextMessage() { + if (isAtLeastBaklava()) { + return nextMessageBaklava(); + } else { + return nextMessageLegacy(); + } + } + + private Message nextMessageBaklava() { + if (isIdle()) { + return mTestLooperManager.poll(); + } else { + return null; + } + } + + private synchronized Message nextMessageLegacy() { if (isIdle()) { - return messageQueueNext(); + return messageQueueNextLegacy(); } else { return null; } @@ -200,9 +313,26 @@ public class TestLooper { * Dispatch the next message in the queue * Asserts that there is a message in the queue */ - public synchronized void dispatchNext() { + public void dispatchNext() { + if (isAtLeastBaklava()) { + dispatchNextBaklava(); + } else { + dispatchNextLegacy(); + } + } + + private void dispatchNextBaklava() { + assertTrue(isIdle()); + Message msg = mTestLooperManager.poll(); + if (msg == null) { + return; + } + msg.getTarget().dispatchMessage(msg); + } + + private synchronized void dispatchNextLegacy() { assertTrue(isIdle()); - Message msg = messageQueueNext(); + Message msg = messageQueueNextLegacy(); if (msg == null) { return; } |