Merge "Allowing state logic to progress from terminal states on touch down." into main
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index e79b8f3..de39847 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -524,19 +524,12 @@
/**
* @hide
- * The IME is active and ready with views but set invisible.
- * This flag cannot be combined with {@link #IME_VISIBLE}.
- */
- public static final int IME_INVISIBLE = 0x4;
-
- /**
- * @hide
* The IME is visible, but not yet perceptible to the user (e.g. fading in)
* by {@link android.view.WindowInsetsController}.
*
* @see InputMethodManager#reportPerceptible
*/
- public static final int IME_VISIBLE_IMPERCEPTIBLE = 0x8;
+ public static final int IME_VISIBLE_IMPERCEPTIBLE = 0x4;
// Min and max values for back disposition.
private static final int BACK_DISPOSITION_MIN = BACK_DISPOSITION_DEFAULT;
@@ -3125,7 +3118,7 @@
mInShowWindow = true;
final int previousImeWindowStatus =
(mDecorViewVisible ? IME_ACTIVE : 0) | (isInputViewShown()
- ? (!mWindowVisible ? IME_INVISIBLE : IME_VISIBLE) : 0);
+ ? (!mWindowVisible ? -1 : IME_VISIBLE) : 0);
startViews(prepareWindow(showInput));
final int nextImeWindowStatus = mapToImeWindowStatus();
if (previousImeWindowStatus != nextImeWindowStatus) {
diff --git a/core/java/android/widget/Chronometer.java b/core/java/android/widget/Chronometer.java
index 0b67cad..9931aea 100644
--- a/core/java/android/widget/Chronometer.java
+++ b/core/java/android/widget/Chronometer.java
@@ -328,7 +328,7 @@
if (running) {
updateText(SystemClock.elapsedRealtime());
dispatchChronometerTick();
- postDelayed(mTickRunnable, 1000);
+ postTickOnNextSecond();
} else {
removeCallbacks(mTickRunnable);
}
@@ -342,11 +342,17 @@
if (mRunning) {
updateText(SystemClock.elapsedRealtime());
dispatchChronometerTick();
- postDelayed(mTickRunnable, 1000);
+ postTickOnNextSecond();
}
}
};
+ private void postTickOnNextSecond() {
+ long nowMillis = SystemClock.elapsedRealtime();
+ int millis = (int) ((nowMillis - mBase) % 1000);
+ postDelayed(mTickRunnable, 1000 - millis);
+ }
+
void dispatchChronometerTick() {
if (mOnChronometerTickListener != null) {
mOnChronometerTickListener.onChronometerTick(this);
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 2daf0fd..921363c 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -108,7 +108,6 @@
* @param backDisposition disposition flags
* @see android.inputmethodservice.InputMethodService#IME_ACTIVE
* @see android.inputmethodservice.InputMethodService#IME_VISIBLE
- * @see android.inputmethodservice.InputMethodService#IME_INVISIBLE
* @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_DEFAULT
* @see android.inputmethodservice.InputMethodService#BACK_DISPOSITION_ADJUST_NOTHING
*/
diff --git a/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java
new file mode 100644
index 0000000..3dab2e3
--- /dev/null
+++ b/core/java/com/android/internal/protolog/ProtoLogCommandHandler.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.ShellCommand;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class ProtoLogCommandHandler extends ShellCommand {
+ @NonNull
+ private final ProtoLogService mProtoLogService;
+ @Nullable
+ private final PrintWriter mPrintWriter;
+
+ public ProtoLogCommandHandler(@NonNull ProtoLogService protoLogService) {
+ this(protoLogService, null);
+ }
+
+ @VisibleForTesting
+ public ProtoLogCommandHandler(
+ @NonNull ProtoLogService protoLogService, @Nullable PrintWriter printWriter) {
+ this.mProtoLogService = protoLogService;
+ this.mPrintWriter = printWriter;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ onHelp();
+ return 0;
+ }
+
+ return switch (cmd) {
+ case "groups" -> handleGroupsCommands(getNextArg());
+ case "logcat" -> handleLogcatCommands(getNextArg());
+ default -> handleDefaultCommands(cmd);
+ };
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("ProtoLog commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println();
+ pw.println(" groups (list | status)");
+ pw.println(" list - lists all ProtoLog groups registered with ProtoLog service");
+ pw.println(" status <group> - print the status of a ProtoLog group");
+ pw.println();
+ pw.println(" logcat (enable | disable) <group>");
+ pw.println(" enable or disable ProtoLog to logcat");
+ pw.println();
+ }
+
+ @NonNull
+ @Override
+ public PrintWriter getOutPrintWriter() {
+ if (mPrintWriter != null) {
+ return mPrintWriter;
+ }
+
+ return super.getOutPrintWriter();
+ }
+
+ private int handleGroupsCommands(@Nullable String cmd) {
+ PrintWriter pw = getOutPrintWriter();
+
+ if (cmd == null) {
+ pw.println("Incomplete command. Use 'cmd protolog help' for guidance.");
+ return 0;
+ }
+
+ switch (cmd) {
+ case "list": {
+ final String[] availableGroups = mProtoLogService.getGroups();
+ if (availableGroups.length == 0) {
+ pw.println("No ProtoLog groups registered with ProtoLog service.");
+ return 0;
+ }
+
+ pw.println("ProtoLog groups registered with service:");
+ for (String group : availableGroups) {
+ pw.println("- " + group);
+ }
+
+ return 0;
+ }
+ case "status": {
+ final String group = getNextArg();
+
+ if (group == null) {
+ pw.println("Incomplete command. Use 'cmd protolog help' for guidance.");
+ return 0;
+ }
+
+ pw.println("ProtoLog group " + group + "'s status:");
+
+ if (!Set.of(mProtoLogService.getGroups()).contains(group)) {
+ pw.println("UNREGISTERED");
+ return 0;
+ }
+
+ pw.println("LOG_TO_LOGCAT = " + mProtoLogService.isLoggingToLogcat(group));
+ return 0;
+ }
+ default: {
+ pw.println("Unknown command: " + cmd);
+ return -1;
+ }
+ }
+ }
+
+ private int handleLogcatCommands(@Nullable String cmd) {
+ PrintWriter pw = getOutPrintWriter();
+
+ if (cmd == null || peekNextArg() == null) {
+ pw.println("Incomplete command. Use 'cmd protolog help' for guidance.");
+ return 0;
+ }
+
+ switch (cmd) {
+ case "enable" -> {
+ mProtoLogService.enableProtoLogToLogcat(processGroups());
+ return 0;
+ }
+ case "disable" -> {
+ mProtoLogService.disableProtoLogToLogcat(processGroups());
+ return 0;
+ }
+ default -> {
+ pw.println("Unknown command: " + cmd);
+ return -1;
+ }
+ }
+ }
+
+ @NonNull
+ private String[] processGroups() {
+ if (getRemainingArgsCount() == 0) {
+ return mProtoLogService.getGroups();
+ }
+
+ final List<String> groups = new ArrayList<>();
+ while (getRemainingArgsCount() > 0) {
+ groups.add(getNextArg());
+ }
+
+ return groups.toArray(new String[0]);
+ }
+}
diff --git a/core/java/com/android/internal/protolog/ProtoLogService.java b/core/java/com/android/internal/protolog/ProtoLogService.java
index 0b13a1a..2333a06 100644
--- a/core/java/com/android/internal/protolog/ProtoLogService.java
+++ b/core/java/com/android/internal/protolog/ProtoLogService.java
@@ -33,6 +33,8 @@
import android.annotation.SystemService;
import android.content.Context;
import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ShellCallback;
import android.os.SystemClock;
import android.tracing.perfetto.DataSourceParams;
import android.tracing.perfetto.InitArguments;
@@ -43,6 +45,7 @@
import com.android.internal.annotations.VisibleForTesting;
+import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -224,6 +227,14 @@
registerGroups(client, args.getGroups(), args.getGroupsDefaultLogcatStatus());
}
+ @Override
+ public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
+ @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback,
+ @NonNull ResultReceiver resultReceiver) throws RemoteException {
+ new ProtoLogCommandHandler(this)
+ .exec(this, in, out, err, args, callback, resultReceiver);
+ }
+
/**
* Get the list of groups clients have registered to the protolog service.
* @return The list of ProtoLog groups registered with this service.
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index c05ea3d..fc3c2f3 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -265,6 +265,17 @@
</intent-filter>
</activity>
+ <activity android:name="android.widget.ChronometerActivity"
+ android:label="ChronometerActivity"
+ android:screenOrientation="portrait"
+ android:exported="true"
+ android:theme="@android:style/Theme.Material.Light">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+ </intent-filter>
+ </activity>
+
<activity android:name="android.widget.DatePickerActivity"
android:label="DatePickerActivity"
android:screenOrientation="portrait"
diff --git a/core/tests/coretests/res/layout/chronometer_layout.xml b/core/tests/coretests/res/layout/chronometer_layout.xml
new file mode 100644
index 0000000..f209c41
--- /dev/null
+++ b/core/tests/coretests/res/layout/chronometer_layout.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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">
+ <Chronometer
+ android:id="@+id/chronometer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</FrameLayout>
diff --git a/core/tests/coretests/src/android/widget/ChronometerActivity.java b/core/tests/coretests/src/android/widget/ChronometerActivity.java
new file mode 100644
index 0000000..aaed430
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/ChronometerActivity.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+/**
+ * A minimal application for DatePickerFocusTest.
+ */
+public class ChronometerActivity extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.chronometer_layout);
+ }
+}
diff --git a/core/tests/coretests/src/android/widget/ChronometerTest.java b/core/tests/coretests/src/android/widget/ChronometerTest.java
new file mode 100644
index 0000000..3c73837
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/ChronometerTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.app.Activity;
+import android.test.ActivityInstrumentationTestCase2;
+
+import androidx.test.filters.LargeTest;
+
+import com.android.frameworks.coretests.R;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test {@link DatePicker} focus changes.
+ */
+@SuppressWarnings("deprecation")
+@LargeTest
+public class ChronometerTest extends ActivityInstrumentationTestCase2<ChronometerActivity> {
+
+ private Activity mActivity;
+ private Chronometer mChronometer;
+
+ public ChronometerTest() {
+ super(ChronometerActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mActivity = getActivity();
+ mChronometer = mActivity.findViewById(R.id.chronometer);
+ }
+
+ public void testChronometerTicksSequentially() throws Throwable {
+ final CountDownLatch latch = new CountDownLatch(5);
+ ArrayList<String> ticks = new ArrayList<>();
+ runOnUiThread(() -> {
+ mChronometer.setOnChronometerTickListener((chronometer) -> {
+ ticks.add(chronometer.getText().toString());
+ latch.countDown();
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ }
+ });
+ mChronometer.start();
+ });
+ assertTrue(latch.await(6, TimeUnit.SECONDS));
+ assertTrue(ticks.size() >= 5);
+ assertEquals("00:00", ticks.get(0));
+ assertEquals("00:01", ticks.get(1));
+ assertEquals("00:02", ticks.get(2));
+ assertEquals("00:03", ticks.get(3));
+ assertEquals("00:04", ticks.get(4));
+ }
+
+ private void runOnUiThread(Runnable runnable) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ mActivity.runOnUiThread(() -> {
+ runnable.run();
+ latch.countDown();
+ });
+ latch.await();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
index 97abda8..65f12cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskPosition.kt
@@ -116,10 +116,10 @@
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
fun Rect.getDesktopTaskPosition(bounds: Rect): DesktopTaskPosition {
return when {
- top == bounds.top && left == bounds.left -> TopLeft
- top == bounds.top && right == bounds.right -> TopRight
- bottom == bounds.bottom && left == bounds.left -> BottomLeft
- bottom == bounds.bottom && right == bounds.right -> BottomRight
+ top == bounds.top && left == bounds.left && bottom != bounds.bottom -> TopLeft
+ top == bounds.top && right == bounds.right && bottom != bounds.bottom -> TopRight
+ bottom == bounds.bottom && left == bounds.left && top != bounds.top -> BottomLeft
+ bottom == bounds.bottom && right == bounds.right && top != bounds.top -> BottomRight
else -> Center
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 78d41b2..f54b44b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -1070,6 +1070,11 @@
// In some launches home task is moved behind new task being launched. Make sure
// that's not the case for launches in desktop.
moveHomeTask(wct, toTop = false)
+ // Move existing minimized tasks behind Home
+ taskRepository.getFreeformTasksInZOrder(task.displayId)
+ .filter { taskId -> taskRepository.isMinimizedTask(taskId) }
+ .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) }
+ .forEach { taskInfo -> wct.reorder(taskInfo.token, /* onTop= */ false) }
// Desktop Mode is already showing and we're launching a new Task - we might need to
// minimize another Task.
val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 3b85859..7bb5449 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -731,6 +731,64 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+ fun addMoveToDesktopChanges_lastWindowSnapLeft_positionResetsToCenter() {
+ setUpLandscapeDisplay()
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ // Add freeform task with half display size snap bounds at left side.
+ setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom))
+
+ val task = setUpFullscreenTask()
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+ .isEqualTo(DesktopTaskPosition.Center)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+ fun addMoveToDesktopChanges_lastWindowSnapRight_positionResetsToCenter() {
+ setUpLandscapeDisplay()
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ // Add freeform task with half display size snap bounds at right side.
+ setUpFreeformTask(bounds = Rect(
+ stableBounds.right - 500, stableBounds.top, stableBounds.right, stableBounds.bottom))
+
+ val task = setUpFullscreenTask()
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+ .isEqualTo(DesktopTaskPosition.Center)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
+ fun addMoveToDesktopChanges_lastWindowMaximised_positionResetsToCenter() {
+ setUpLandscapeDisplay()
+ val stableBounds = Rect()
+ displayLayout.getStableBoundsForDesktopMode(stableBounds)
+
+ // Add maximised freeform task.
+ setUpFreeformTask(bounds = Rect(stableBounds))
+
+ val task = setUpFullscreenTask()
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDesktopChanges(wct, task)
+
+ val finalBounds = findBoundsChange(wct, task)
+ assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!))
+ .isEqualTo(DesktopTaskPosition.Center)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS)
fun addMoveToDesktopChanges_defaultToCenterIfFree() {
setUpLandscapeDisplay()
val stableBounds = Rect()
@@ -1349,13 +1407,36 @@
val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
freeformTasks.forEach { markTaskVisible(it) }
val fullscreenTask = createFullscreenTask()
+ val homeTask = setUpHomeTask(DEFAULT_DISPLAY)
val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
// Make sure we reorder the new task to top, and the back task to the bottom
- assertThat(wct!!.hierarchyOps.size).isEqualTo(2)
+ assertThat(wct!!.hierarchyOps.size).isEqualTo(3)
wct.assertReorderAt(0, fullscreenTask, toTop = true)
- wct.assertReorderAt(1, freeformTasks[0], toTop = false)
+ wct.assertReorderAt(1, homeTask, toTop = false)
+ wct.assertReorderAt(2, freeformTasks[0], toTop = false)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTaskToFreeform_alreadyBeyondLimit_existingAndNewTasksAreMinimized() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val minimizedTask = setUpFreeformTask()
+ taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId)
+ val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
+ freeformTasks.forEach { markTaskVisible(it) }
+ val homeTask = setUpHomeTask()
+ val fullscreenTask = createFullscreenTask()
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertThat(wct!!.hierarchyOps.size).isEqualTo(4)
+ wct.assertReorderAt(0, fullscreenTask, toTop = true)
+ // Make sure we reorder the home task to the bottom, and minimized tasks below the home task.
+ wct.assertReorderAt(1, homeTask, toTop = false)
+ wct.assertReorderAt(2, minimizedTask, toTop = false)
+ wct.assertReorderAt(3, freeformTasks[0], toTop = false)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
index f82a7b8..5dd6c22 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt
@@ -85,7 +85,9 @@
GestureEduModel(
signalCount = 2,
educationShownCount = 1,
- lastShortcutTriggeredTime = kosmos.fakeEduClock.instant()
+ lastShortcutTriggeredTime = kosmos.fakeEduClock.instant(),
+ lastEducationTime = kosmos.fakeEduClock.instant(),
+ usageSessionStartTime = kosmos.fakeEduClock.instant(),
)
underTest.updateGestureEduModel(BACK) { newModel }
val model by collectLastValue(underTest.readGestureEduModelFlow(BACK))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
index 1b4632a..6867089 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
@@ -22,9 +22,15 @@
import com.android.systemui.contextualeducation.GestureType
import com.android.systemui.contextualeducation.GestureType.BACK
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.education.data.model.GestureEduModel
+import com.android.systemui.education.data.repository.contextualEducationRepository
+import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.education.shared.model.EducationUiType
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -37,6 +43,7 @@
private val testScope = kosmos.testScope
private val contextualEduInteractor = kosmos.contextualEducationInteractor
private val underTest: KeyboardTouchpadEduInteractor = kosmos.keyboardTouchpadEduInteractor
+ private val eduClock = kosmos.fakeEduClock
@Before
fun setup() {
@@ -46,12 +53,32 @@
@Test
fun newEducationInfoOnMaxSignalCountReached() =
testScope.runTest {
- tryTriggeringEducation(BACK)
+ triggerMaxEducationSignals(BACK)
val model by collectLastValue(underTest.educationTriggered)
assertThat(model?.gestureType).isEqualTo(BACK)
}
@Test
+ fun newEducationToastOn1stEducation() =
+ testScope.runTest {
+ val model by collectLastValue(underTest.educationTriggered)
+ triggerMaxEducationSignals(BACK)
+ assertThat(model?.educationUiType).isEqualTo(EducationUiType.Toast)
+ }
+
+ @Test
+ @kotlinx.coroutines.ExperimentalCoroutinesApi
+ fun newEducationNotificationOn2ndEducation() =
+ testScope.runTest {
+ val model by collectLastValue(underTest.educationTriggered)
+ triggerMaxEducationSignals(BACK)
+ // runCurrent() to trigger 1st education
+ runCurrent()
+ triggerMaxEducationSignals(BACK)
+ assertThat(model?.educationUiType).isEqualTo(EducationUiType.Notification)
+ }
+
+ @Test
fun noEducationInfoBeforeMaxSignalCountReached() =
testScope.runTest {
contextualEduInteractor.incrementSignalCount(BACK)
@@ -64,11 +91,30 @@
testScope.runTest {
val model by collectLastValue(underTest.educationTriggered)
contextualEduInteractor.updateShortcutTriggerTime(BACK)
- tryTriggeringEducation(BACK)
+ triggerMaxEducationSignals(BACK)
assertThat(model).isNull()
}
- private suspend fun tryTriggeringEducation(gestureType: GestureType) {
+ @Test
+ fun startNewUsageSessionWhen2ndSignalReceivedAfterSessionDeadline() =
+ testScope.runTest {
+ val model by
+ collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
+ contextualEduInteractor.incrementSignalCount(BACK)
+ eduClock.offset(KeyboardTouchpadEduInteractor.usageSessionDuration.plus(1.seconds))
+ val secondSignalReceivedTime = eduClock.instant()
+ contextualEduInteractor.incrementSignalCount(BACK)
+
+ assertThat(model)
+ .isEqualTo(
+ GestureEduModel(
+ signalCount = 1,
+ usageSessionStartTime = secondSignalReceivedTime
+ )
+ )
+ }
+
+ private suspend fun triggerMaxEducationSignals(gestureType: GestureType) {
// Increment max number of signal to try triggering education
for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
contextualEduInteractor.incrementSignalCount(gestureType)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
index 46b370f..d1f908d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
@@ -16,27 +16,16 @@
package com.android.systemui.lifecycle
-import android.view.View
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.Assert
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.awaitCancellation
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.argumentCaptor
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.stub
-import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -121,45 +110,4 @@
assertThat(isActive).isFalse()
}
-
- @Test
- fun viewModel_viewBinder() = runTest {
- Assert.setTestThread(Thread.currentThread())
-
- val view: View = mock { on { isAttachedToWindow } doReturn false }
- val viewModel = FakeViewModel()
- backgroundScope.launch {
- view.viewModel(
- minWindowLifecycleState = WindowLifecycleState.ATTACHED,
- factory = { viewModel },
- ) {
- awaitCancellation()
- }
- }
- runCurrent()
-
- assertThat(viewModel.isActivated).isFalse()
-
- view.stub { on { isAttachedToWindow } doReturn true }
- argumentCaptor<View.OnAttachStateChangeListener>()
- .apply { verify(view).addOnAttachStateChangeListener(capture()) }
- .allValues
- .forEach { it.onViewAttachedToWindow(view) }
- runCurrent()
-
- assertThat(viewModel.isActivated).isTrue()
- }
-}
-
-private class FakeViewModel : SysUiViewModel() {
- var isActivated = false
-
- override suspend fun onActivated() {
- isActivated = true
- try {
- awaitCancellation()
- } finally {
- isActivated = false
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt
index 9f6cb4d..a171f87 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt
@@ -26,4 +26,6 @@
val signalCount: Int = 0,
val educationShownCount: Int = 0,
val lastShortcutTriggeredTime: Instant? = null,
+ val usageSessionStartTime: Instant? = null,
+ val lastEducationTime: Instant? = null,
)
diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
index 22ba4ad..7c3d6338 100644
--- a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt
@@ -73,6 +73,8 @@
const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT"
const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN"
const val LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX = "_LAST_SHORTCUT_TRIGGERED_TIME"
+ const val USAGE_SESSION_START_TIME_SUFFIX = "_USAGE_SESSION_START_TIME"
+ const val LAST_EDUCATION_TIME_SUFFIX = "_LAST_EDUCATION_TIME"
const val DATASTORE_DIR = "education/USER%s_ContextualEducation"
}
@@ -113,6 +115,14 @@
preferences[getLastShortcutTriggeredTimeKey(gestureType)]?.let {
Instant.ofEpochSecond(it)
},
+ usageSessionStartTime =
+ preferences[getUsageSessionStartTimeKey(gestureType)]?.let {
+ Instant.ofEpochSecond(it)
+ },
+ lastEducationTime =
+ preferences[getLastEducationTimeKey(gestureType)]?.let {
+ Instant.ofEpochSecond(it)
+ },
)
}
@@ -125,11 +135,21 @@
val updatedModel = transform(currentModel)
preferences[getSignalCountKey(gestureType)] = updatedModel.signalCount
preferences[getEducationShownCountKey(gestureType)] = updatedModel.educationShownCount
- updateTimeByInstant(
+ setInstant(
preferences,
updatedModel.lastShortcutTriggeredTime,
getLastShortcutTriggeredTimeKey(gestureType)
)
+ setInstant(
+ preferences,
+ updatedModel.usageSessionStartTime,
+ getUsageSessionStartTimeKey(gestureType)
+ )
+ setInstant(
+ preferences,
+ updatedModel.lastEducationTime,
+ getLastEducationTimeKey(gestureType)
+ )
}
}
@@ -142,7 +162,13 @@
private fun getLastShortcutTriggeredTimeKey(gestureType: GestureType): Preferences.Key<Long> =
longPreferencesKey(gestureType.name + LAST_SHORTCUT_TRIGGERED_TIME_SUFFIX)
- private fun updateTimeByInstant(
+ private fun getUsageSessionStartTimeKey(gestureType: GestureType): Preferences.Key<Long> =
+ longPreferencesKey(gestureType.name + USAGE_SESSION_START_TIME_SUFFIX)
+
+ private fun getLastEducationTimeKey(gestureType: GestureType): Preferences.Key<Long> =
+ longPreferencesKey(gestureType.name + LAST_EDUCATION_TIME_SUFFIX)
+
+ private fun setInstant(
preferences: MutablePreferences,
instant: Instant?,
key: Preferences.Key<Long>
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
index 5ec1006..db5c386 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/ContextualEducationInteractor.kt
@@ -68,7 +68,13 @@
}
suspend fun incrementSignalCount(gestureType: GestureType) {
- repository.updateGestureEduModel(gestureType) { it.copy(signalCount = it.signalCount + 1) }
+ repository.updateGestureEduModel(gestureType) {
+ it.copy(
+ signalCount = it.signalCount + 1,
+ usageSessionStartTime =
+ if (it.signalCount == 0) clock.instant() else it.usageSessionStartTime
+ )
+ }
}
suspend fun updateShortcutTriggerTime(gestureType: GestureType) {
@@ -76,4 +82,22 @@
it.copy(lastShortcutTriggeredTime = clock.instant())
}
}
+
+ suspend fun updateOnEduTriggered(gestureType: GestureType) {
+ repository.updateGestureEduModel(gestureType) {
+ it.copy(
+ // Reset signal counter and usageSessionStartTime after edu triggered
+ signalCount = 0,
+ lastEducationTime = clock.instant(),
+ educationShownCount = it.educationShownCount + 1,
+ usageSessionStartTime = null
+ )
+ }
+ }
+
+ suspend fun startNewUsageSession(gestureType: GestureType) {
+ repository.updateGestureEduModel(gestureType) {
+ it.copy(usageSessionStartTime = clock.instant(), signalCount = 1)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
index 9016c73..3a3fb8c 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractor.kt
@@ -17,17 +17,19 @@
package com.android.systemui.education.domain.interactor
import com.android.systemui.CoreStartable
+import com.android.systemui.contextualeducation.GestureType.BACK
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.education.dagger.ContextualEducationModule.EduClock
import com.android.systemui.education.data.model.GestureEduModel
import com.android.systemui.education.shared.model.EducationInfo
import com.android.systemui.education.shared.model.EducationUiType
+import java.time.Clock
import javax.inject.Inject
+import kotlin.time.Duration.Companion.hours
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
/** Allow listening to new contextual education triggered */
@@ -36,11 +38,13 @@
@Inject
constructor(
@Background private val backgroundScope: CoroutineScope,
- private val contextualEducationInteractor: ContextualEducationInteractor
+ private val contextualEducationInteractor: ContextualEducationInteractor,
+ @EduClock private val clock: Clock,
) : CoreStartable {
companion object {
const val MAX_SIGNAL_COUNT: Int = 2
+ val usageSessionDuration = 72.hours
}
private val _educationTriggered = MutableStateFlow<EducationInfo?>(null)
@@ -48,25 +52,30 @@
override fun start() {
backgroundScope.launch {
- contextualEducationInteractor.backGestureModelFlow
- .mapNotNull { getEduType(it) }
- .collect { _educationTriggered.value = EducationInfo(BACK, it) }
- }
- }
-
- private fun getEduType(model: GestureEduModel): EducationUiType? {
- if (isEducationNeeded(model)) {
- return EducationUiType.Toast
- } else {
- return null
+ contextualEducationInteractor.backGestureModelFlow.collect {
+ if (isUsageSessionExpired(it)) {
+ contextualEducationInteractor.startNewUsageSession(BACK)
+ } else if (isEducationNeeded(it)) {
+ _educationTriggered.value = EducationInfo(BACK, getEduType(it))
+ contextualEducationInteractor.updateOnEduTriggered(BACK)
+ }
+ }
}
}
private fun isEducationNeeded(model: GestureEduModel): Boolean {
// Todo: b/354884305 - add complete education logic to show education in correct scenarios
- val shortcutWasTriggered = model.lastShortcutTriggeredTime == null
+ val noShortcutTriggered = model.lastShortcutTriggeredTime == null
val signalCountReached = model.signalCount >= MAX_SIGNAL_COUNT
-
- return shortcutWasTriggered && signalCountReached
+ return noShortcutTriggered && signalCountReached
}
+
+ private fun isUsageSessionExpired(model: GestureEduModel): Boolean {
+ return model.usageSessionStartTime
+ ?.plusSeconds(usageSessionDuration.inWholeSeconds)
+ ?.isBefore(clock.instant()) ?: false
+ }
+
+ private fun getEduType(model: GestureEduModel) =
+ if (model.educationShownCount > 0) EducationUiType.Notification else EducationUiType.Toast
}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index c2b5d98..661da6d 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -227,33 +227,13 @@
}
/**
- * Runs the given [block] in a new coroutine when `this` [View]'s Window's [WindowLifecycleState] is
- * at least at [state] (or immediately after calling this function if the window is already at least
- * at [state]), automatically canceling the work when the window is no longer at least at that
- * state.
- *
- * [block] may be run multiple times, running once per every time this` [View]'s Window's
- * [WindowLifecycleState] becomes at least at [state].
- */
-suspend fun View.repeatOnWindowLifecycle(
- state: WindowLifecycleState,
- block: suspend CoroutineScope.() -> Unit,
-): Nothing {
- when (state) {
- WindowLifecycleState.ATTACHED -> repeatWhenAttachedToWindow(block)
- WindowLifecycleState.VISIBLE -> repeatWhenWindowIsVisible(block)
- WindowLifecycleState.FOCUSED -> repeatWhenWindowHasFocus(block)
- }
-}
-
-/**
* Runs the given [block] every time the [View] becomes attached (or immediately after calling this
* function, if the view was already attached), automatically canceling the work when the view
* becomes detached.
*
* Only use from the main thread.
*
- * [block] may be run multiple times, running once per every time the view is attached.
+ * The [block] may be run multiple times, running once per every time the view is attached.
*/
@MainThread
suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> Unit): Nothing {
@@ -269,7 +249,7 @@
*
* Only use from the main thread.
*
- * [block] may be run multiple times, running once per every time the window becomes visible.
+ * The [block] may be run multiple times, running once per every time the window becomes visible.
*/
@MainThread
suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> Unit): Nothing {
@@ -285,7 +265,7 @@
*
* Only use from the main thread.
*
- * [block] may be run multiple times, running once per every time the window is focused.
+ * The [block] may be run multiple times, running once per every time the window is focused.
*/
@MainThread
suspend fun View.repeatWhenWindowHasFocus(block: suspend CoroutineScope.() -> Unit): Nothing {
@@ -294,21 +274,6 @@
awaitCancellation() // satisfies return type of Nothing
}
-/** Lifecycle states for a [View]'s interaction with a [android.view.Window]. */
-enum class WindowLifecycleState {
- /** Indicates that the [View] is attached to a [android.view.Window]. */
- ATTACHED,
- /**
- * Indicates that the [View] is attached to a [android.view.Window], and the window is visible.
- */
- VISIBLE,
- /**
- * Indicates that the [View] is attached to a [android.view.Window], and the window is visible
- * and focused.
- */
- FOCUSED
-}
-
private val View.isAttached
get() = conflatedCallbackFlow {
val onAttachListener =
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
index 7731481..0af5fea 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
@@ -16,10 +16,9 @@
package com.android.systemui.lifecycle
-import android.view.View
import androidx.compose.runtime.Composable
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
/** Base class for all System UI view-models. */
abstract class SysUiViewModel : SafeActivatable() {
@@ -38,20 +37,8 @@
fun <T : SysUiViewModel> rememberViewModel(
key: Any = Unit,
factory: () -> T,
-): T = rememberActivated(key, factory)
-
-/**
- * Invokes [block] in a new coroutine with a new [SysUiViewModel] that is automatically activated
- * whenever `this` [View]'s Window's [WindowLifecycleState] is at least at
- * [minWindowLifecycleState], and is automatically canceled once that is no longer the case.
- */
-suspend fun <T : SysUiViewModel> View.viewModel(
- minWindowLifecycleState: WindowLifecycleState,
- factory: () -> T,
- block: suspend CoroutineScope.(T) -> Unit,
-): Nothing =
- repeatOnWindowLifecycle(minWindowLifecycleState) {
- val instance = factory()
- launch { instance.activate() }
- block(instance)
- }
+): T {
+ val instance = remember(key) { factory() }
+ LaunchedEffect(instance) { instance.activate() }
+ return instance
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 540d4c4..7b802a2 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -17,7 +17,6 @@
package com.android.systemui.screenshot;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
import static com.android.systemui.Flags.screenshotSaveImageExporter;
import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
@@ -31,7 +30,6 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
@@ -52,17 +50,12 @@
import android.util.Log;
import android.view.Display;
import android.view.ScrollCaptureResponse;
-import android.view.View;
-import android.view.ViewGroup;
import android.view.ViewRootImpl;
-import android.view.ViewTreeObserver;
-import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Toast;
import android.window.WindowContext;
import com.android.internal.logging.UiEventLogger;
-import com.android.internal.policy.PhoneWindow;
import com.android.settingslib.applications.InterestingConfigChanges;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.broadcast.BroadcastSender;
@@ -115,11 +108,9 @@
private final BroadcastDispatcher mBroadcastDispatcher;
private final ScreenshotActionsController mActionsController;
- private final WindowManager mWindowManager;
- private final WindowManager.LayoutParams mWindowLayoutParams;
@Nullable
private final ScreenshotSoundController mScreenshotSoundController;
- private final PhoneWindow mWindow;
+ private final ScreenshotWindow mWindow;
private final Display mDisplay;
private final ScrollCaptureExecutor mScrollCaptureExecutor;
private final ScreenshotNotificationSmartActionsProvider
@@ -135,8 +126,6 @@
private Bitmap mScreenBitmap;
private SaveImageInBackgroundTask mSaveInBgTask;
private boolean mScreenshotTakenInPortrait;
- private boolean mAttachRequested;
- private boolean mDetachRequested;
private Animator mScreenshotAnimation;
private RequestCallback mCurrentRequestCallback;
private String mPackageName = "";
@@ -155,7 +144,7 @@
@AssistedInject
ScreenshotController(
Context context,
- WindowManager windowManager,
+ ScreenshotWindow.Factory screenshotWindowFactory,
FeatureFlags flags,
ScreenshotShelfViewProxy.Factory viewProxyFactory,
ScreenshotSmartActions screenshotSmartActions,
@@ -195,9 +184,8 @@
mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);
mDisplay = display;
- mWindowManager = windowManager;
- final Context displayContext = context.createDisplayContext(display);
- mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
+ mWindow = screenshotWindowFactory.create(mDisplay);
+ mContext = mWindow.getContext();
mFlags = flags;
mUserManager = userManager;
mMessageContainerController = messageContainerController;
@@ -213,17 +201,10 @@
mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT);
});
- // Setup the window that we are going to use
- mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
- mWindowLayoutParams.setTitle("ScreenshotAnimation");
-
- mWindow = FloatingWindowUtil.getFloatingWindow(mContext);
- mWindow.setWindowManager(mWindowManager, null, null);
-
mConfigChanges.applyNewConfig(context.getResources());
reloadAssets();
- mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy,
+ mActionExecutor = actionExecutorFactory.create(mWindow.getWindow(), mViewProxy,
() -> {
finishDismiss();
return Unit.INSTANCE;
@@ -318,12 +299,12 @@
}
// The window is focusable by default
- setWindowFocusable(true);
+ mWindow.setFocusable(true);
mViewProxy.requestFocus();
enqueueScrollCaptureRequest(requestId, screenshot.getUserHandle());
- attachWindow();
+ mWindow.attachWindow();
boolean showFlash;
if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) {
@@ -347,13 +328,10 @@
mViewProxy.setScreenshot(screenshot);
- // ignore system bar insets for the purpose of window layout
- mWindow.getDecorView().setOnApplyWindowInsetsListener(
- (v, insets) -> WindowInsets.CONSUMED);
}
void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) {
- withWindowAttached(() -> {
+ mWindow.whenWindowAttached(() -> {
mAnnouncementResolver.getScreenshotAnnouncement(
screenshot.getUserHandle().getIdentifier(),
announcement -> {
@@ -444,7 +422,7 @@
@Override
public void onTouchOutside() {
// TODO(159460485): Remove this when focus is handled properly in the system
- setWindowFocusable(false);
+ mWindow.setFocusable(false);
}
});
@@ -457,9 +435,9 @@
private void enqueueScrollCaptureRequest(UUID requestId, UserHandle owner) {
// Wait until this window is attached to request because it is
// the reference used to locate the target window (below).
- withWindowAttached(() -> {
+ mWindow.whenWindowAttached(() -> {
requestScrollCapture(requestId, owner);
- mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
+ mWindow.setActivityConfigCallback(
new ViewRootImpl.ActivityConfigCallback() {
@Override
public void onConfigurationChanged(Configuration overrideConfig,
@@ -472,8 +450,7 @@
// to set up in the new orientation.
mScreenshotHandler.postDelayed(
() -> requestScrollCapture(requestId, owner), 150);
- mViewProxy.updateInsets(
- mWindowManager.getCurrentWindowMetrics().getWindowInsets());
+ mViewProxy.updateInsets(mWindow.getWindowInsets());
// Screenshot animation calculations won't be valid anymore,
// so just end
if (mScreenshotAnimation != null
@@ -489,7 +466,7 @@
private void requestScrollCapture(UUID requestId, UserHandle owner) {
mScrollCaptureExecutor.requestScrollCapture(
mDisplay.getDisplayId(),
- mWindow.getDecorView().getWindowToken(),
+ mWindow.getWindowToken(),
(response) -> {
mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
0, response.getPackageName());
@@ -528,61 +505,9 @@
mViewProxy::startLongScreenshotTransition);
}
- private void withWindowAttached(Runnable action) {
- View decorView = mWindow.getDecorView();
- if (decorView.isAttachedToWindow()) {
- action.run();
- } else {
- decorView.getViewTreeObserver().addOnWindowAttachListener(
- new ViewTreeObserver.OnWindowAttachListener() {
- @Override
- public void onWindowAttached() {
- mAttachRequested = false;
- decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
- action.run();
- }
-
- @Override
- public void onWindowDetached() {
- }
- });
-
- }
- }
-
- @MainThread
- private void attachWindow() {
- View decorView = mWindow.getDecorView();
- if (decorView.isAttachedToWindow() || mAttachRequested) {
- return;
- }
- if (DEBUG_WINDOW) {
- Log.d(TAG, "attachWindow");
- }
- mAttachRequested = true;
- mWindowManager.addView(decorView, mWindowLayoutParams);
- decorView.requestApplyInsets();
-
- ViewGroup layout = decorView.requireViewById(android.R.id.content);
- layout.setClipChildren(false);
- layout.setClipToPadding(false);
- }
-
@Override
public void removeWindow() {
- final View decorView = mWindow.peekDecorView();
- if (decorView != null && decorView.isAttachedToWindow()) {
- if (DEBUG_WINDOW) {
- Log.d(TAG, "Removing screenshot window");
- }
- mWindowManager.removeViewImmediate(decorView);
- mDetachRequested = false;
- }
- if (mAttachRequested && !mDetachRequested) {
- mDetachRequested = true;
- withWindowAttached(this::removeWindow);
- }
-
+ mWindow.removeWindow();
mViewProxy.stopInputListening();
}
@@ -759,33 +684,6 @@
.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
}
- /**
- * Updates the window focusability. If the window is already showing, then it updates the
- * window immediately, otherwise the layout params will be applied when the window is next
- * shown.
- */
- private void setWindowFocusable(boolean focusable) {
- if (DEBUG_WINDOW) {
- Log.d(TAG, "setWindowFocusable: " + focusable);
- }
- int flags = mWindowLayoutParams.flags;
- if (focusable) {
- mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- } else {
- mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
- }
- if (mWindowLayoutParams.flags == flags) {
- if (DEBUG_WINDOW) {
- Log.d(TAG, "setWindowFocusable: skipping, already " + focusable);
- }
- return;
- }
- final View decorView = mWindow.peekDecorView();
- if (decorView != null && decorView.isAttachedToWindow()) {
- mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
- }
- }
-
private Rect getFullScreenRect() {
DisplayMetrics displayMetrics = new DisplayMetrics();
mDisplay.getRealMetrics(displayMetrics);
@@ -826,6 +724,6 @@
*
* @param display display to capture
*/
- LegacyScreenshotController create(Display display);
+ ScreenshotController create(Display display);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt
new file mode 100644
index 0000000..644e12c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotWindow.kt
@@ -0,0 +1,194 @@
+/*
+ * 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.systemui.screenshot
+
+import android.R
+import android.annotation.MainThread
+import android.content.Context
+import android.graphics.PixelFormat
+import android.os.IBinder
+import android.util.Log
+import android.view.Display
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewRootImpl
+import android.view.ViewTreeObserver.OnWindowAttachListener
+import android.view.Window
+import android.view.WindowInsets
+import android.view.WindowManager
+import android.window.WindowContext
+import com.android.internal.policy.PhoneWindow
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Creates and manages the window in which the screenshot UI is displayed. */
+class ScreenshotWindow
+@AssistedInject
+constructor(
+ private val windowManager: WindowManager,
+ private val context: Context,
+ @Assisted private val display: Display,
+) {
+
+ val window: PhoneWindow =
+ PhoneWindow(
+ context
+ .createDisplayContext(display)
+ .createWindowContext(WindowManager.LayoutParams.TYPE_SCREENSHOT, null)
+ )
+ private val params =
+ WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ 0, /* xpos */
+ 0, /* ypos */
+ WindowManager.LayoutParams.TYPE_SCREENSHOT,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN or
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
+ WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
+ WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
+ WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
+ PixelFormat.TRANSLUCENT
+ )
+ .apply {
+ layoutInDisplayCutoutMode =
+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ setFitInsetsTypes(0)
+ // This is needed to let touches pass through outside the touchable areas
+ privateFlags =
+ privateFlags or WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+ title = "ScreenshotUI"
+ }
+ private var attachRequested: Boolean = false
+ private var detachRequested: Boolean = false
+
+ init {
+ window.requestFeature(Window.FEATURE_NO_TITLE)
+ window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
+ window.setBackgroundDrawableResource(R.color.transparent)
+ window.setWindowManager(windowManager, null, null)
+ }
+
+ @MainThread
+ fun attachWindow() {
+ val decorView: View = window.getDecorView()
+ if (decorView.isAttachedToWindow || attachRequested) {
+ return
+ }
+ if (LogConfig.DEBUG_WINDOW) {
+ Log.d(TAG, "attachWindow")
+ }
+ attachRequested = true
+ windowManager.addView(decorView, params)
+
+ decorView.requestApplyInsets()
+ decorView.requireViewById<ViewGroup>(R.id.content).apply {
+ clipChildren = false
+ clipToPadding = false
+ // ignore system bar insets for the purpose of window layout
+ setOnApplyWindowInsetsListener { _, _ -> WindowInsets.CONSUMED }
+ }
+ }
+
+ fun whenWindowAttached(action: Runnable) {
+ val decorView: View = window.getDecorView()
+ if (decorView.isAttachedToWindow) {
+ action.run()
+ } else {
+ decorView
+ .getViewTreeObserver()
+ .addOnWindowAttachListener(
+ object : OnWindowAttachListener {
+ override fun onWindowAttached() {
+ attachRequested = false
+ decorView.getViewTreeObserver().removeOnWindowAttachListener(this)
+ action.run()
+ }
+
+ override fun onWindowDetached() {}
+ }
+ )
+ }
+ }
+
+ fun removeWindow() {
+ val decorView: View? = window.peekDecorView()
+ if (decorView != null && decorView.isAttachedToWindow) {
+ if (LogConfig.DEBUG_WINDOW) {
+ Log.d(TAG, "Removing screenshot window")
+ }
+ windowManager.removeViewImmediate(decorView)
+ detachRequested = false
+ }
+ if (attachRequested && !detachRequested) {
+ detachRequested = true
+ whenWindowAttached { removeWindow() }
+ }
+ }
+
+ /**
+ * Updates the window focusability. If the window is already showing, then it updates the window
+ * immediately, otherwise the layout params will be applied when the window is next shown.
+ */
+ fun setFocusable(focusable: Boolean) {
+ if (LogConfig.DEBUG_WINDOW) {
+ Log.d(TAG, "setWindowFocusable: $focusable")
+ }
+ val flags: Int = params.flags
+ if (focusable) {
+ params.flags = params.flags and WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv()
+ } else {
+ params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ }
+ if (params.flags == flags) {
+ if (LogConfig.DEBUG_WINDOW) {
+ Log.d(TAG, "setWindowFocusable: skipping, already $focusable")
+ }
+ return
+ }
+ window.peekDecorView()?.also {
+ if (it.isAttachedToWindow) {
+ windowManager.updateViewLayout(it, params)
+ }
+ }
+ }
+
+ fun getContext(): WindowContext = window.context as WindowContext
+
+ fun getWindowToken(): IBinder = window.decorView.windowToken
+
+ fun getWindowInsets(): WindowInsets = windowManager.currentWindowMetrics.windowInsets
+
+ fun setContentView(view: View) {
+ window.setContentView(view)
+ }
+
+ fun setActivityConfigCallback(callback: ViewRootImpl.ActivityConfigCallback) {
+ window.peekDecorView().viewRootImpl.setActivityConfigCallback(callback)
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(display: Display): ScreenshotWindow
+ }
+
+ companion object {
+ private const val TAG = "ScreenshotWindow"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index cea97d6..50be6dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -19,7 +19,6 @@
import static android.app.StatusBarManager.DISABLE2_NONE;
import static android.app.StatusBarManager.DISABLE_NONE;
import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
-import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
import static android.view.Display.INVALID_DISPLAY;
import android.annotation.Nullable;
@@ -1219,7 +1218,7 @@
&& mLastUpdatedImeDisplayId != INVALID_DISPLAY) {
// Set previous NavBar's IME window status as invisible when IME
// window switched to another display for single-session IME case.
- sendImeInvisibleStatusForPrevNavBar();
+ sendImeNotVisibleStatusForPrevNavBar();
}
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).setImeWindowStatus(displayId, vis, backDisposition, showImeSwitcher);
@@ -1227,9 +1226,9 @@
mLastUpdatedImeDisplayId = displayId;
}
- private void sendImeInvisibleStatusForPrevNavBar() {
+ private void sendImeNotVisibleStatusForPrevNavBar() {
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).setImeWindowStatus(mLastUpdatedImeDisplayId, IME_INVISIBLE,
+ mCallbacks.get(i).setImeWindowStatus(mLastUpdatedImeDisplayId, 0 /* vis */,
BACK_DISPOSITION_DEFAULT, false /* showImeSwitcher */);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index a30b877..fd08e89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -17,14 +17,14 @@
package com.android.systemui.statusbar.notification.stack.ui.viewbinder
import android.util.Log
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.common.ui.view.onLayoutChanged
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
-import com.android.systemui.lifecycle.WindowLifecycleState
import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.lifecycle.viewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationScrollViewModel
@@ -33,6 +33,7 @@
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@@ -45,7 +46,7 @@
dumpManager: DumpManager,
@Main private val mainImmediateDispatcher: CoroutineDispatcher,
private val view: NotificationScrollView,
- private val viewModelFactory: NotificationScrollViewModel.Factory,
+ private val viewModel: NotificationScrollViewModel,
private val configuration: ConfigurationState,
) : FlowDumperImpl(dumpManager) {
@@ -60,42 +61,38 @@
}
fun bindWhileAttached(): DisposableHandle {
- return view.asView().repeatWhenAttached(mainImmediateDispatcher) { bind() }
+ return view.asView().repeatWhenAttached(mainImmediateDispatcher) {
+ repeatOnLifecycle(Lifecycle.State.CREATED) { bind() }
+ }
}
- suspend fun bind(): Nothing =
- view.asView().viewModel(
- minWindowLifecycleState = WindowLifecycleState.ATTACHED,
- factory = viewModelFactory::create,
- ) { viewModel ->
- launchAndDispose {
- updateViewPosition()
- view.asView().onLayoutChanged { updateViewPosition() }
- }
+ suspend fun bind() = coroutineScope {
+ launchAndDispose {
+ updateViewPosition()
+ view.asView().onLayoutChanged { updateViewPosition() }
+ }
- launch {
- viewModel
- .shadeScrimShape(cornerRadius = scrimRadius, viewLeftOffset = viewLeftOffset)
- .collect { view.setScrimClippingShape(it) }
- }
+ launch {
+ viewModel
+ .shadeScrimShape(cornerRadius = scrimRadius, viewLeftOffset = viewLeftOffset)
+ .collect { view.setScrimClippingShape(it) }
+ }
- launch { viewModel.maxAlpha.collect { view.setMaxAlpha(it) } }
- launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } }
- launch {
- viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) }
- }
- launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
- launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
+ launch { viewModel.maxAlpha.collect { view.setMaxAlpha(it) } }
+ launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } }
+ launch { viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) } }
+ launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
+ launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
- launchAndDispose {
- view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
- view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer)
- DisposableHandle {
- view.setSyntheticScrollConsumer(null)
- view.setCurrentGestureOverscrollConsumer(null)
- }
+ launchAndDispose {
+ view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
+ view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer)
+ DisposableHandle {
+ view.setSyntheticScrollConsumer(null)
+ view.setCurrentGestureOverscrollConsumer(null)
}
}
+ }
/** flow of the scrim clipping radius */
private val scrimRadius: Flow<Int>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 4281025..2ba79a8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -19,9 +19,9 @@
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.SceneFamilies
@@ -33,11 +33,9 @@
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
-import com.android.systemui.util.kotlin.FlowDumper
import com.android.systemui.util.kotlin.FlowDumperImpl
import dagger.Lazy
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
+import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -45,8 +43,9 @@
import kotlinx.coroutines.flow.map
/** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
+@SysUISingleton
class NotificationScrollViewModel
-@AssistedInject
+@Inject
constructor(
dumpManager: DumpManager,
stackAppearanceInteractor: NotificationStackAppearanceInteractor,
@@ -55,9 +54,7 @@
// TODO(b/336364825) Remove Lazy when SceneContainerFlag is released -
// while the flag is off, creating this object too early results in a crash
keyguardInteractor: Lazy<KeyguardInteractor>,
-) : FlowDumper by FlowDumperImpl(dumpManager, "NotificationScrollViewModel"),
- SysUiViewModel() {
-
+) : FlowDumperImpl(dumpManager) {
/**
* The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
* from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
@@ -189,9 +186,4 @@
keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing")
}
}
-
- @AssistedFactory
- interface Factory {
- fun create(): NotificationScrollViewModel
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
index a8cbbd4..a52ab0c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
@@ -20,7 +20,6 @@
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN;
import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
-import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
import static android.inputmethodservice.InputMethodService.IME_VISIBLE;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
@@ -512,7 +511,7 @@
externalNavBar.setImeWindowStatus(EXTERNAL_DISPLAY_ID, IME_VISIBLE,
BACK_DISPOSITION_DEFAULT, true);
- defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, IME_INVISIBLE,
+ defaultNavBar.setImeWindowStatus(DEFAULT_DISPLAY, 0 /* vis */,
BACK_DISPOSITION_DEFAULT, false);
// Verify IME window state will be updated in external NavBar & default NavBar state reset.
assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 86d21e8..6916bbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -16,7 +16,6 @@
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
-import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
@@ -207,7 +206,7 @@
mCommandQueue.setImeWindowStatus(SECONDARY_DISPLAY, 1, 2, true);
waitForIdleSync();
- verify(mCallbacks).setImeWindowStatus(eq(DEFAULT_DISPLAY), eq(IME_INVISIBLE),
+ verify(mCallbacks).setImeWindowStatus(eq(DEFAULT_DISPLAY), eq(0),
eq(BACK_DISPOSITION_DEFAULT), eq(false));
verify(mCallbacks).setImeWindowStatus(eq(SECONDARY_DISPLAY), eq(1), eq(2), eq(true));
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
index aac5e57..1d2bce2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryKosmos.kt
@@ -17,10 +17,9 @@
package com.android.systemui.education.data.repository
import com.android.systemui.kosmos.Kosmos
-import java.time.Clock
import java.time.Instant
var Kosmos.contextualEducationRepository: ContextualEducationRepository by
Kosmos.Fixture { FakeContextualEducationRepository() }
-var Kosmos.fakeEduClock: Clock by Kosmos.Fixture { FakeEduClock(Instant.MIN) }
+var Kosmos.fakeEduClock: FakeEduClock by Kosmos.Fixture { FakeEduClock(Instant.MIN) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt
index 513c143..c9a5d4b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/data/repository/FakeEduClock.kt
@@ -19,8 +19,9 @@
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
+import kotlin.time.Duration
-class FakeEduClock(private val base: Instant) : Clock() {
+class FakeEduClock(private var base: Instant) : Clock() {
private val zone: ZoneId = ZoneId.of("UTC")
override fun instant(): Instant {
@@ -34,4 +35,8 @@
override fun getZone(): ZoneId {
return zone
}
+
+ fun offset(duration: Duration) {
+ base = base.plusSeconds(duration.inWholeSeconds)
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
index fb4e901..5088677 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.education.domain.interactor
+import com.android.systemui.education.data.repository.fakeEduClock
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
@@ -23,7 +24,8 @@
Kosmos.Fixture {
KeyboardTouchpadEduInteractor(
backgroundScope = testScope.backgroundScope,
- contextualEducationInteractor = contextualEducationInteractor
+ contextualEducationInteractor = contextualEducationInteractor,
+ clock = fakeEduClock
)
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
index 94b1473..079b724 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodBindingController.java
@@ -109,10 +109,6 @@
* <dd>
* If this bit is ON, some of IME view, e.g. software input, candidate view, is visible.
* </dd>
- * <dt>{@link InputMethodService#IME_INVISIBLE}</dt>
- * <dd> If this bit is ON, IME is ready with views from last EditorInfo but is
- * currently invisible.
- * </dd>
* </dl>
* <em>Do not update this value outside of {@link #setImeWindowStatus(IBinder, int, int)} and
* {@link InputMethodBindingController#unbindCurrentMethod()}.</em>
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 4dcc353..3863cf0 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -2625,8 +2625,7 @@
&& mWindowManagerInternal.isKeyguardSecure(userId)) {
return false;
}
- if ((visibility & InputMethodService.IME_ACTIVE) == 0
- || (visibility & InputMethodService.IME_INVISIBLE) != 0) {
+ if ((visibility & InputMethodService.IME_ACTIVE) == 0) {
return false;
}
if (mWindowManagerInternal.isHardKeyboardAvailable() && !Flags.imeSwitcherRevamp()) {
@@ -2791,7 +2790,7 @@
if (DEBUG) {
Slog.d(TAG, "IME window vis: " + vis
+ " active: " + (vis & InputMethodService.IME_ACTIVE)
- + " inv: " + (vis & InputMethodService.IME_INVISIBLE)
+ + " visible: " + (vis & InputMethodService.IME_VISIBLE)
+ " displayId: " + curTokenDisplayId);
}
final IBinder focusedWindowToken = userData.mImeBindingState != null
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index bd551fb..b4459cb 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -1194,9 +1194,9 @@
}
boolean shouldIgnoreNotification(final NotificationRecord record) {
- // Ignore group summaries
- return (record.getSbn().isGroup() && record.getSbn().getNotification()
- .isGroupSummary());
+ // Ignore auto-group summaries => don't count them as app-posted notifications
+ // for the cooldown budget
+ return (record.getSbn().isGroup() && GroupHelper.isAggregatedGroup(record));
}
/**
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 8e3248e..f86d307 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -708,17 +708,15 @@
}
@Nullable
- static <V> PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser)
+ static PolicyKey readPolicyKeyFromXml(TypedXmlPullParser parser)
throws XmlPullParserException, IOException {
- // TODO: can we avoid casting?
PolicyKey policyKey = PolicyKey.readGenericPolicyKeyFromXml(parser);
if (policyKey == null) {
Slogf.wtf(TAG, "Error parsing PolicyKey, GenericPolicyKey is null");
return null;
}
- PolicyDefinition<PolicyValue<V>> genericPolicyDefinition =
- (PolicyDefinition<PolicyValue<V>>) POLICY_DEFINITIONS.get(
- policyKey.getIdentifier());
+ PolicyDefinition<?> genericPolicyDefinition =
+ POLICY_DEFINITIONS.get(policyKey.getIdentifier());
if (genericPolicyDefinition == null) {
Slogf.wtf(TAG, "Error parsing PolicyKey, Unknown generic policy key: " + policyKey);
return null;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index de70280..14ad15e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -15,7 +15,9 @@
*/
package com.android.server.notification;
+import static android.app.Notification.FLAG_AUTOGROUP_SUMMARY;
import static android.app.Notification.FLAG_BUBBLE;
+import static android.app.Notification.FLAG_GROUP_SUMMARY;
import static android.app.Notification.GROUP_ALERT_ALL;
import static android.app.Notification.GROUP_ALERT_CHILDREN;
import static android.app.Notification.GROUP_ALERT_SUMMARY;
@@ -539,6 +541,36 @@
return r;
}
+ private NotificationRecord getAutogroupSummaryNotificationRecord(int id, String groupKey,
+ int groupAlertBehavior, UserHandle userHandle, String packageName) {
+ final Builder builder = new Builder(getContext())
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setFlag(FLAG_GROUP_SUMMARY | FLAG_AUTOGROUP_SUMMARY, true);
+
+ int defaults = 0;
+ defaults |= Notification.DEFAULT_SOUND;
+ mChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
+ Notification.AUDIO_ATTRIBUTES_DEFAULT);
+
+ builder.setDefaults(defaults);
+ builder.setGroup(groupKey);
+ builder.setGroupAlertBehavior(groupAlertBehavior);
+ Notification n = builder.build();
+
+ Context context = spy(getContext());
+ PackageManager packageManager = spy(context.getPackageManager());
+ when(context.getPackageManager()).thenReturn(packageManager);
+ when(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)).thenReturn(false);
+
+ StatusBarNotification sbn = new StatusBarNotification(packageName, packageName, id, mTag,
+ mUid, mPid, n, userHandle, null, System.currentTimeMillis());
+ NotificationRecord r = new NotificationRecord(context, sbn, mChannel);
+ mService.addNotification(r);
+ return r;
+ }
+
//
// Convenience functions for interacting with mocks
//
@@ -2603,6 +2635,79 @@
}
@Test
+ public void testBeepVolume_politeNotif_justSummaries() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+ TestableFlagResolver flagResolver = new TestableFlagResolver();
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+ // NOTIFICATION_COOLDOWN_ALL setting is enabled
+ Settings.System.putInt(getContext().getContentResolver(),
+ Settings.System.NOTIFICATION_COOLDOWN_ALL, 1);
+ initAttentionHelper(flagResolver);
+
+ NotificationRecord summary = getBeepyNotificationRecord("a", GROUP_ALERT_ALL);
+ summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+
+ // first update at 100% volume
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+ assertNotEquals(-1, summary.getLastAudiblyAlertedMs());
+ verifyBeepVolume(1.0f);
+ Mockito.reset(mRingtonePlayer);
+
+ // update should beep at 50% volume
+ summary = getBeepyNotificationRecord("a", GROUP_ALERT_ALL);
+ summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+ assertNotEquals(-1, summary.getLastAudiblyAlertedMs());
+ verifyBeepVolume(0.5f);
+ Mockito.reset(mRingtonePlayer);
+
+ // next update at 0% volume
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+ assertEquals(-1, summary.getLastAudiblyAlertedMs());
+ verifyBeepVolume(0.0f);
+
+ verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
+ }
+
+ @Test
+ public void testBeepVolume_politeNotif_autogroupSummary() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+ TestableFlagResolver flagResolver = new TestableFlagResolver();
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+ // NOTIFICATION_COOLDOWN_ALL setting is enabled
+ Settings.System.putInt(getContext().getContentResolver(),
+ Settings.System.NOTIFICATION_COOLDOWN_ALL, 1);
+ initAttentionHelper(flagResolver);
+
+ // child should beep at 100% volume
+ NotificationRecord child = getBeepyNotificationRecord("a", GROUP_ALERT_ALL);
+ mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+ assertNotEquals(-1, child.getLastAudiblyAlertedMs());
+ verifyBeepVolume(1.0f);
+ Mockito.reset(mRingtonePlayer);
+
+ // summary 0% volume (GROUP_ALERT_CHILDREN)
+ NotificationRecord summary = getAutogroupSummaryNotificationRecord(mId, "a",
+ GROUP_ALERT_CHILDREN, mUser, mPkg);
+ mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+ verifyNeverBeep();
+ assertFalse(summary.isInterruptive());
+ assertEquals(-1, summary.getLastAudiblyAlertedMs());
+ Mockito.reset(mRingtonePlayer);
+
+ // next update at 50% volume because autogroup summary was ignored
+ mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+ assertNotEquals(-1, child.getLastAudiblyAlertedMs());
+ verifyBeepVolume(0.5f);
+
+ verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
+ }
+
+ @Test
public void testBeepVolume_politeNotif_applyPerApp() throws Exception {
mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index d582b07..a745724 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -80,19 +80,33 @@
@Nullable
private Consumer<ActivityRecord> mOnPostActivityCreation;
+ @Nullable
+ private Consumer<DisplayContent> mOnPostDisplayContentCreation;
+
AppCompatActivityRobot(@NonNull WindowManagerService wm,
@NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor,
- int displayWidth, int displayHeight) {
+ int displayWidth, int displayHeight,
+ @Nullable Consumer<ActivityRecord> onPostActivityCreation,
+ @Nullable Consumer<DisplayContent> onPostDisplayContentCreation) {
mAtm = atm;
mSupervisor = supervisor;
mDisplayWidth = displayWidth;
mDisplayHeight = displayHeight;
mActivityStack = new TestComponentStack<>();
mTaskStack = new TestComponentStack<>();
+ mOnPostActivityCreation = onPostActivityCreation;
+ mOnPostDisplayContentCreation = onPostDisplayContentCreation;
createNewDisplay();
}
AppCompatActivityRobot(@NonNull WindowManagerService wm,
+ @NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor,
+ int displayWidth, int displayHeight) {
+ this(wm, atm, supervisor, displayWidth, displayHeight, /* onPostActivityCreation */ null,
+ /* onPostDisplayContentCreation */ null);
+ }
+
+ AppCompatActivityRobot(@NonNull WindowManagerService wm,
@NonNull ActivityTaskManagerService atm, @NonNull ActivityTaskSupervisor supervisor) {
this(wm, atm, supervisor, DEFAULT_DISPLAY_WIDTH, DEFAULT_DISPLAY_HEIGHT);
}
@@ -114,7 +128,6 @@
createActivityWithComponentInNewTask(/* inNewTask */ true, /* inNewDisplay */ true);
}
-
void configureTopActivity(float minAspect, float maxAspect, int screenOrientation,
boolean isUnresizable) {
prepareLimitedBounds(mActivityStack.top(), minAspect, maxAspect, screenOrientation,
@@ -260,21 +273,20 @@
void createNewDisplay() {
mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayWidth, mDisplayHeight)
.build();
- spyOn(mDisplayContent);
- spyOnAppCompatCameraPolicy();
+ onPostDisplayContentCreation(mDisplayContent);
}
void createNewTask() {
final Task newTask = new WindowTestsBase.TaskBuilder(mSupervisor)
.setDisplay(mDisplayContent).build();
- pushTask(newTask);
+ mTaskStack.push(newTask);
}
void createNewTaskWithBaseActivity() {
final Task newTask = new WindowTestsBase.TaskBuilder(mSupervisor)
.setCreateActivity(true)
.setDisplay(mDisplayContent).build();
- pushTask(newTask);
+ mTaskStack.push(newTask);
pushActivity(newTask.getTopNonFinishingActivity());
}
@@ -408,7 +420,6 @@
*/
@CallSuper
void onPostActivityCreation(@NonNull ActivityRecord activity) {
- spyOn(activity);
spyOn(activity.mLetterboxUiController);
if (mOnPostActivityCreation != null) {
mOnPostActivityCreation.accept(activity);
@@ -416,14 +427,17 @@
}
/**
- * Each Robot can specify its own set of operation to execute on a newly created
- * {@link ActivityRecord}. Most common the use of spyOn().
+ * Specific Robots can override this method to add operation to run on a newly created
+ * {@link DisplayContent}. Common case is to invoke spyOn().
*
- * @param onPostActivityCreation The reference to the code to execute after the creation of a
- * new {@link ActivityRecord}.
+ * @param displayContent The newly created {@link DisplayContent}.
*/
- void setOnPostActivityCreation(@Nullable Consumer<ActivityRecord> onPostActivityCreation) {
- mOnPostActivityCreation = onPostActivityCreation;
+ @CallSuper
+ void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
+ spyOn(mDisplayContent);
+ if (mOnPostDisplayContentCreation != null) {
+ mOnPostDisplayContentCreation.accept(mDisplayContent);
+ }
}
private void createActivityWithComponentInNewTask(boolean inNewTask, boolean inNewDisplay) {
@@ -489,28 +503,5 @@
private void pushActivity(@NonNull ActivityRecord activity) {
mActivityStack.push(activity);
onPostActivityCreation(activity);
- // TODO (b/351763164): Use these spyOn calls only when necessary.
- spyOn(activity.mAppCompatController.getTransparentPolicy());
- spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides());
- spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy());
- spyOn(activity.mAppCompatController.getAppCompatFocusOverrides());
- spyOn(activity.mAppCompatController.getAppCompatResizeOverrides());
- spyOn(activity.mAppCompatController.getAppCompatReachabilityPolicy());
- spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides());
- }
-
- private void pushTask(@NonNull Task task) {
- spyOn(task);
- mTaskStack.push(task);
- }
-
- private void spyOnAppCompatCameraPolicy() {
- spyOn(mDisplayContent.mAppCompatCameraPolicy);
- if (mDisplayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) {
- spyOn(mDisplayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy);
- }
- if (mDisplayContent.mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()) {
- spyOn(mDisplayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy);
- }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
index a6fd112..1e40aa0 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
@@ -291,7 +291,6 @@
* Runs a test scenario providing a Robot.
*/
void runTestScenario(@NonNull Consumer<AspectRatioOverridesRobotTest> consumer) {
- spyOn(mWm.mAppCompatConfiguration);
final AspectRatioOverridesRobotTest robot =
new AspectRatioOverridesRobotTest(mWm, mAtm, mSupervisor);
consumer.accept(robot);
@@ -305,6 +304,18 @@
super(wm, atm, supervisor);
}
+ @Override
+ void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
+ super.onPostDisplayContentCreation(displayContent);
+ spyOn(displayContent.mAppCompatCameraPolicy);
+ }
+
+ @Override
+ void onPostActivityCreation(@NonNull ActivityRecord activity) {
+ super.onPostActivityCreation(activity);
+ spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides());
+ }
+
void checkShouldApplyUserFullscreenOverride(boolean expected) {
assertEquals(expected, getTopActivityAppCompatAspectRatioOverrides()
.shouldApplyUserFullscreenOverride());
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
index de99f54..84ffcb8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
@@ -387,6 +387,12 @@
super(wm, atm, supervisor);
}
+ @Override
+ void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
+ super.onPostDisplayContentCreation(displayContent);
+ spyOn(displayContent.mAppCompatCameraPolicy);
+ }
+
void checkShouldRefreshActivityForCameraCompat(boolean expected) {
Assert.assertEquals(getAppCompatCameraOverrides()
.shouldRefreshActivityForCameraCompat(), expected);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
index 0b1bb0f..c42228d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
@@ -150,6 +150,12 @@
super(wm, atm, supervisor);
}
+ @Override
+ void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
+ super.onPostDisplayContentCreation(displayContent);
+ spyOn(displayContent.mAppCompatCameraPolicy);
+ }
+
void checkTopActivityHasDisplayRotationCompatPolicy(boolean exists) {
Assert.assertEquals(exists, activity().top().mDisplayContent
.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy());
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
index 6c0d8c4..d9b5f37 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationOverridesTest.java
@@ -250,6 +250,12 @@
mTestCurrentTimeMillisSupplier = new CurrentTimeMillisSupplierFake();
}
+ @Override
+ void onPostActivityCreation(@NonNull ActivityRecord activity) {
+ super.onPostActivityCreation(activity);
+ spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy());
+ }
+
// Useful to reduce timeout during tests
void prepareMockedTime() {
getTopOrientationOverrides().mOrientationOverridesState.mCurrentTimeMillisSupplier =
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
index ad34a6b..f6d0744 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -536,6 +536,25 @@
}
}
+ @Override
+ void onPostActivityCreation(@NonNull ActivityRecord activity) {
+ super.onPostActivityCreation(activity);
+ spyOn(activity.mAppCompatController.getAppCompatAspectRatioOverrides());
+ spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy());
+ }
+
+ @Override
+ void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
+ super.onPostDisplayContentCreation(displayContent);
+ spyOn(displayContent.mAppCompatCameraPolicy);
+ if (displayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) {
+ spyOn(displayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy);
+ }
+ if (displayContent.mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()) {
+ spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy);
+ }
+ }
+
void prepareRelaunchingAfterRequestedOrientationChanged(boolean enabled) {
getTopOrientationOverrides().setRelaunchingAfterRequestedOrientationChanged(enabled);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java
index 47f584a..5ff8f02 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java
@@ -183,6 +183,7 @@
@Override
void onPostActivityCreation(@NonNull ActivityRecord activity) {
super.onPostActivityCreation(activity);
+ spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides());
activity.mAppCompatController.getAppCompatReachabilityPolicy()
.setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java
index 84f89b5..96734b3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityPolicyTest.java
@@ -247,6 +247,7 @@
@Override
void onPostActivityCreation(@NonNull ActivityRecord activity) {
super.onPostActivityCreation(activity);
+ spyOn(activity.mAppCompatController.getAppCompatReachabilityOverrides());
activity.mAppCompatController.getAppCompatReachabilityPolicy()
.setLetterboxInnerBoundsSupplier(mLetterboxInnerBoundsSupplier);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
index 57ff4f6..4e58e1d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatRobotBase.java
@@ -43,8 +43,8 @@
@NonNull ActivityTaskSupervisor supervisor,
int displayWidth, int displayHeight) {
mActivityRobot = new AppCompatActivityRobot(wm, atm, supervisor,
- displayWidth, displayHeight);
- mActivityRobot.setOnPostActivityCreation(this::onPostActivityCreation);
+ displayWidth, displayHeight, this::onPostActivityCreation,
+ this::onPostDisplayContentCreation);
mConfigurationRobot =
new AppCompatConfigurationRobot(wm.mAppCompatConfiguration);
mOptPropRobot = new AppCompatComponentPropRobot(wm);
@@ -66,6 +66,16 @@
void onPostActivityCreation(@NonNull ActivityRecord activity) {
}
+ /**
+ * Specific Robots can override this method to add operation to run on a newly created
+ * {@link DisplayContent}. Common case is to invoke spyOn().
+ *
+ * @param displayContent THe newly created {@link DisplayContent}.
+ */
+ @CallSuper
+ void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
+ }
+
@NonNull
AppCompatConfigurationRobot conf() {
return mConfigurationRobot;
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
index 9e242ee..21fac9b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
@@ -16,6 +16,8 @@
package com.android.server.wm;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
import static org.mockito.Mockito.when;
import android.platform.test.annotations.Presubmit;
@@ -42,7 +44,10 @@
@Test
public void getLetterboxReasonString_inSizeCompatMode() {
runTestScenario((robot) -> {
- robot.activity().setTopActivityInSizeCompatMode(/* inScm */ true);
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponent();
+ a.setTopActivityInSizeCompatMode(/* inScm */ true);
+ });
robot.checkTopActivityLetterboxReason(/* expected */ "SIZE_COMPAT_MODE");
});
@@ -51,7 +56,10 @@
@Test
public void getLetterboxReasonString_fixedOrientation() {
runTestScenario((robot) -> {
- robot.activity().checkTopActivityInSizeCompatMode(/* inScm */ false);
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponent();
+ a.checkTopActivityInSizeCompatMode(/* inScm */ false);
+ });
robot.setIsLetterboxedForFixedOrientationAndAspectRatio(
/* forFixedOrientationAndAspectRatio */ true);
@@ -62,7 +70,10 @@
@Test
public void getLetterboxReasonString_isLetterboxedForDisplayCutout() {
runTestScenario((robot) -> {
- robot.activity().checkTopActivityInSizeCompatMode(/* inScm */ false);
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponent();
+ a.checkTopActivityInSizeCompatMode(/* inScm */ false);
+ });
robot.setIsLetterboxedForFixedOrientationAndAspectRatio(
/* forFixedOrientationAndAspectRatio */ false);
robot.setIsLetterboxedForDisplayCutout(/* displayCutout */ true);
@@ -74,7 +85,10 @@
@Test
public void getLetterboxReasonString_aspectRatio() {
runTestScenario((robot) -> {
- robot.activity().checkTopActivityInSizeCompatMode(/* inScm */ false);
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponent();
+ a.checkTopActivityInSizeCompatMode(/* inScm */ false);
+ });
robot.setIsLetterboxedForFixedOrientationAndAspectRatio(
/* forFixedOrientationAndAspectRatio */ false);
robot.setIsLetterboxedForDisplayCutout(/* displayCutout */ false);
@@ -87,7 +101,10 @@
@Test
public void getLetterboxReasonString_unknownReason() {
runTestScenario((robot) -> {
- robot.activity().checkTopActivityInSizeCompatMode(/* inScm */ false);
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponent();
+ a.checkTopActivityInSizeCompatMode(/* inScm */ false);
+ });
robot.setIsLetterboxedForFixedOrientationAndAspectRatio(
/* forFixedOrientationAndAspectRatio */ false);
robot.setIsLetterboxedForDisplayCutout(/* displayCutout */ false);
@@ -97,7 +114,6 @@
});
}
-
/**
* Runs a test scenario providing a Robot.
*/
@@ -114,10 +130,15 @@
@NonNull ActivityTaskManagerService atm,
@NonNull ActivityTaskSupervisor supervisor) {
super(wm, atm, supervisor);
- activity().createActivityWithComponent();
mWindowState = Mockito.mock(WindowState.class);
}
+ @Override
+ void onPostActivityCreation(@NonNull ActivityRecord activity) {
+ super.onPostActivityCreation(activity);
+ spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy());
+ }
+
void setIsLetterboxedForFixedOrientationAndAspectRatio(
boolean forFixedOrientationAndAspectRatio) {
when(activity().top().mAppCompatController.getAppCompatAspectRatioPolicy()
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
index 407218d..a0641cd 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java
@@ -22,6 +22,8 @@
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_90;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
import static org.mockito.Mockito.clearInvocations;
import android.platform.test.annotations.EnableFlags;
@@ -363,6 +365,12 @@
activity().createNewTaskWithBaseActivity();
}
+ @Override
+ void onPostActivityCreation(@NonNull ActivityRecord activity) {
+ super.onPostActivityCreation(activity);
+ spyOn(activity.mAppCompatController.getTransparentPolicy());
+ }
+
void transparentActivity(@NonNull Consumer<AppCompatTransparentActivityRobot> consumer) {
consumer.accept(mTransparentActivityRobot);
}
diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java
new file mode 100644
index 0000000..e3ec62d
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogCommandHandlerTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.protolog;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.endsWith;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.times;
+
+import android.platform.test.annotations.Presubmit;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Test class for {@link ProtoLogImpl}.
+ */
+@Presubmit
+@RunWith(MockitoJUnitRunner.class)
+public class ProtoLogCommandHandlerTest {
+
+ @Mock
+ ProtoLogService mProtoLogService;
+ @Mock
+ PrintWriter mPrintWriter;
+
+ @Test
+ public void printsHelpForAllAvailableCommands() {
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.onHelp();
+ validateOnHelpPrinted();
+ }
+
+ @Test
+ public void printsHelpIfCommandIsNull() {
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.onCommand(null);
+ validateOnHelpPrinted();
+ }
+
+ @Test
+ public void handlesGroupListCommand() {
+ Mockito.when(mProtoLogService.getGroups())
+ .thenReturn(new String[] {"MY_TEST_GROUP", "MY_OTHER_GROUP"});
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "groups", "list" });
+
+ Mockito.verify(mPrintWriter, times(1))
+ .println(contains("MY_TEST_GROUP"));
+ Mockito.verify(mPrintWriter, times(1))
+ .println(contains("MY_OTHER_GROUP"));
+ }
+
+ @Test
+ public void handlesIncompleteGroupsCommand() {
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "groups" });
+
+ Mockito.verify(mPrintWriter, times(1))
+ .println(contains("Incomplete command"));
+ }
+
+ @Test
+ public void handlesGroupStatusCommand() {
+ Mockito.when(mProtoLogService.getGroups()).thenReturn(new String[] {"MY_GROUP"});
+ Mockito.when(mProtoLogService.isLoggingToLogcat("MY_GROUP")).thenReturn(true);
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "groups", "status", "MY_GROUP" });
+
+ Mockito.verify(mPrintWriter, times(1))
+ .println(contains("MY_GROUP"));
+ Mockito.verify(mPrintWriter, times(1))
+ .println(contains("LOG_TO_LOGCAT = true"));
+ }
+
+ @Test
+ public void handlesGroupStatusCommandOfUnregisteredGroups() {
+ Mockito.when(mProtoLogService.getGroups()).thenReturn(new String[] {});
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "groups", "status", "MY_GROUP" });
+
+ Mockito.verify(mPrintWriter, times(1))
+ .println(contains("MY_GROUP"));
+ Mockito.verify(mPrintWriter, times(1))
+ .println(contains("UNREGISTERED"));
+ }
+
+ @Test
+ public void handlesGroupStatusCommandWithNoGroups() {
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "groups", "status" });
+
+ Mockito.verify(mPrintWriter, times(1))
+ .println(contains("Incomplete command"));
+ }
+
+ @Test
+ public void handlesIncompleteLogcatCommand() {
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "logcat" });
+
+ Mockito.verify(mPrintWriter, times(1))
+ .println(contains("Incomplete command"));
+ }
+
+ @Test
+ public void handlesLogcatEnableCommand() {
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "logcat", "enable", "MY_GROUP" });
+ Mockito.verify(mProtoLogService).enableProtoLogToLogcat("MY_GROUP");
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "logcat", "enable", "MY_GROUP", "MY_OTHER_GROUP" });
+ Mockito.verify(mProtoLogService)
+ .enableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP");
+ }
+
+ @Test
+ public void handlesLogcatDisableCommand() {
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "logcat", "disable", "MY_GROUP" });
+ Mockito.verify(mProtoLogService).disableProtoLogToLogcat("MY_GROUP");
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "logcat", "disable", "MY_GROUP", "MY_OTHER_GROUP" });
+ Mockito.verify(mProtoLogService)
+ .disableProtoLogToLogcat("MY_GROUP", "MY_OTHER_GROUP");
+ }
+
+ @Test
+ public void handlesLogcatEnableCommandWithNoGroups() {
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "logcat", "enable" });
+ Mockito.verify(mPrintWriter).println(contains("Incomplete command"));
+ }
+
+ @Test
+ public void handlesLogcatDisableCommandWithNoGroups() {
+ final ProtoLogCommandHandler cmdHandler =
+ new ProtoLogCommandHandler(mProtoLogService, mPrintWriter);
+
+ cmdHandler.exec(mProtoLogService, FileDescriptor.in, FileDescriptor.out, FileDescriptor.err,
+ new String[] { "logcat", "disable" });
+ Mockito.verify(mPrintWriter).println(contains("Incomplete command"));
+ }
+
+ private void validateOnHelpPrinted() {
+ Mockito.verify(mPrintWriter, times(1)).println(endsWith("help"));
+ Mockito.verify(mPrintWriter, times(1))
+ .println(endsWith("groups (list | status)"));
+ Mockito.verify(mPrintWriter, times(1))
+ .println(endsWith("logcat (enable | disable) <group>"));
+ Mockito.verify(mPrintWriter, atLeast(0)).println(anyString());
+ }
+}