Merge "fix: remove unused test" into main
diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java
index e2d215e..4bc5bd2 100644
--- a/core/java/android/inputmethodservice/AbstractInputMethodService.java
+++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java
@@ -29,6 +29,7 @@
import android.view.MotionEvent;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodSession;
import android.window.WindowProviderService;
@@ -186,6 +187,10 @@
if (callback != null) {
callback.finishedEvent(seq, handled);
}
+ if (Flags.imeSwitcherRevamp() && !handled && event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getUnicodeChar() > 0 && mInputMethodServiceInternal != null) {
+ mInputMethodServiceInternal.notifyUserActionIfNecessary();
+ }
}
/**
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 943b04f..d560399 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -101,6 +101,7 @@
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
+import android.os.UserHandle;
import android.provider.Settings;
import android.text.InputType;
import android.text.Layout;
@@ -4341,6 +4342,16 @@
}
/**
+ * Called when the IME switch button was clicked from the client. This will show the input
+ * method picker dialog.
+ *
+ * @hide
+ */
+ final void onImeSwitchButtonClickFromClient() {
+ mPrivOps.onImeSwitchButtonClickFromClient(getDisplayId(), UserHandle.myUserId());
+ }
+
+ /**
* Used to inject custom {@link InputMethodServiceInternal}.
*
* @return the {@link InputMethodServiceInternal} to be used.
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index de67e06..3ce67b0 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -42,6 +42,7 @@
import android.view.WindowInsetsController.Appearance;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
+import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import com.android.internal.inputmethod.InputMethodNavButtonFlags;
@@ -145,7 +146,8 @@
return mImpl.toDebugString();
}
- private static final class Impl implements Callback, Window.DecorCallback {
+ private static final class Impl implements Callback, Window.DecorCallback,
+ NavigationBarView.ButtonClickListener {
private static final int DEFAULT_COLOR_ADAPT_TRANSITION_TIME = 1700;
// Copied from com.android.systemui.animation.Interpolators#LEGACY_DECELERATE
@@ -241,6 +243,7 @@
? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN
: 0);
navigationBarView.setNavigationIconHints(hints);
+ navigationBarView.prepareNavButtons(this);
}
} else {
mNavigationBarFrame.setLayoutParams(new FrameLayout.LayoutParams(
@@ -592,6 +595,17 @@
return drawLegacyNavigationBarBackground;
}
+ @Override
+ public void onImeSwitchButtonClick(View v) {
+ mService.onImeSwitchButtonClickFromClient();
+ }
+
+ @Override
+ public boolean onImeSwitchButtonLongClick(View v) {
+ v.getContext().getSystemService(InputMethodManager.class).showInputMethodPicker();
+ return true;
+ }
+
/**
* Returns the height of the IME caption bar if this should be shown, or {@code 0} instead.
*/
diff --git a/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java b/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java
index f423672..540243c 100644
--- a/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java
+++ b/core/java/android/inputmethodservice/navigationbar/KeyButtonView.java
@@ -41,6 +41,7 @@
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputConnection;
import android.widget.ImageView;
@@ -58,12 +59,30 @@
private int mTouchDownY;
private AudioManager mAudioManager;
private boolean mGestureAborted;
+ /**
+ * Whether the long click action has been invoked. The short click action is invoked on the up
+ * event while a long click is invoked as soon as the long press duration is reached, so a long
+ * click could be performed before the short click is checked, in which case the short click's
+ * action should not be invoked.
+ *
+ * @see View#mHasPerformedLongPress
+ */
+ private boolean mLongClicked;
private OnClickListener mOnClickListener;
private final KeyButtonRipple mRipple;
private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private float mDarkIntensity;
private boolean mHasOvalBg = false;
+ /** Runnable for checking whether the long click action should be performed. */
+ private final Runnable mCheckLongPress = new Runnable() {
+ public void run() {
+ if (isPressed() && performLongClick()) {
+ mLongClicked = true;
+ }
+ }
+ };
+
public KeyButtonView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -159,6 +178,7 @@
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownTime = SystemClock.uptimeMillis();
+ mLongClicked = false;
setPressed(true);
// Use raw X and Y to detect gestures in case a parent changes the x and y values
@@ -173,6 +193,10 @@
if (!showSwipeUI) {
playSoundEffect(SoundEffectConstants.CLICK);
}
+ if (Flags.imeSwitcherRevamp() && isLongClickable()) {
+ removeCallbacks(mCheckLongPress);
+ postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
+ }
break;
case MotionEvent.ACTION_MOVE:
x = (int) ev.getRawX();
@@ -183,6 +207,9 @@
// When quick step is enabled, prevent animating the ripple triggered by
// setPressed and decide to run it on touch up
setPressed(false);
+ if (isLongClickable()) {
+ removeCallbacks(mCheckLongPress);
+ }
}
break;
case MotionEvent.ACTION_CANCEL:
@@ -190,9 +217,12 @@
if (mCode != KEYCODE_UNKNOWN) {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
+ if (isLongClickable()) {
+ removeCallbacks(mCheckLongPress);
+ }
break;
case MotionEvent.ACTION_UP:
- final boolean doIt = isPressed();
+ final boolean doIt = isPressed() && !mLongClicked;
setPressed(false);
final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
if (showSwipeUI) {
@@ -201,7 +231,7 @@
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
playSoundEffect(SoundEffectConstants.CLICK);
}
- } else if (doHapticFeedback) {
+ } else if (doHapticFeedback && !mLongClicked) {
// Always send a release ourselves because it doesn't seem to be sent elsewhere
// and it feels weird to sometimes get a release haptic and other times not.
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
@@ -221,6 +251,9 @@
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
}
+ if (isLongClickable()) {
+ removeCallbacks(mCheckLongPress);
+ }
break;
}
diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
index e28f345..a3beaf4 100644
--- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
+++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
@@ -26,6 +26,7 @@
import android.animation.PropertyValuesHolder;
import android.annotation.DrawableRes;
import android.annotation.FloatRange;
+import android.annotation.NonNull;
import android.app.StatusBarManager;
import android.content.Context;
import android.content.res.Configuration;
@@ -39,6 +40,7 @@
import android.view.View;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
@@ -79,6 +81,28 @@
private NavigationBarInflaterView mNavigationInflaterView;
+ /**
+ * Interface definition for callbacks to be invoked when navigation bar buttons are clicked.
+ */
+ public interface ButtonClickListener {
+
+ /**
+ * Called when the IME switch button is clicked.
+ *
+ * @param v The view that was clicked.
+ */
+ void onImeSwitchButtonClick(View v);
+
+ /**
+ * Called when the IME switch button has been clicked and held.
+ *
+ * @param v The view that was clicked and held.
+ *
+ * @return true if the callback consumed the long click, false otherwise.
+ */
+ boolean onImeSwitchButtonLongClick(View v);
+ }
+
public NavigationBarView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -98,13 +122,27 @@
new ButtonDispatcher(com.android.internal.R.id.input_method_nav_home_handle));
mDeadZone = new android.inputmethodservice.navigationbar.DeadZone(this);
+ }
+ /**
+ * Prepares the navigation bar buttons to be used and sets the on click listeners.
+ *
+ * @param listener The listener used to handle the clicks on the navigation bar buttons.
+ */
+ public void prepareNavButtons(@NonNull ButtonClickListener listener) {
getBackButton().setLongClickable(false);
- final ButtonDispatcher imeSwitchButton = getImeSwitchButton();
- imeSwitchButton.setLongClickable(false);
- imeSwitchButton.setOnClickListener(view -> view.getContext()
- .getSystemService(InputMethodManager.class).showInputMethodPicker());
+ if (Flags.imeSwitcherRevamp()) {
+ final var imeSwitchButton = getImeSwitchButton();
+ imeSwitchButton.setLongClickable(true);
+ imeSwitchButton.setOnClickListener(listener::onImeSwitchButtonClick);
+ imeSwitchButton.setOnLongClickListener(listener::onImeSwitchButtonLongClick);
+ } else {
+ final ButtonDispatcher imeSwitchButton = getImeSwitchButton();
+ imeSwitchButton.setLongClickable(false);
+ imeSwitchButton.setOnClickListener(view -> view.getContext()
+ .getSystemService(InputMethodManager.class).showInputMethodPicker());
+ }
}
@Override
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index e7a2fb9..07a9794 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -465,6 +465,20 @@
}
@AnyThread
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ static void onImeSwitchButtonClickFromSystem(int displayId) {
+ final IInputMethodManager service = getService();
+ if (service == null) {
+ return;
+ }
+ try {
+ service.onImeSwitchButtonClickFromSystem(displayId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @AnyThread
@Nullable
@RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)
static InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) {
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 0c63e58..dbbfff0 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -4354,6 +4354,19 @@
}
/**
+ * Called when the IME switch button was clicked from the system. This will show the input
+ * method picker dialog.
+ *
+ * @param displayId The ID of the display where the input method picker dialog should be shown.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ public void onImeSwitchButtonClickFromSystem(int displayId) {
+ IInputMethodManagerGlobalInvoker.onImeSwitchButtonClickFromSystem(displayId);
+ }
+
+ /**
* A test API for CTS to check whether there are any pending IME visibility requests.
*
* @return {@code true} iff there are pending IME visibility requests.
diff --git a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
index 63623c7..82ae76a 100644
--- a/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
+++ b/core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl
@@ -43,6 +43,7 @@
void switchToPreviousInputMethod(in AndroidFuture future /* T=Boolean */);
void switchToNextInputMethod(boolean onlyCurrentIme, in AndroidFuture future /* T=Boolean */);
void shouldOfferSwitchingToNextInputMethod(in AndroidFuture future /* T=Boolean */);
+ void onImeSwitchButtonClickFromClient(int displayId, int userId);
void notifyUserActionAsync();
void applyImeVisibilityAsync(IBinder showOrHideInputToken, boolean setVisible,
in ImeTracker.Token statsToken);
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 72c41be..f50f02e 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -378,6 +378,22 @@
}
/**
+ * Calls {@link IInputMethodPrivilegedOperations#onImeSwitchButtonClickFromClient(int, int)}
+ */
+ @AnyThread
+ public void onImeSwitchButtonClickFromClient(int displayId, int userId) {
+ final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
+ if (ops == null) {
+ return;
+ }
+ try {
+ ops.onImeSwitchButtonClickFromClient(displayId, userId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Calls {@link IInputMethodPrivilegedOperations#notifyUserActionAsync()}
*/
@AnyThread
diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl
index 2b3ffeb2..cba27ce 100644
--- a/core/java/com/android/internal/view/IInputMethodManager.aidl
+++ b/core/java/com/android/internal/view/IInputMethodManager.aidl
@@ -135,6 +135,19 @@
+ "android.Manifest.permission.TEST_INPUT_METHOD)")
boolean isInputMethodPickerShownForTest();
+ /**
+ * Called when the IME switch button was clicked from the system. Depending on the number of
+ * enabled IME subtypes, this will either switch to the next IME/subtype, or show the input
+ * method picker dialog.
+ *
+ * @param displayId The ID of the display where the input method picker dialog should be shown.
+ * @param userId The ID of the user that triggered the click.
+ */
+ @EnforcePermission("WRITE_SECURE_SETTINGS")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.WRITE_SECURE_SETTINGS)")
+ oneway void onImeSwitchButtonClickFromSystem(int displayId);
+
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ "android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true)")
@nullable InputMethodSubtype getCurrentInputMethodSubtype(int userId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 1532211..b5b476d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -18,6 +18,7 @@
import static android.view.WindowManager.TRANSIT_CHANGE;
+import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW;
import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW;
import android.graphics.Point;
@@ -103,6 +104,9 @@
wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true);
mTaskOrganizer.applyTransaction(wct);
}
+ } else {
+ mInteractionJankMonitor.begin(mDesktopWindowDecoration.mTaskSurface,
+ mDesktopWindowDecoration.mContext, CUJ_DESKTOP_MODE_DRAG_WINDOW);
}
mDragStartListener.onDragStart(mDesktopWindowDecoration.mTaskInfo.taskId);
mRepositionTaskBounds.set(mTaskBoundsAtDragStart);
@@ -157,11 +161,16 @@
}
mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_RESIZE_WINDOW);
} else {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
- wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
- mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
+ if (!mTaskBoundsAtDragStart.equals(mRepositionTaskBounds)) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
+ mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
+ } else {
+ // Drag-move ended where it originally started, no need to update WM.
+ mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_DRAG_WINDOW);
+ }
}
mCtrlType = CTRL_TYPE_UNDEFINED;
@@ -202,6 +211,7 @@
mCtrlType = CTRL_TYPE_UNDEFINED;
finishCallback.onTransitionFinished(null);
mIsResizingOrAnimatingResize = false;
+ mInteractionJankMonitor.end(CUJ_DESKTOP_MODE_DRAG_WINDOW);
return true;
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowMultiWindow.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowMultiWindow.kt
new file mode 100644
index 0000000..bbf0ce5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowMultiWindow.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.wm.shell.flicker.service.desktopmode.scenarios
+
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.server.wm.flicker.helpers.MailAppHelper
+import com.android.server.wm.flicker.helpers.NewTasksAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+
+/** Base scenario test for window drag CUJ with multiple windows. */
+@Ignore("Base Test Class")
+abstract class DragAppWindowMultiWindow : DragAppWindowScenarioTestBase()
+{
+ private val imeAppHelper = ImeAppHelper(instrumentation)
+ private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation))
+ private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation))
+ private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation))
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ testApp.enterDesktopWithDrag(wmHelper, device)
+ mailApp.launchViaIntent(wmHelper)
+ newTasksApp.launchViaIntent(wmHelper)
+ imeApp.launchViaIntent(wmHelper)
+ }
+
+ @Test
+ override fun dragAppWindow() {
+ val (startXIme, startYIme) = getWindowDragStartCoordinate(imeAppHelper)
+
+ imeApp.dragWindow(startXIme, startYIme,
+ endX = startXIme + 150, endY = startYIme + 150,
+ wmHelper, device)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ mailApp.exit(wmHelper)
+ newTasksApp.exit(wmHelper)
+ imeApp.exit(wmHelper)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowScenarioTestBase.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowScenarioTestBase.kt
new file mode 100644
index 0000000..a613ca1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowScenarioTestBase.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.wm.shell.flicker.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+/** Base test class for window drag CUJ. */
+@Ignore("Base Test Class")
+abstract class DragAppWindowScenarioTestBase {
+
+ val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ val tapl = LauncherInstrumentation()
+ val wmHelper = WindowManagerStateHelper(instrumentation)
+ val device = UiDevice.getInstance(instrumentation)
+
+ @Rule
+ @JvmField
+ val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0)
+
+ @Test abstract fun dragAppWindow()
+
+ /** Return the top-center coordinate of the app header as the start coordinate. */
+ fun getWindowDragStartCoordinate(appHelper: StandardAppHelper): Pair<Int, Int> {
+ val windowRect = wmHelper.getWindowRegion(appHelper).bounds
+ // Set start x-coordinate as center of app header.
+ val startX = windowRect.centerX()
+ val startY = windowRect.top
+ return Pair(startX, startY)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowSingleWindow.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowSingleWindow.kt
new file mode 100644
index 0000000..0655620
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/DragAppWindowSingleWindow.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.scenarios
+
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Test
+
+/** Base scenario test for window drag CUJ with single window. */
+@Ignore("Base Test Class")
+abstract class DragAppWindowSingleWindow : DragAppWindowScenarioTestBase()
+{
+ private val simpleAppHelper = SimpleAppHelper(instrumentation)
+ private val testApp = DesktopModeAppHelper(simpleAppHelper)
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ testApp.enterDesktopWithDrag(wmHelper, device)
+ }
+
+ @Test
+ override fun dragAppWindow() {
+ val (startXTest, startYTest) = getWindowDragStartCoordinate(simpleAppHelper)
+ testApp.dragWindow(startXTest, startYTest,
+ endX = startXTest + 150, endY = startYTest + 150,
+ wmHelper, device)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index cee8ae9..2cbfa7d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -16,9 +16,11 @@
package com.android.systemui.communal
+import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
@@ -60,6 +62,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(FLAG_COMMUNAL_HUB)
class CommunalSceneStartableTest : SysuiTestCase() {
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index 444f63a..60c9bb0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -41,6 +41,7 @@
import com.android.internal.logging.UiEventLogger
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.ambient.touch.TouchMonitor
import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
@@ -118,11 +119,11 @@
@Mock
lateinit var mDreamComplicationComponentFactory:
- com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory
+ com.android.systemui.dreams.complication.dagger.ComplicationComponent.Factory
@Mock
lateinit var mDreamComplicationComponent:
- com.android.systemui.dreams.complication.dagger.ComplicationComponent
+ com.android.systemui.dreams.complication.dagger.ComplicationComponent
@Mock lateinit var mHideComplicationTouchHandler: HideComplicationTouchHandler
@@ -202,8 +203,12 @@
whenever(mScrimManager.getCurrentController()).thenReturn(mScrimController)
whenever(mLazyViewCapture.value).thenReturn(viewCaptureSpy)
mWindowParams = WindowManager.LayoutParams()
- mViewCaptureAwareWindowManager = ViewCaptureAwareWindowManager(mWindowManager,
- mLazyViewCapture, isViewCaptureEnabled = false)
+ mViewCaptureAwareWindowManager =
+ ViewCaptureAwareWindowManager(
+ mWindowManager,
+ mLazyViewCapture,
+ isViewCaptureEnabled = false
+ )
mService =
DreamOverlayService(
mContext,
@@ -257,7 +262,7 @@
mMainExecutor.runAllReady()
verify(mUiEventLogger).log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_ENTER_START)
verify(mUiEventLogger)
- .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
+ .log(DreamOverlayService.DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START)
}
@Test
@@ -634,7 +639,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT)
+ @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB)
@kotlin.Throws(RemoteException::class)
fun testTransitionToGlanceableHub() =
testScope.runTest {
@@ -658,7 +663,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT)
+ @EnableFlags(Flags.FLAG_DREAM_WAKE_REDIRECT, FLAG_COMMUNAL_HUB)
@Throws(RemoteException::class)
fun testRedirectExit() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index ec4fd79..b885800 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -123,7 +123,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, Flags.FLAG_COMMUNAL_HUB)
fun testTransitionToLockscreen_onPowerButtonPress_canDream_glanceableHubAvailable() =
testScope.runTest {
whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
index 1c3021e..73a0039 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropStateTest.kt
@@ -82,6 +82,20 @@
TestEditTiles.forEach { assertThat(underTest.isMoving(it.tileSpec)).isFalse() }
}
+ @Test
+ fun onMoveOutOfBounds_removeMovingTileFromCurrentList() {
+ val movingTileSpec = TestEditTiles[0].tileSpec
+
+ // Start the drag movement
+ underTest.onStarted(movingTileSpec)
+
+ // Move the tile outside of the list
+ underTest.movedOutOfBounds()
+
+ // Asserts the moving tile is not current
+ assertThat(listState.tiles.first { it.tileSpec == movingTileSpec }.isCurrent).isFalse()
+ }
+
companion object {
private fun createEditTile(tileSpec: String): EditTileViewModel {
return EditTileViewModel(
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 090033d..5d804cc 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -120,7 +120,7 @@
oneway void notifyTaskbarAutohideSuspend(boolean suspend) = 48;
/**
- * Notifies SystemUI to invoke IME Switcher.
+ * Notifies that the IME switcher button has been pressed.
*/
oneway void onImeSwitcherPressed() = 49;
@@ -167,5 +167,10 @@
*/
oneway void toggleQuickSettingsPanel() = 56;
- // Next id = 57
+ /**
+ * Notifies that the IME Switcher button has been long pressed.
+ */
+ oneway void onImeSwitcherLongPress() = 57;
+
+ // Next id = 58
}
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 1e4fb4f..493afde 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -64,7 +64,6 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
-import android.service.dreams.IDreamManager;
import android.sysprop.TelephonyProperties;
import android.telecom.TelecomManager;
import android.telephony.ServiceState;
@@ -197,7 +196,6 @@
private final Context mContext;
private final GlobalActionsManager mWindowManagerFuncs;
private final AudioManager mAudioManager;
- private final IDreamManager mDreamManager;
private final DevicePolicyManager mDevicePolicyManager;
private final LockPatternUtils mLockPatternUtils;
private final SelectedUserInteractor mSelectedUserInteractor;
@@ -345,7 +343,6 @@
Context context,
GlobalActionsManager windowManagerFuncs,
AudioManager audioManager,
- IDreamManager iDreamManager,
DevicePolicyManager devicePolicyManager,
LockPatternUtils lockPatternUtils,
BroadcastDispatcher broadcastDispatcher,
@@ -382,7 +379,6 @@
mContext = context;
mWindowManagerFuncs = windowManagerFuncs;
mAudioManager = audioManager;
- mDreamManager = iDreamManager;
mDevicePolicyManager = devicePolicyManager;
mLockPatternUtils = lockPatternUtils;
mTelephonyListenerManager = telephonyListenerManager;
@@ -510,20 +506,7 @@
mHandler.sendEmptyMessage(MESSAGE_DISMISS);
}
- protected void awakenIfNecessary() {
- if (mDreamManager != null) {
- try {
- if (mDreamManager.isDreaming()) {
- mDreamManager.awaken();
- }
- } catch (RemoteException e) {
- // we tried
- }
- }
- }
-
protected void handleShow(@Nullable Expandable expandable) {
- awakenIfNecessary();
mDialog = createDialog();
prepareDialog();
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionLog.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionLog.kt
new file mode 100644
index 0000000..a80bc09
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.mediaprojection
+
+import javax.inject.Qualifier
+
+/** Logs for media projection related events. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class MediaProjectionLog
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt
index 3489459..7fd77a9 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt
@@ -16,12 +16,25 @@
package com.android.systemui.mediaprojection
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
import com.android.systemui.mediaprojection.data.repository.MediaProjectionManagerRepository
import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository
import dagger.Binds
import dagger.Module
+import dagger.Provides
@Module
interface MediaProjectionModule {
@Binds fun mediaRepository(impl: MediaProjectionManagerRepository): MediaProjectionRepository
+
+ companion object {
+ @Provides
+ @SysUISingleton
+ @MediaProjectionLog
+ fun provideMediaProjectionLogBuffer(factory: LogBufferFactory): LogBuffer {
+ return factory.create("MediaProjection", 50)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
index c90f197..5704e80 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
@@ -21,7 +21,6 @@
import android.media.projection.MediaProjectionInfo
import android.media.projection.MediaProjectionManager
import android.os.Handler
-import android.util.Log
import android.view.ContentRecordingSession
import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -29,6 +28,9 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.mediaprojection.MediaProjectionLog
import com.android.systemui.mediaprojection.MediaProjectionServiceHelper
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository
@@ -56,22 +58,27 @@
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val tasksRepository: TasksRepository,
private val mediaProjectionServiceHelper: MediaProjectionServiceHelper,
+ @MediaProjectionLog private val logger: LogBuffer,
) : MediaProjectionRepository {
override suspend fun switchProjectedTask(task: RunningTaskInfo) {
withContext(backgroundDispatcher) {
if (mediaProjectionServiceHelper.updateTaskRecordingSession(task.token)) {
- Log.d(TAG, "Successfully switched projected task")
+ logger.log(TAG, LogLevel.DEBUG, {}, { "Successfully switched projected task" })
} else {
- Log.d(TAG, "Failed to switch projected task")
+ logger.log(TAG, LogLevel.WARNING, {}, { "Failed to switch projected task" })
}
}
}
override suspend fun stopProjecting() {
withContext(backgroundDispatcher) {
- // TODO(b/332662551): Convert Logcat to LogBuffer.
- Log.d(TAG, "Requesting MediaProjectionManager#stopActiveProjection")
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ {},
+ { "Requesting MediaProjectionManager#stopActiveProjection" },
+ )
mediaProjectionManager.stopActiveProjection()
}
}
@@ -81,12 +88,22 @@
val callback =
object : MediaProjectionManager.Callback() {
override fun onStart(info: MediaProjectionInfo?) {
- Log.d(TAG, "MediaProjectionManager.Callback#onStart")
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ {},
+ { "MediaProjectionManager.Callback#onStart" },
+ )
trySendWithFailureLogging(CallbackEvent.OnStart, TAG)
}
override fun onStop(info: MediaProjectionInfo?) {
- Log.d(TAG, "MediaProjectionManager.Callback#onStop")
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ {},
+ { "MediaProjectionManager.Callback#onStop" },
+ )
trySendWithFailureLogging(CallbackEvent.OnStop, TAG)
}
@@ -94,7 +111,12 @@
info: MediaProjectionInfo,
session: ContentRecordingSession?
) {
- Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session")
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ { str1 = session.toString() },
+ { "MediaProjectionManager.Callback#onSessionStarted: $str1" },
+ )
trySendWithFailureLogging(
CallbackEvent.OnRecordingSessionSet(info, session),
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
index e832abb..afdfa59 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
@@ -40,6 +40,8 @@
import static com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen;
import static com.android.systemui.shared.rotation.RotationButtonController.DEBUG_ROTATION;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE;
+import static com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
@@ -48,8 +50,6 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.systemui.shared.system.QuickStepContract.isGesturalMode;
-import static com.android.systemui.shared.statusbar.phone.BarTransitions.MODE_OPAQUE;
-import static com.android.systemui.shared.statusbar.phone.BarTransitions.TransitionMode;
import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG_WINDOW_STATE;
import static com.android.systemui.statusbar.phone.CentralSurfaces.dumpBarTransitions;
import static com.android.systemui.util.Utils.isGesturalModeOnDefaultDisplay;
@@ -97,6 +97,7 @@
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.Nullable;
@@ -117,17 +118,17 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.keyguard.WakefulnessLifecycle;
import com.android.systemui.model.SysUiState;
-import com.android.systemui.navigationbar.views.buttons.NavBarButtonClickLogger;
import com.android.systemui.navigationbar.NavBarHelper;
import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener;
+import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
+import com.android.systemui.navigationbar.gestural.QuickswitchOrientedNavHandle;
import com.android.systemui.navigationbar.views.buttons.ButtonDispatcher;
import com.android.systemui.navigationbar.views.buttons.DeadZone;
import com.android.systemui.navigationbar.views.buttons.KeyButtonView;
+import com.android.systemui.navigationbar.views.buttons.NavBarButtonClickLogger;
import com.android.systemui.navigationbar.views.buttons.NavbarOrientationTrackingLogger;
-import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
-import com.android.systemui.navigationbar.gestural.QuickswitchOrientedNavHandle;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.recents.Recents;
@@ -1348,6 +1349,9 @@
ButtonDispatcher imeSwitcherButton = mView.getImeSwitchButton();
imeSwitcherButton.setOnClickListener(this::onImeSwitcherClick);
+ if (Flags.imeSwitcherRevamp()) {
+ imeSwitcherButton.setOnLongClickListener(this::onImeSwitcherLongClick);
+ }
updateScreenPinningGestures();
}
@@ -1501,12 +1505,29 @@
mCommandQueue.toggleRecentApps();
}
- private void onImeSwitcherClick(View v) {
+ @VisibleForTesting
+ void onImeSwitcherClick(View v) {
+ mNavBarButtonClickLogger.logImeSwitcherClick();
+ if (Flags.imeSwitcherRevamp()) {
+ mInputMethodManager.onImeSwitchButtonClickFromSystem(mDisplayId);
+ } else {
+ mInputMethodManager.showInputMethodPickerFromSystem(
+ true /* showAuxiliarySubtypes */, mDisplayId);
+ }
+ mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
+ }
+
+ @VisibleForTesting
+ boolean onImeSwitcherLongClick(View v) {
+ if (!Flags.imeSwitcherRevamp()) {
+ return false;
+ }
mNavBarButtonClickLogger.logImeSwitcherClick();
mInputMethodManager.showInputMethodPickerFromSystem(
true /* showAuxiliarySubtypes */, mDisplayId);
- mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
- };
+ mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+ return true;
+ }
private boolean onLongPressBackHome(View v) {
return onLongPressNavigationButtons(v, R.id.back, R.id.home);
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java
index 1e85d6c..133d14d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/buttons/KeyButtonView.java
@@ -112,6 +112,9 @@
@UiEvent(doc = "The overview button was long-pressed in the navigation bar.")
NAVBAR_OVERVIEW_BUTTON_LONGPRESS(538),
+ @UiEvent(doc = "The ime switcher button was long-pressed in the navigation bar.")
+ NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS(1799),
+
NONE(0); // an event we should not log
private final int mId;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
index 295a998..782fb2a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/DragAndDropState.kt
@@ -48,6 +48,9 @@
val sourceSpec: MutableState<TileSpec?>,
private val listState: EditTileListState
) {
+ val dragInProgress: Boolean
+ get() = sourceSpec.value != null
+
/** Returns index of the dragged tile if it's present in the list. Returns -1 if not. */
fun currentPosition(): Int {
return sourceSpec.value?.let { listState.indexOf(it) } ?: -1
@@ -65,6 +68,12 @@
sourceSpec.value?.let { listState.move(it, targetSpec) }
}
+ fun movedOutOfBounds() {
+ // Removing the tiles from the current tile grid if it moves out of bounds. This clears
+ // the spacer and makes it apparent that dropping the tile at that point would remove it.
+ sourceSpec.value?.let { listState.removeFromCurrent(it) }
+ }
+
fun onDrop() {
sourceSpec.value = null
}
@@ -112,6 +121,42 @@
}
/**
+ * Registers a composable as a [DragAndDropTarget] to receive drop events. Use this outside the tile
+ * grid to catch out of bounds drops.
+ *
+ * @param dragAndDropState The [DragAndDropState] using the tiles list
+ * @param onDrop Action to be executed when a [TileSpec] is dropped on the composable
+ */
+@Composable
+fun Modifier.dragAndDropRemoveZone(
+ dragAndDropState: DragAndDropState,
+ onDrop: (TileSpec) -> Unit,
+): Modifier {
+ val target =
+ remember(dragAndDropState) {
+ object : DragAndDropTarget {
+ override fun onDrop(event: DragAndDropEvent): Boolean {
+ return dragAndDropState.sourceSpec.value?.let {
+ onDrop(it)
+ dragAndDropState.onDrop()
+ true
+ } ?: false
+ }
+
+ override fun onEntered(event: DragAndDropEvent) {
+ dragAndDropState.movedOutOfBounds()
+ }
+ }
+ }
+ return dragAndDropTarget(
+ shouldStartDragAndDrop = { event ->
+ event.mimeTypes().contains(QsDragAndDrop.TILESPEC_MIME_TYPE)
+ },
+ target = target,
+ )
+}
+
+/**
* Registers a tile list as a [DragAndDropTarget] to receive drop events. Use this on list
* containers to catch drops outside of tiles.
*
@@ -128,6 +173,10 @@
val target =
remember(dragAndDropState) {
object : DragAndDropTarget {
+ override fun onEnded(event: DragAndDropEvent) {
+ dragAndDropState.onDrop()
+ }
+
override fun onDrop(event: DragAndDropEvent): Boolean {
return dragAndDropState.sourceSpec.value?.let {
onDrop(it, dragAndDropState.currentPosition())
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
index 482c498..34876c4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditTileListState.kt
@@ -46,6 +46,18 @@
tiles.apply { add(toIndex, removeAt(fromIndex).copy(isCurrent = isMovingToCurrent)) }
}
+ /**
+ * Sets the [TileSpec] as a non-current tile. Use this when a tile is dragged out of the current
+ * tile grid.
+ */
+ fun removeFromCurrent(tileSpec: TileSpec) {
+ val fromIndex = indexOf(tileSpec)
+ if (fromIndex >= 0 && fromIndex < tiles.size) {
+ // Mark the moving tile as non-current
+ tiles[fromIndex] = tiles[fromIndex].copy(isCurrent = false)
+ }
+ }
+
fun indexOf(tileSpec: TileSpec): Int {
return tiles.indexOfFirst { it.tileSpec == tileSpec }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index ada774d..add830e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -84,7 +84,7 @@
DefaultEditTileGrid(
tiles = tiles,
isIconOnly = isIcon,
- columns = GridCells.Fixed(columns),
+ columns = columns,
modifier = modifier,
onAddTile = onAddTile,
onRemoveTile = onRemoveTile,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
index 8ca91d8..6c84edd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
@@ -305,9 +305,9 @@
largeTiles,
ClickAction.ADD,
addTileToEnd,
- onDoubleTap,
isIconOnly,
dragAndDropState,
+ onDoubleTap = onDoubleTap,
acceptDrops = { true },
onDrop = onDrop,
)
@@ -318,10 +318,10 @@
smallTiles,
ClickAction.ADD,
addTileToEnd,
- onDoubleTap,
isIconOnly,
+ dragAndDropState,
+ onDoubleTap = onDoubleTap,
showLabels = showLabels,
- dragAndDropState = dragAndDropState,
acceptDrops = { true },
onDrop = onDrop,
)
@@ -332,10 +332,10 @@
tilesCustom,
ClickAction.ADD,
addTileToEnd,
- onDoubleTap,
isIconOnly,
+ dragAndDropState,
+ onDoubleTap = onDoubleTap,
showLabels = showLabels,
- dragAndDropState = dragAndDropState,
acceptDrops = { true },
onDrop = onDrop,
)
@@ -372,11 +372,6 @@
}
}
- private fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp {
- val rows = (nTiles + columns - 1) / columns
- return ((tileHeight + padding) * rows) - padding
- }
-
/** Fill up the rest of the row if it's not complete. */
private fun LazyGridScope.fillUpRow(nTiles: Int, columns: Int) {
if (nTiles % columns != 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
index 770d4412..3e48245 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
@@ -100,7 +100,7 @@
DefaultEditTileGrid(
tiles = tiles,
isIconOnly = iconTilesViewModel::isIconTile,
- columns = GridCells.Fixed(columns),
+ columns = columns,
modifier = modifier,
onAddTile = onAddTile,
onRemoveTile = onRemoveTile,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index 3fdd7f7..bd7956d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -23,13 +23,19 @@
import android.service.quicksettings.Tile.STATE_INACTIVE
import android.text.TextUtils
import androidx.appcompat.content.res.AppCompatResources
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
+import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.spacedBy
@@ -37,20 +43,31 @@
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -80,6 +97,8 @@
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.common.ui.compose.load
import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.TileRow
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.toUiState
@@ -269,7 +288,7 @@
fun DefaultEditTileGrid(
tiles: List<EditTileViewModel>,
isIconOnly: (TileSpec) -> Boolean,
- columns: GridCells,
+ columns: Int,
modifier: Modifier,
onAddTile: (TileSpec, Int) -> Unit,
onRemoveTile: (TileSpec) -> Unit,
@@ -277,84 +296,264 @@
) {
val currentListState = rememberEditListState(tiles)
val dragAndDropState = rememberDragAndDropState(currentListState)
-
val (currentTiles, otherTiles) = currentListState.tiles.partition { it.isCurrent }
- val (otherTilesStock, otherTilesCustom) =
- otherTiles
- .filter { !dragAndDropState.isMoving(it.tileSpec) }
- .partition { it.appName == null }
+
val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
onAddTile(it, CurrentTilesInteractor.POSITION_AT_END)
}
-
val onDropAdd: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, position ->
onAddTile(tileSpec, position)
}
- val onDropRemove: (TileSpec, Int) -> Unit by rememberUpdatedState { tileSpec, _ ->
- onRemoveTile(tileSpec)
- }
val onDoubleTap: (TileSpec) -> Unit by rememberUpdatedState { tileSpec ->
onResize(tileSpec, !isIconOnly(tileSpec))
}
+ val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical)
- TileLazyGrid(
- modifier = modifier.dragAndDropTileList(dragAndDropState, { true }, onDropAdd),
- columns = columns
+ CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
+ Column(
+ verticalArrangement =
+ spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
+ modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState())
+ ) {
+ AnimatedContent(
+ targetState = dragAndDropState.dragInProgress,
+ modifier = Modifier.wrapContentSize()
+ ) { dragIsInProgress ->
+ EditGridHeader(Modifier.dragAndDropRemoveZone(dragAndDropState, onRemoveTile)) {
+ if (dragIsInProgress) {
+ RemoveTileTarget()
+ } else {
+ Text(text = "Hold and drag to rearrange tiles.")
+ }
+ }
+ }
+
+ CurrentTilesGrid(
+ currentTiles,
+ columns,
+ tilePadding,
+ isIconOnly,
+ onRemoveTile,
+ onDoubleTap,
+ dragAndDropState,
+ onDropAdd,
+ )
+
+ // Hide available tiles when dragging
+ AnimatedVisibility(
+ visible = !dragAndDropState.dragInProgress,
+ enter = fadeIn(),
+ exit = fadeOut()
+ ) {
+ Column(
+ verticalArrangement =
+ spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)),
+ modifier = modifier.fillMaxSize()
+ ) {
+ EditGridHeader { Text(text = "Hold and drag to add tiles.") }
+
+ AvailableTileGrid(
+ otherTiles,
+ columns,
+ tilePadding,
+ addTileToEnd,
+ dragAndDropState,
+ )
+ }
+ }
+
+ // Drop zone to remove tiles dragged out of the tile grid
+ Spacer(
+ modifier =
+ Modifier.fillMaxWidth()
+ .weight(1f)
+ .dragAndDropRemoveZone(dragAndDropState, onRemoveTile)
+ )
+ }
+ }
+}
+
+@Composable
+private fun EditGridHeader(
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit
+) {
+ CompositionLocalProvider(
+ LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f)
) {
- // These Text are just placeholders to see the different sections. Not final UI.
- item(span = { GridItemSpan(maxLineSpan) }) { Text("Current tiles", color = Color.White) }
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = modifier.fillMaxWidth().height(TileDefaults.EditGridHeaderHeight)
+ ) {
+ content()
+ }
+ }
+}
- editTiles(
- currentTiles,
- ClickAction.REMOVE,
- onRemoveTile,
- onDoubleTap,
- isIconOnly,
- indicatePosition = true,
- dragAndDropState = dragAndDropState,
- acceptDrops = { true },
- onDrop = onDropAdd,
- )
+@Composable
+private fun RemoveTileTarget() {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = tileHorizontalArrangement(),
+ modifier =
+ Modifier.fillMaxHeight()
+ .border(1.dp, LocalContentColor.current, shape = TileDefaults.TileShape)
+ .padding(10.dp)
+ ) {
+ Icon(imageVector = Icons.Default.Clear, contentDescription = null)
+ Text(text = "Remove")
+ }
+}
- item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) }
+@Composable
+private fun CurrentTilesContainer(content: @Composable () -> Unit) {
+ Box(
+ Modifier.fillMaxWidth()
+ .border(
+ width = 1.dp,
+ color = MaterialTheme.colorScheme.onBackground.copy(alpha = .5f),
+ shape = RoundedCornerShape(48.dp),
+ )
+ .padding(dimensionResource(R.dimen.qs_tile_margin_vertical))
+ ) {
+ content()
+ }
+}
+@Composable
+private fun CurrentTilesGrid(
+ tiles: List<EditTileViewModel>,
+ columns: Int,
+ tilePadding: Dp,
+ isIconOnly: (TileSpec) -> Boolean,
+ onClick: (TileSpec) -> Unit,
+ onDoubleTap: (TileSpec) -> Unit,
+ dragAndDropState: DragAndDropState,
+ onDrop: (TileSpec, Int) -> Unit
+) {
+ val tileHeight = tileHeight()
+ val currentRows =
+ remember(tiles) {
+ calculateRows(
+ tiles.map {
+ SizedTile(
+ it,
+ if (isIconOnly(it.tileSpec)) {
+ 1
+ } else {
+ 2
+ }
+ )
+ },
+ columns
+ )
+ }
+ val currentGridHeight = gridHeight(currentRows, tileHeight, tilePadding)
+ // Current tiles
+ CurrentTilesContainer {
+ TileLazyGrid(
+ modifier =
+ Modifier.height(currentGridHeight)
+ .dragAndDropTileList(dragAndDropState, { true }, onDrop),
+ columns = GridCells.Fixed(columns)
+ ) {
+ editTiles(
+ tiles,
+ ClickAction.REMOVE,
+ onClick,
+ isIconOnly,
+ dragAndDropState,
+ onDoubleTap = onDoubleTap,
+ indicatePosition = true,
+ acceptDrops = { true },
+ onDrop = onDrop,
+ )
+ }
+ }
+}
+
+@Composable
+private fun AvailableTileGrid(
+ tiles: List<EditTileViewModel>,
+ columns: Int,
+ tilePadding: Dp,
+ onClick: (TileSpec) -> Unit,
+ dragAndDropState: DragAndDropState,
+) {
+ val (otherTilesStock, otherTilesCustom) =
+ tiles.filter { !dragAndDropState.isMoving(it.tileSpec) }.partition { it.appName == null }
+ val availableTileHeight = tileHeight(true)
+ val availableGridHeight = gridHeight(tiles.size, availableTileHeight, columns, tilePadding)
+
+ // Available tiles
+ TileLazyGrid(
+ modifier =
+ Modifier.height(availableGridHeight)
+ .dragAndDropTileList(dragAndDropState, { false }, { _, _ -> }),
+ columns = GridCells.Fixed(columns)
+ ) {
editTiles(
otherTilesStock,
ClickAction.ADD,
- addTileToEnd,
- onDoubleTap,
- isIconOnly,
+ onClick,
+ isIconOnly = { true },
dragAndDropState = dragAndDropState,
- acceptDrops = { true },
- onDrop = onDropRemove,
+ acceptDrops = { false },
+ showLabels = true,
)
-
- item(span = { GridItemSpan(maxLineSpan) }) {
- Text("Custom tiles to add", color = Color.White)
- }
-
editTiles(
otherTilesCustom,
ClickAction.ADD,
- addTileToEnd,
- onDoubleTap,
- isIconOnly,
+ onClick,
+ isIconOnly = { true },
dragAndDropState = dragAndDropState,
- acceptDrops = { true },
- onDrop = onDropRemove,
+ acceptDrops = { false },
+ showLabels = true,
)
}
}
+fun gridHeight(nTiles: Int, tileHeight: Dp, columns: Int, padding: Dp): Dp {
+ val rows = (nTiles + columns - 1) / columns
+ return gridHeight(rows, tileHeight, padding)
+}
+
+fun gridHeight(rows: Int, tileHeight: Dp, padding: Dp): Dp {
+ return ((tileHeight + padding) * rows) - padding
+}
+
+private fun calculateRows(tiles: List<SizedTile<EditTileViewModel>>, columns: Int): Int {
+ val row = TileRow<EditTileViewModel>(columns)
+ var count = 0
+
+ for (tile in tiles) {
+ if (row.maybeAddTile(tile)) {
+ if (row.isFull()) {
+ // Row is full, no need to stretch tiles
+ count += 1
+ row.clear()
+ }
+ } else {
+ count += 1
+ row.clear()
+ row.maybeAddTile(tile)
+ }
+ }
+ if (row.tiles.isNotEmpty()) {
+ count += 1
+ }
+ return count
+}
+
fun LazyGridScope.editTiles(
tiles: List<EditTileViewModel>,
clickAction: ClickAction,
onClick: (TileSpec) -> Unit,
- onDoubleTap: (TileSpec) -> Unit,
isIconOnly: (TileSpec) -> Boolean,
dragAndDropState: DragAndDropState,
acceptDrops: (TileSpec) -> Boolean,
- onDrop: (TileSpec, Int) -> Unit,
+ onDoubleTap: (TileSpec) -> Unit = {},
+ onDrop: (TileSpec, Int) -> Unit = { _, _ -> },
showLabels: Boolean = false,
indicatePosition: Boolean = false,
) {
@@ -534,6 +733,7 @@
private object TileDefaults {
val TileShape = CircleShape
val IconTileWithLabelHeight = 140.dp
+ val EditGridHeaderHeight = 60.dp
@Composable
fun activeTileColors(): TileColors =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
index 62bfc72..ef2c8bf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
@@ -151,12 +151,27 @@
* present, it will be moved to the new position.
*/
fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) {
- // Removing tile if it's already present to insert it at the new index.
- if (currentTilesInteractor.currentTilesSpecs.contains(tileSpec)) {
- removeTile(tileSpec)
+ val specs = currentTilesInteractor.currentTilesSpecs.toMutableList()
+ val currentPosition = specs.indexOf(tileSpec)
+
+ if (currentPosition != -1) {
+ // No operation needed if the element is already in the list at the right position
+ if (currentPosition == position) {
+ return
+ }
+ // Removing tile if it's present at a different position to insert it at the new index.
+ specs.removeAt(currentPosition)
}
- currentTilesInteractor.addTile(tileSpec, position)
+ if (position >= 0 && position < specs.size) {
+ specs.add(position, tileSpec)
+ } else {
+ specs.add(tileSpec)
+ }
+
+ // Setting the new tiles as one operation to avoid UI jank with tiles disappearing and
+ // reappearing
+ currentTilesInteractor.setTiles(specs)
}
/** Immediately removes [tileSpec] from the current tiles. */
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index b3624ad..371707d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -72,6 +72,7 @@
import android.view.MotionEvent;
import android.view.Surface;
import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
@@ -302,10 +303,29 @@
public void onImeSwitcherPressed() {
// TODO(b/204901476) We're intentionally using the default display for now since
// Launcher/Taskbar isn't display aware.
+ if (Flags.imeSwitcherRevamp()) {
+ mContext.getSystemService(InputMethodManager.class)
+ .onImeSwitchButtonClickFromSystem(mDisplayTracker.getDefaultDisplayId());
+ } else {
+ mContext.getSystemService(InputMethodManager.class)
+ .showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */,
+ mDisplayTracker.getDefaultDisplayId());
+ }
+ mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
+ }
+
+ @Override
+ public void onImeSwitcherLongPress() {
+ if (!Flags.imeSwitcherRevamp()) {
+ return;
+ }
+ // TODO(b/204901476) We're intentionally using the default display for now since
+ // Launcher/Taskbar isn't display aware.
mContext.getSystemService(InputMethodManager.class)
.showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */,
mDisplayTracker.getDefaultDisplayId());
- mUiEventLogger.log(KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP);
+ mUiEventLogger.log(
+ KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index af1b6e1..46c5861 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -27,7 +27,6 @@
import android.os.CountDownTimer;
import android.os.Process;
import android.os.UserHandle;
-import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -60,8 +59,6 @@
@SysUISingleton
public class RecordingController
implements CallbackController<RecordingController.RecordingStateChangeCallback> {
- private static final String TAG = "RecordingController";
-
private boolean mIsStarting;
private boolean mIsRecording;
private PendingIntent mStopIntent;
@@ -71,6 +68,7 @@
private final BroadcastDispatcher mBroadcastDispatcher;
private final FeatureFlags mFlags;
private final UserTracker mUserTracker;
+ private final RecordingControllerLogger mRecordingControllerLogger;
private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger;
private final ScreenCaptureDisabledDialogDelegate mScreenCaptureDisabledDialogDelegate;
private final ScreenRecordDialogDelegate.Factory mScreenRecordDialogFactory;
@@ -102,9 +100,10 @@
if (intent != null && INTENT_UPDATE_STATE.equals(intent.getAction())) {
if (intent.hasExtra(EXTRA_STATE)) {
boolean state = intent.getBooleanExtra(EXTRA_STATE, false);
+ mRecordingControllerLogger.logIntentStateUpdated(state);
updateState(state);
} else {
- Log.e(TAG, "Received update intent with no state");
+ mRecordingControllerLogger.logIntentMissingState();
}
}
}
@@ -120,6 +119,7 @@
FeatureFlags flags,
Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver,
UserTracker userTracker,
+ RecordingControllerLogger recordingControllerLogger,
MediaProjectionMetricsLogger mediaProjectionMetricsLogger,
ScreenCaptureDisabledDialogDelegate screenCaptureDisabledDialogDelegate,
ScreenRecordDialogDelegate.Factory screenRecordDialogFactory,
@@ -130,6 +130,7 @@
mDevicePolicyResolver = devicePolicyResolver;
mBroadcastDispatcher = broadcastDispatcher;
mUserTracker = userTracker;
+ mRecordingControllerLogger = recordingControllerLogger;
mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger;
mScreenCaptureDisabledDialogDelegate = screenCaptureDisabledDialogDelegate;
mScreenRecordDialogFactory = screenRecordDialogFactory;
@@ -212,9 +213,9 @@
IntentFilter stateFilter = new IntentFilter(INTENT_UPDATE_STATE);
mBroadcastDispatcher.registerReceiver(mStateChangeReceiver, stateFilter, null,
UserHandle.ALL);
- Log.d(TAG, "sent start intent");
+ mRecordingControllerLogger.logSentStartIntent();
} catch (PendingIntent.CanceledException e) {
- Log.e(TAG, "Pending intent was cancelled: " + e.getMessage());
+ mRecordingControllerLogger.logPendingIntentCancelled(e);
}
}
};
@@ -227,9 +228,10 @@
*/
public void cancelCountdown() {
if (mCountDownTimer != null) {
+ mRecordingControllerLogger.logCountdownCancelled();
mCountDownTimer.cancel();
} else {
- Log.e(TAG, "Timer was null");
+ mRecordingControllerLogger.logCountdownCancelErrorNoTimer();
}
mIsStarting = false;
@@ -258,16 +260,16 @@
* Stop the recording
*/
public void stopRecording() {
- // TODO(b/332662551): Convert Logcat to LogBuffer.
try {
if (mStopIntent != null) {
+ mRecordingControllerLogger.logRecordingStopped();
mStopIntent.send(mInteractiveBroadcastOption);
} else {
- Log.e(TAG, "Stop intent was null");
+ mRecordingControllerLogger.logRecordingStopErrorNoStopIntent();
}
updateState(false);
} catch (PendingIntent.CanceledException e) {
- Log.e(TAG, "Error stopping: " + e.getMessage());
+ mRecordingControllerLogger.logRecordingStopError(e);
}
}
@@ -276,6 +278,7 @@
* @param isRecording
*/
public synchronized void updateState(boolean isRecording) {
+ mRecordingControllerLogger.logStateUpdated(isRecording);
if (!isRecording && mIsRecording) {
// Unregister receivers if we have stopped recording
mUserTracker.removeCallback(mUserChangedCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLog.kt
new file mode 100644
index 0000000..dd2baef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLog.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenrecord
+
+import javax.inject.Qualifier
+
+/**
+ * Logs for screen record events. See [com.android.systemui.screenrecord.RecordingController] and
+ * [com.android.systemui.screenrecord.RecordingControllerLogger].
+ */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class RecordingControllerLog
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLogger.kt
new file mode 100644
index 0000000..e16c010
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingControllerLogger.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.screenrecord
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import javax.inject.Inject
+
+/** Helper class for logging events to [RecordingControllerLog] from Java. */
+@SysUISingleton
+class RecordingControllerLogger
+@Inject
+constructor(
+ @RecordingControllerLog private val logger: LogBuffer,
+) {
+ fun logStateUpdated(isRecording: Boolean) =
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ { bool1 = isRecording },
+ { "Updating state. isRecording=$bool1" },
+ )
+
+ fun logIntentStateUpdated(isRecording: Boolean) =
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ { bool1 = isRecording },
+ { "Update intent has state. isRecording=$bool1" },
+ )
+
+ fun logIntentMissingState() =
+ logger.log(TAG, LogLevel.ERROR, {}, { "Received update intent with no state" })
+
+ fun logSentStartIntent() = logger.log(TAG, LogLevel.DEBUG, {}, { "Sent start intent" })
+
+ fun logPendingIntentCancelled(e: Exception) =
+ logger.log(TAG, LogLevel.ERROR, {}, { "Pending intent was cancelled" }, e)
+
+ fun logCountdownCancelled() =
+ logger.log(TAG, LogLevel.DEBUG, {}, { "Record countdown cancelled" })
+
+ fun logCountdownCancelErrorNoTimer() =
+ logger.log(TAG, LogLevel.ERROR, {}, { "Couldn't cancel countdown because timer was null" })
+
+ fun logRecordingStopped() = logger.log(TAG, LogLevel.DEBUG, {}, { "Stopping recording" })
+
+ fun logRecordingStopErrorNoStopIntent() =
+ logger.log(
+ TAG,
+ LogLevel.ERROR,
+ {},
+ { "Couldn't stop recording because stop intent was null" },
+ )
+
+ fun logRecordingStopError(e: Exception) =
+ logger.log(TAG, LogLevel.DEBUG, {}, { "Couldn't stop recording" }, e)
+
+ companion object {
+ private const val TAG = "RecordingController"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
index d7ddc50..a830e1b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
@@ -16,6 +16,9 @@
package com.android.systemui.screenrecord
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tileimpl.QSTileImpl
@@ -53,7 +56,7 @@
@IntoMap
@StringKey(SCREEN_RECORD_TILE_SPEC)
fun provideScreenRecordAvailabilityInteractor(
- impl: ScreenRecordTileDataInteractor
+ impl: ScreenRecordTileDataInteractor
): QSTileAvailabilityInteractor
companion object {
@@ -89,5 +92,12 @@
stateInteractor,
mapper,
)
+
+ @Provides
+ @SysUISingleton
+ @RecordingControllerLog
+ fun provideRecordingControllerLogBuffer(factory: LogBufferFactory): LogBuffer {
+ return factory.create("RecordingControllerLog", 50)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
index 994a0d0..7b82b56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
@@ -26,7 +26,6 @@
import android.media.projection.MediaProjectionManager;
import android.os.Handler;
import android.util.ArrayMap;
-import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -46,11 +45,11 @@
/** Platform implementation of the cast controller. **/
@SysUISingleton
public class CastControllerImpl implements CastController {
- public static final String TAG = "CastController";
- private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final String TAG = "CastController";
private final Context mContext;
private final PackageManager mPackageManager;
+ private final CastControllerLogger mLogger;
@GuardedBy("mCallbacks")
private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
private final MediaRouter mMediaRouter;
@@ -67,20 +66,22 @@
public CastControllerImpl(
Context context,
PackageManager packageManager,
- DumpManager dumpManager) {
+ DumpManager dumpManager,
+ CastControllerLogger logger) {
mContext = context;
mPackageManager = packageManager;
+ mLogger = logger;
mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
mMediaRouter.setRouterGroupId(MediaRouter.MIRRORING_GROUP_ID);
mProjectionManager = (MediaProjectionManager)
context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
mProjection = mProjectionManager.getActiveProjectionInfo();
mProjectionManager.addCallback(mProjectionCallback, new Handler());
- dumpManager.registerDumpable(TAG, this);
- if (DEBUG) Log.d(TAG, "new CastController()");
+ dumpManager.registerNormalDumpable(TAG, this);
}
- public void dump(PrintWriter pw, String[] args) {
+ @Override
+ public void dump(PrintWriter pw, @NonNull String[] args) {
pw.println("CastController state:");
pw.print(" mDiscovering="); pw.println(mDiscovering);
pw.print(" mCallbackRegistered="); pw.println(mCallbackRegistered);
@@ -88,7 +89,7 @@
pw.print(" mRoutes.size="); pw.println(mRoutes.size());
for (int i = 0; i < mRoutes.size(); i++) {
final RouteInfo route = mRoutes.valueAt(i);
- pw.print(" "); pw.println(routeToString(route));
+ pw.print(" "); pw.println(CastControllerLogger.Companion.toLogString(route));
}
pw.print(" mProjection="); pw.println(mProjection);
}
@@ -119,7 +120,7 @@
synchronized (mDiscoveringLock) {
if (mDiscovering == request) return;
mDiscovering = request;
- if (DEBUG) Log.d(TAG, "setDiscovering: " + request);
+ mLogger.logDiscovering(request);
handleDiscoveryChangeLocked();
}
}
@@ -166,7 +167,8 @@
CastDevice.Companion.toCastDevice(
mProjection,
mContext,
- mPackageManager));
+ mPackageManager,
+ mLogger));
}
}
@@ -177,7 +179,7 @@
public void startCasting(CastDevice device) {
if (device == null || device.getTag() == null) return;
final RouteInfo route = (RouteInfo) device.getTag();
- if (DEBUG) Log.d(TAG, "startCasting: " + routeToString(route));
+ mLogger.logStartCasting(route);
mMediaRouter.selectRoute(ROUTE_TYPE_REMOTE_DISPLAY, route);
}
@@ -185,15 +187,16 @@
public void stopCasting(CastDevice device) {
// TODO(b/332662551): Convert Logcat to LogBuffer.
final boolean isProjection = device.getTag() instanceof MediaProjectionInfo;
- if (DEBUG) Log.d(TAG, "stopCasting isProjection=" + isProjection);
+ mLogger.logStopCasting(isProjection);
if (isProjection) {
final MediaProjectionInfo projection = (MediaProjectionInfo) device.getTag();
if (Objects.equals(mProjectionManager.getActiveProjectionInfo(), projection)) {
mProjectionManager.stopActiveProjection();
} else {
- Log.w(TAG, "Projection is no longer active: " + projection);
+ mLogger.logStopCastingNoProjection(projection);
}
} else {
+ mLogger.logStopCastingMediaRouter();
mMediaRouter.getFallbackRoute().select();
}
}
@@ -218,7 +221,7 @@
}
}
if (changed) {
- if (DEBUG) Log.d(TAG, "setProjection: " + oldProjection + " -> " + mProjection);
+ mLogger.logSetProjection(oldProjection, mProjection);
fireOnCastDevicesChanged();
}
}
@@ -265,42 +268,30 @@
callback.onCastDevicesChanged();
}
- private static String routeToString(RouteInfo route) {
- if (route == null) return null;
- final StringBuilder sb = new StringBuilder().append(route.getName()).append('/')
- .append(route.getDescription()).append('@').append(route.getDeviceAddress())
- .append(",status=").append(route.getStatus());
- if (route.isDefault()) sb.append(",default");
- if (route.isEnabled()) sb.append(",enabled");
- if (route.isConnecting()) sb.append(",connecting");
- if (route.isSelected()) sb.append(",selected");
- return sb.append(",id=").append(route.getTag()).toString();
- }
-
private final MediaRouter.SimpleCallback mMediaCallback = new MediaRouter.SimpleCallback() {
@Override
public void onRouteAdded(MediaRouter router, RouteInfo route) {
- if (DEBUG) Log.d(TAG, "onRouteAdded: " + routeToString(route));
+ mLogger.logRouteAdded(route);
updateRemoteDisplays();
}
@Override
public void onRouteChanged(MediaRouter router, RouteInfo route) {
- if (DEBUG) Log.d(TAG, "onRouteChanged: " + routeToString(route));
+ mLogger.logRouteChanged(route);
updateRemoteDisplays();
}
@Override
public void onRouteRemoved(MediaRouter router, RouteInfo route) {
- if (DEBUG) Log.d(TAG, "onRouteRemoved: " + routeToString(route));
+ mLogger.logRouteRemoved(route);
updateRemoteDisplays();
}
@Override
public void onRouteSelected(MediaRouter router, int type, RouteInfo route) {
- if (DEBUG) Log.d(TAG, "onRouteSelected(" + type + "): " + routeToString(route));
+ mLogger.logRouteSelected(route, type);
updateRemoteDisplays();
}
@Override
public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
- if (DEBUG) Log.d(TAG, "onRouteUnselected(" + type + "): " + routeToString(route));
+ mLogger.logRouteUnselected(route, type);
updateRemoteDisplays();
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerLogger.kt
new file mode 100644
index 0000000..9a3a244
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerLogger.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.statusbar.policy
+
+import android.media.MediaRouter.RouteInfo
+import android.media.projection.MediaProjectionInfo
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.MessageInitializer
+import com.android.systemui.log.core.MessagePrinter
+import com.android.systemui.statusbar.policy.dagger.CastControllerLog
+import javax.inject.Inject
+
+/** Helper class for logging events to [CastControllerLog] from Java. */
+@SysUISingleton
+class CastControllerLogger
+@Inject
+constructor(
+ @CastControllerLog val logger: LogBuffer,
+) {
+ /** Passthrough to [logger]. */
+ inline fun log(
+ tag: String,
+ level: LogLevel,
+ messageInitializer: MessageInitializer,
+ noinline messagePrinter: MessagePrinter,
+ exception: Throwable? = null,
+ ) {
+ logger.log(tag, level, messageInitializer, messagePrinter, exception)
+ }
+
+ fun logDiscovering(isDiscovering: Boolean) =
+ logger.log(TAG, LogLevel.DEBUG, { bool1 = isDiscovering }, { "setDiscovering: $bool1" })
+
+ fun logStartCasting(route: RouteInfo) =
+ logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "startCasting: $str1" })
+
+ fun logStopCasting(isProjection: Boolean) =
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ { bool1 = isProjection },
+ { "stopCasting. isProjection=$bool1" },
+ )
+
+ fun logStopCastingNoProjection(projection: MediaProjectionInfo) =
+ logger.log(
+ TAG,
+ LogLevel.WARNING,
+ { str1 = projection.toString() },
+ { "stopCasting failed because projection is no longer active: $str1" },
+ )
+
+ fun logStopCastingMediaRouter() =
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ {},
+ { "stopCasting is selecting fallback route in MediaRouter" },
+ )
+
+ fun logSetProjection(oldInfo: MediaProjectionInfo?, newInfo: MediaProjectionInfo?) =
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = oldInfo.toString()
+ str2 = newInfo.toString()
+ },
+ { "setProjection: $str1 -> $str2" },
+ )
+
+ fun logRouteAdded(route: RouteInfo) =
+ logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "onRouteAdded: $str1" })
+
+ fun logRouteChanged(route: RouteInfo) =
+ logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "onRouteChanged: $str1" })
+
+ fun logRouteRemoved(route: RouteInfo) =
+ logger.log(TAG, LogLevel.DEBUG, { str1 = route.toLogString() }, { "onRouteRemoved: $str1" })
+
+ fun logRouteSelected(route: RouteInfo, type: Int) =
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = route.toLogString()
+ int1 = type
+ },
+ { "onRouteSelected($int1): $str1" },
+ )
+
+ fun logRouteUnselected(route: RouteInfo, type: Int) =
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = route.toLogString()
+ int1 = type
+ },
+ { "onRouteUnselected($int1): $str1" },
+ )
+
+ companion object {
+ @JvmStatic
+ fun RouteInfo?.toLogString(): String? {
+ if (this == null) return null
+ val sb =
+ StringBuilder()
+ .append(name)
+ .append('/')
+ .append(description)
+ .append('@')
+ .append(deviceAddress)
+ .append(",status=")
+ .append(status)
+ if (isDefault) sb.append(",default")
+ if (isEnabled) sb.append(",enabled")
+ if (isConnecting) sb.append(",connecting")
+ if (isSelected) sb.append(",selected")
+ return sb.append(",id=").append(this.tag).toString()
+ }
+
+ private const val TAG = "CastController"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt
index 68edd75..a787f7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastDevice.kt
@@ -20,7 +20,7 @@
import android.media.MediaRouter
import android.media.projection.MediaProjectionInfo
import android.text.TextUtils
-import android.util.Log
+import com.android.systemui.log.core.LogLevel
import com.android.systemui.res.R
import com.android.systemui.util.Utils
@@ -64,11 +64,12 @@
/** Creates a [CastDevice] based on the provided information from MediaProjection. */
fun MediaProjectionInfo.toCastDevice(
context: Context,
- packageManager: PackageManager
+ packageManager: PackageManager,
+ logger: CastControllerLogger,
): CastDevice {
return CastDevice(
id = this.packageName,
- name = getAppName(this.packageName, packageManager),
+ name = getAppName(this.packageName, packageManager, logger),
description = context.getString(R.string.quick_settings_casting),
state = CastState.Connected,
tag = this,
@@ -76,7 +77,11 @@
)
}
- private fun getAppName(packageName: String, packageManager: PackageManager): String {
+ private fun getAppName(
+ packageName: String,
+ packageManager: PackageManager,
+ logger: CastControllerLogger,
+ ): String {
if (Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)) {
return ""
}
@@ -86,9 +91,20 @@
if (!TextUtils.isEmpty(label)) {
return label.toString()
}
- Log.w(CastControllerImpl.TAG, "No label found for package: $packageName")
+ logger.log(
+ "#getAppName",
+ LogLevel.WARNING,
+ { str1 = packageName },
+ { "No label found for package: $str1" },
+ )
} catch (e: PackageManager.NameNotFoundException) {
- Log.w(CastControllerImpl.TAG, "Error getting appName for package: $packageName", e)
+ logger.log(
+ "#getAppName",
+ LogLevel.WARNING,
+ { str1 = packageName },
+ { "Error getting appName for package=$str1" },
+ e,
+ )
}
return packageName
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/CastControllerLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/CastControllerLog.kt
new file mode 100644
index 0000000..23aade6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/CastControllerLog.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.dagger
+
+import javax.inject.Qualifier
+
+/**
+ * Logs for cast events. See [com.android.systemui.statusbar.policy.CastControllerImpl] and
+ * [com.android.systemui.statusbar.policy.CastControllerLogger].
+ */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class CastControllerLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index e08e4d7..71bcdfcb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -228,4 +228,12 @@
static LogBuffer provideBatteryControllerLog(LogBufferFactory factory) {
return factory.create(BatteryControllerLogger.TAG, 30);
}
+
+ /** Provides a log buffer for CastControllerImpl */
+ @Provides
+ @SysUISingleton
+ @CastControllerLog
+ static LogBuffer provideCastControllerLog(LogBufferFactory factory) {
+ return factory.create("CastControllerLog", 50);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
index e2cca38..ae635b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java
@@ -41,7 +41,6 @@
import android.os.Handler;
import android.os.UserManager;
import android.provider.Settings;
-import android.service.dreams.IDreamManager;
import android.testing.TestableLooper;
import android.view.GestureDetector;
import android.view.IWindowManager;
@@ -106,7 +105,6 @@
@Mock private GlobalActions.GlobalActionsManager mWindowManagerFuncs;
@Mock private AudioManager mAudioManager;
- @Mock private IDreamManager mDreamManager;
@Mock private DevicePolicyManager mDevicePolicyManager;
@Mock private LockPatternUtils mLockPatternUtils;
@Mock private BroadcastDispatcher mBroadcastDispatcher;
@@ -165,7 +163,6 @@
mGlobalActionsDialogLite = new GlobalActionsDialogLite(mContext,
mWindowManagerFuncs,
mAudioManager,
- mDreamManager,
mDevicePolicyManager,
mLockPatternUtils,
mBroadcastDispatcher,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index 5db8981..785d5a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -30,6 +30,7 @@
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
@@ -273,6 +274,7 @@
applicationScope = kosmos.applicationCoroutineScope,
backgroundDispatcher = kosmos.testDispatcher,
mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
+ logger = logcatLogBuffer("TestMediaProjection"),
)
val state by collectLastValue(repoWithTimingControl.mediaProjectionState)
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 98ff6c9..45d77f6 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
@@ -29,6 +29,8 @@
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.HOME_BUTTON_LONG_PRESS_DURATION_MS;
import static com.android.systemui.assist.AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
import static com.android.systemui.navigationbar.views.NavigationBar.NavBarActionEvent.NAVBAR_ASSIST_LONGPRESS;
+import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS;
+import static com.android.systemui.navigationbar.views.buttons.KeyButtonView.NavBarButtonEvent.NAVBAR_IME_SWITCHER_BUTTON_TAP;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.google.common.truth.Truth.assertThat;
@@ -38,6 +40,7 @@
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
@@ -70,6 +73,7 @@
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityManager;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -162,6 +166,8 @@
@Mock
ButtonDispatcher mImeSwitchButton;
@Mock
+ KeyButtonView mImeSwitchButtonView;
+ @Mock
ButtonDispatcher mBackButton;
@Mock
NavigationBarTransitions mNavigationBarTransitions;
@@ -433,6 +439,45 @@
}
@Test
+ public void testImeSwitcherClick() {
+ mNavigationBar.init();
+ mNavigationBar.onViewAttached();
+ mNavigationBar.onImeSwitcherClick(mImeSwitchButtonView);
+
+ verify(mUiEventLogger).log(NAVBAR_IME_SWITCHER_BUTTON_TAP);
+ verify(mUiEventLogger, never()).log(NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+ if (Flags.imeSwitcherRevamp()) {
+ verify(mInputMethodManager)
+ .onImeSwitchButtonClickFromSystem(mNavigationBar.mDisplayId);
+ verify(mInputMethodManager, never()).showInputMethodPickerFromSystem(
+ anyBoolean() /* showAuxiliarySubtypes */, anyInt() /* displayId */);
+ } else {
+ verify(mInputMethodManager, never())
+ .onImeSwitchButtonClickFromSystem(anyInt() /* displayId */);
+ verify(mInputMethodManager).showInputMethodPickerFromSystem(
+ true /* showAuxiliarySubtypes */, mNavigationBar.mDisplayId);
+ }
+ }
+
+ @Test
+ public void testImeSwitcherLongClick() {
+ mNavigationBar.init();
+ mNavigationBar.onViewAttached();
+ mNavigationBar.onImeSwitcherLongClick(mImeSwitchButtonView);
+
+ verify(mUiEventLogger, never()).log(NAVBAR_IME_SWITCHER_BUTTON_TAP);
+ if (Flags.imeSwitcherRevamp()) {
+ verify(mUiEventLogger).log(NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+ verify(mInputMethodManager).showInputMethodPickerFromSystem(
+ true /* showAuxiliarySubtypes */, mNavigationBar.mDisplayId);
+ } else {
+ verify(mUiEventLogger, never()).log(NAVBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+ verify(mInputMethodManager, never()).showInputMethodPickerFromSystem(
+ anyBoolean() /* showAuxiliarySubtypes */, anyInt() /* displayId */);
+ }
+ }
+
+ @Test
public void testRegisteredWithUserTracker() {
mNavigationBar.init();
mNavigationBar.onViewAttached();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index 2444af7..477c50b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -18,6 +18,8 @@
import static android.os.Process.myUid;
+import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
+
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertFalse;
@@ -69,10 +71,6 @@
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
-/**
- * Tests for exception handling and bitmap configuration in adding smart actions to Screenshot
- * Notification.
- */
public class RecordingControllerTest extends SysuiTestCase {
private static final int TEST_USER_ID = 12345;
@@ -146,6 +144,7 @@
mFeatureFlags,
() -> mDevicePolicyResolver,
mUserTracker,
+ new RecordingControllerLogger(logcatLogBuffer("RecordingControllerTest")),
mMediaProjectionMetricsLogger,
mScreenCaptureDisabledDialogDelegate,
mScreenRecordDialogFactory,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java
index 59b20c8..627463b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastControllerImplTest.java
@@ -1,6 +1,8 @@
package com.android.systemui.statusbar.policy;
+import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
+
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
@@ -58,7 +60,8 @@
mController = new CastControllerImpl(
mContext,
mock(PackageManager.class),
- mock(DumpManager.class));
+ mock(DumpManager.class),
+ new CastControllerLogger(logcatLogBuffer("CastControllerImplTest")));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
index 03ad66c..16061df 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
@@ -25,6 +25,7 @@
import android.media.projection.MediaProjectionInfo
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.CastDevice.Companion.toCastDevice
import com.google.common.truth.Truth.assertThat
@@ -40,6 +41,7 @@
class CastDeviceTest : SysuiTestCase() {
private val mockAppInfo =
mock<ApplicationInfo>().apply { whenever(this.loadLabel(any())).thenReturn("") }
+ private val logger = CastControllerLogger(logcatLogBuffer("CastDeviceTest"))
private val packageManager =
mock<PackageManager>().apply {
@@ -322,7 +324,7 @@
whenever(this.packageName).thenReturn("fake.package")
}
- val device = projection.toCastDevice(context, packageManager)
+ val device = projection.toCastDevice(context, packageManager, logger)
assertThat(device.id).isEqualTo("fake.package")
}
@@ -334,7 +336,7 @@
whenever(this.packageName).thenReturn(HEADLESS_REMOTE_PACKAGE)
}
- val device = projection.toCastDevice(context, packageManager)
+ val device = projection.toCastDevice(context, packageManager, logger)
assertThat(device.name).isEmpty()
}
@@ -349,7 +351,7 @@
whenever(packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>()))
.thenThrow(PackageManager.NameNotFoundException())
- val device = projection.toCastDevice(context, packageManager)
+ val device = projection.toCastDevice(context, packageManager, logger)
assertThat(device.name).isEqualTo(NORMAL_PACKAGE)
}
@@ -366,7 +368,7 @@
whenever(packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>()))
.thenReturn(appInfo)
- val device = projection.toCastDevice(context, packageManager)
+ val device = projection.toCastDevice(context, packageManager, logger)
assertThat(device.name).isEqualTo(NORMAL_PACKAGE)
}
@@ -383,7 +385,7 @@
whenever(packageManager.getApplicationInfo(eq(NORMAL_PACKAGE), any<Int>()))
.thenReturn(appInfo)
- val device = projection.toCastDevice(context, packageManager)
+ val device = projection.toCastDevice(context, packageManager, logger)
assertThat(device.name).isEqualTo("Valid App Name")
}
@@ -392,7 +394,7 @@
fun projectionToCastDevice_descriptionIsCasting() {
val projection = mockProjectionInfo()
- val device = projection.toCastDevice(context, packageManager)
+ val device = projection.toCastDevice(context, packageManager, logger)
assertThat(device.description).isEqualTo(context.getString(R.string.quick_settings_casting))
}
@@ -401,7 +403,7 @@
fun projectionToCastDevice_stateIsConnected() {
val projection = mockProjectionInfo()
- val device = projection.toCastDevice(context, packageManager)
+ val device = projection.toCastDevice(context, packageManager, logger)
assertThat(device.state).isEqualTo(CastDevice.CastState.Connected)
}
@@ -410,7 +412,7 @@
fun projectionToCastDevice_tagIsProjection() {
val projection = mockProjectionInfo()
- val device = projection.toCastDevice(context, packageManager)
+ val device = projection.toCastDevice(context, packageManager, logger)
assertThat(device.tag).isEqualTo(projection)
}
@@ -419,7 +421,7 @@
fun projectionToCastDevice_originIsMediaProjection() {
val projection = mockProjectionInfo()
- val device = projection.toCastDevice(context, packageManager)
+ val device = projection.toCastDevice(context, packageManager, logger)
assertThat(device.origin).isEqualTo(CastDevice.CastOrigin.MediaProjection)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt
index 81ba77a..0412274 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepositoryKosmos.kt
@@ -21,6 +21,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.mediaprojection.taskswitcher.activityTaskManagerTasksRepository
import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
@@ -37,5 +38,6 @@
tasksRepository = activityTaskManagerTasksRepository,
backgroundDispatcher = testDispatcher,
mediaProjectionServiceHelper = fakeMediaProjectionManager.helper,
+ logger = logcatLogBuffer("TestMediaProjection"),
)
}
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
index 3f28c47..a7280e6 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java
@@ -138,6 +138,9 @@
@PermissionVerified(Manifest.permission.TEST_INPUT_METHOD)
boolean isInputMethodPickerShownForTest();
+ @PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+ void onImeSwitchButtonClickFromSystem(int displayId);
+
InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId);
void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes,
@@ -344,6 +347,14 @@
return mCallback.isInputMethodPickerShownForTest();
}
+ @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+ @Override
+ public void onImeSwitchButtonClickFromSystem(int displayId) {
+ super.onImeSwitchButtonClickFromSystem_enforcePermission();
+
+ mCallback.onImeSwitchButtonClickFromSystem(displayId);
+ }
+
@Override
public InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) {
return mCallback.getCurrentInputMethodSubtype(userId);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index f5faeef..c093c22 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -53,6 +53,7 @@
import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO;
import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed;
import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -3967,6 +3968,29 @@
}
}
+ @BinderThread
+ private void onImeSwitchButtonClickFromClient(@NonNull IBinder token, int displayId,
+ @UserIdInt int userId) {
+ userId = mActivityManagerInternal.handleIncomingUser(
+ Binder.getCallingPid(), Binder.getCallingUid(), userId, false,
+ ActivityManagerInternal.ALLOW_FULL_ONLY, "onImeSwitchButtonClickFromClient", null);
+
+ synchronized (ImfLock.class) {
+ if (!calledWithValidTokenLocked(token, userId)) {
+ return;
+ }
+ showInputMethodPickerFromSystem(
+ InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId);
+ }
+ }
+
+ @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+ @Override
+ public void onImeSwitchButtonClickFromSystem(int displayId) {
+ showInputMethodPickerFromSystem(
+ InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES, displayId);
+ }
+
@NonNull
private static IllegalArgumentException getExceptionForUnknownImeId(
@Nullable String imeId) {
@@ -4112,7 +4136,8 @@
final var currentImi = bindingController.getSelectedMethod();
final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController
.getNextInputMethodLocked(onlyCurrentIme, currentImi,
- bindingController.getCurrentSubtype());
+ bindingController.getCurrentSubtype(),
+ MODE_AUTO, true /* forward */);
if (nextSubtype == null) {
return false;
}
@@ -4132,7 +4157,8 @@
final var currentImi = bindingController.getSelectedMethod();
final ImeSubtypeListItem nextSubtype = getUserData(userId).mSwitchingController
.getNextInputMethodLocked(false /* onlyCurrentIme */, currentImi,
- bindingController.getCurrentSubtype());
+ bindingController.getCurrentSubtype(),
+ MODE_AUTO, true /* forward */);
return nextSubtype != null;
}
}
@@ -5457,6 +5483,10 @@
// Set InputMethod here
settings.putSelectedInputMethod(imi != null ? imi.getId() : "");
}
+
+ if (Flags.imeSwitcherRevamp()) {
+ getUserData(userId).mSwitchingController.onInputMethodSubtypeChanged();
+ }
}
@GuardedBy("ImfLock.class")
@@ -5577,11 +5607,29 @@
if (currentImi == null) {
return;
}
- final InputMethodSubtypeHandle currentSubtypeHandle =
- InputMethodSubtypeHandle.of(currentImi, bindingController.getCurrentSubtype());
- final InputMethodSubtypeHandle nextSubtypeHandle =
- getUserData(userId).mHardwareKeyboardShortcutController.onSubtypeSwitch(
+ final var currentSubtype = bindingController.getCurrentSubtype();
+ final InputMethodSubtypeHandle nextSubtypeHandle;
+ if (Flags.imeSwitcherRevamp()) {
+ final var nextItem = getUserData(userId).mSwitchingController
+ .getNextInputMethodForHardware(
+ false /* onlyCurrentIme */, currentImi, currentSubtype, MODE_AUTO,
+ direction > 0 /* forward */);
+ if (nextItem == null) {
+ Slog.i(TAG, "Hardware keyboard switching shortcut,"
+ + " next input method and subtype not found");
+ return;
+ }
+
+ final var nextSubtype = nextItem.mSubtypeId > NOT_A_SUBTYPE_ID
+ ? nextItem.mImi.getSubtypeAt(nextItem.mSubtypeId) : null;
+ nextSubtypeHandle = InputMethodSubtypeHandle.of(nextItem.mImi, nextSubtype);
+ } else {
+ final InputMethodSubtypeHandle currentSubtypeHandle =
+ InputMethodSubtypeHandle.of(currentImi, currentSubtype);
+ nextSubtypeHandle =
+ getUserData(userId).mHardwareKeyboardShortcutController.onSubtypeSwitch(
currentSubtypeHandle, direction > 0);
+ }
if (nextSubtypeHandle == null) {
return;
}
@@ -6905,6 +6953,12 @@
@BinderThread
@Override
+ public void onImeSwitchButtonClickFromClient(int displayId, @UserIdInt int userId) {
+ mImms.onImeSwitchButtonClickFromClient(mToken, displayId, userId);
+ }
+
+ @BinderThread
+ @Override
public void notifyUserActionAsync() {
mImms.notifyUserAction(mToken, mUserId);
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index bb1b9df..05cc598 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -16,6 +16,8 @@
package com.android.server.inputmethod;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -24,11 +26,14 @@
import android.util.ArraySet;
import android.util.Printer;
import android.util.Slog;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.VisibleForTesting;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -45,6 +50,34 @@
private static final boolean DEBUG = false;
private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
+ @IntDef(prefix = {"MODE_"}, value = {
+ MODE_STATIC,
+ MODE_RECENT,
+ MODE_AUTO
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SwitchMode {
+ }
+
+ /**
+ * Switch using the static order (the order of the given list of input methods and subtypes).
+ * This order is only set when given a new list, and never updated.
+ */
+ public static final int MODE_STATIC = 0;
+
+ /**
+ * Switch using the recency based order, going from most recent to least recent,
+ * updated on {@link #onUserActionLocked user action}.
+ */
+ public static final int MODE_RECENT = 1;
+
+ /**
+ * If there was a {@link #onUserActionLocked user action} since the last
+ * {@link #onInputMethodSubtypeChanged() switch}, and direction is forward,
+ * use {@link #MODE_RECENT}, otherwise use {@link #MODE_STATIC}.
+ */
+ public static final int MODE_AUTO = 2;
+
public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> {
@NonNull
@@ -117,20 +150,25 @@
if (result != 0) {
return result;
}
- // Subtype that has the same locale of the system's has higher priority.
- result = (mIsSystemLocale ? -1 : 0) - (other.mIsSystemLocale ? -1 : 0);
- if (result != 0) {
- return result;
+ if (!Flags.imeSwitcherRevamp()) {
+ // Subtype that has the same locale of the system's has higher priority.
+ result = (mIsSystemLocale ? -1 : 0) - (other.mIsSystemLocale ? -1 : 0);
+ if (result != 0) {
+ return result;
+ }
+ // Subtype that has the same language of the system's has higher priority.
+ result = (mIsSystemLanguage ? -1 : 0) - (other.mIsSystemLanguage ? -1 : 0);
+ if (result != 0) {
+ return result;
+ }
+ result = compareNullableCharSequences(mSubtypeName, other.mSubtypeName);
+ if (result != 0) {
+ return result;
+ }
}
- // Subtype that has the same language of the system's has higher priority.
- result = (mIsSystemLanguage ? -1 : 0) - (other.mIsSystemLanguage ? -1 : 0);
- if (result != 0) {
- return result;
- }
- result = compareNullableCharSequences(mSubtypeName, other.mSubtypeName);
- if (result != 0) {
- return result;
- }
+ // This will no longer compare by subtype name, however as {@link Collections.sort} is
+ // guaranteed to be a stable sorting, this allows sorting by the IME name (and ID),
+ // while maintaining the order of subtypes (given by each IME) at the IME level.
return mImi.getId().compareTo(other.mImi.getId());
}
@@ -226,6 +264,59 @@
return imList;
}
+ @NonNull
+ private static List<ImeSubtypeListItem> getInputMethodAndSubtypeListForHardwareKeyboard(
+ @NonNull Context context, @NonNull InputMethodSettings settings) {
+ if (!Flags.imeSwitcherRevamp()) {
+ return new ArrayList<>();
+ }
+ final int userId = settings.getUserId();
+ final Context userAwareContext = context.getUserId() == userId
+ ? context
+ : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
+ final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag();
+
+ final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList();
+ if (imis.isEmpty()) {
+ Slog.w(TAG, "Enabled input method list is empty.");
+ return new ArrayList<>();
+ }
+
+ final ArrayList<ImeSubtypeListItem> imList = new ArrayList<>();
+ final int numImes = imis.size();
+ for (int i = 0; i < numImes; ++i) {
+ final InputMethodInfo imi = imis.get(i);
+ if (!imi.shouldShowInInputMethodPicker()) {
+ continue;
+ }
+ final var subtypes = settings.getEnabledInputMethodSubtypeList(imi, true);
+ final ArraySet<InputMethodSubtype> enabledSubtypeSet = new ArraySet<>(subtypes);
+ final CharSequence imeLabel = imi.loadLabel(userAwareContext.getPackageManager());
+ if (!subtypes.isEmpty()) {
+ final int subtypeCount = imi.getSubtypeCount();
+ if (DEBUG) {
+ Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId());
+ }
+ for (int j = 0; j < subtypeCount; j++) {
+ final InputMethodSubtype subtype = imi.getSubtypeAt(j);
+ if (enabledSubtypeSet.contains(subtype)
+ && subtype.isSuitableForPhysicalKeyboardLayoutMapping()) {
+ final CharSequence subtypeLabel =
+ subtype.overridesImplicitlyEnabledSubtype() ? null : subtype
+ .getDisplayName(userAwareContext, imi.getPackageName(),
+ imi.getServiceInfo().applicationInfo);
+ imList.add(new ImeSubtypeListItem(imeLabel,
+ subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr));
+ }
+ }
+ } else {
+ imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null,
+ mSystemLocaleStr));
+ }
+ }
+ return imList;
+ }
+
private static int calculateSubtypeId(@NonNull InputMethodInfo imi,
@Nullable InputMethodSubtype subtype) {
return subtype != null ? SubtypeUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode())
@@ -385,6 +476,132 @@
}
}
+ /**
+ * List container that allows getting the next item in either forwards or backwards direction,
+ * in either static or recency order, and either in the same IME or not.
+ */
+ private static class RotationList {
+
+ /**
+ * List of items in a static order.
+ */
+ @NonNull
+ private final List<ImeSubtypeListItem> mItems;
+
+ /**
+ * Mapping of recency index to static index (in {@link #mItems}), with lower indices being
+ * more recent.
+ */
+ @NonNull
+ private final int[] mRecencyMap;
+
+ RotationList(@NonNull List<ImeSubtypeListItem> items) {
+ mItems = items;
+ mRecencyMap = new int[items.size()];
+ for (int i = 0; i < mItems.size(); i++) {
+ mRecencyMap[i] = i;
+ }
+ }
+
+ /**
+ * Gets the next input method and subtype from the given ones.
+ *
+ * @param imi the input method to find the next value from.
+ * @param subtype the input method subtype to find the next value from, if any.
+ * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+ * @param useRecency whether to use the recency order, or the static order.
+ * @param forward whether to search forwards to backwards in the list.
+ * @return the next input method and subtype if found, otherwise {@code null}.
+ */
+ @Nullable
+ public ImeSubtypeListItem next(@NonNull InputMethodInfo imi,
+ @Nullable InputMethodSubtype subtype, boolean onlyCurrentIme,
+ boolean useRecency, boolean forward) {
+ final int size = mItems.size();
+ if (size <= 1) {
+ return null;
+ }
+ final int index = getIndex(imi, subtype, useRecency);
+ if (index < 0) {
+ return null;
+ }
+
+ final int incrementSign = (forward ? 1 : -1);
+
+ for (int i = 1; i < size; i++) {
+ final int nextIndex = (index + i * incrementSign + size) % size;
+ final int mappedIndex = useRecency ? mRecencyMap[nextIndex] : nextIndex;
+ final var nextItem = mItems.get(mappedIndex);
+ if (!onlyCurrentIme || nextItem.mImi.equals(imi)) {
+ return nextItem;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the given input method and subtype as the most recent one.
+ *
+ * @param imi the input method to set as the most recent.
+ * @param subtype the input method subtype to set as the most recent, if any.
+ * @return {@code true} if the recency was updated, otherwise {@code false}.
+ */
+ public boolean setMostRecent(@NonNull InputMethodInfo imi,
+ @Nullable InputMethodSubtype subtype) {
+ if (mItems.size() <= 1) {
+ return false;
+ }
+
+ final int recencyIndex = getIndex(imi, subtype, true /* useRecency */);
+ if (recencyIndex <= 0) {
+ // Already most recent or not found.
+ return false;
+ }
+ final int staticIndex = mRecencyMap[recencyIndex];
+ System.arraycopy(mRecencyMap, 0, mRecencyMap, 1, recencyIndex);
+ mRecencyMap[0] = staticIndex;
+ return true;
+ }
+
+ /**
+ * Gets the index of the given input method and subtype, in either recency or static order.
+ *
+ * @param imi the input method to get the index of.
+ * @param subtype the input method subtype to get the index of, if any.
+ * @param useRecency whether to get the index in the recency or static order.
+ * @return an index in either {@link #mItems} or {@link #mRecencyMap}, or {@code -1}
+ * if not found.
+ */
+ @IntRange(from = -1)
+ private int getIndex(@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+ boolean useRecency) {
+ final int subtypeIndex = calculateSubtypeId(imi, subtype);
+ for (int i = 0; i < mItems.size(); i++) {
+ final int mappedIndex = useRecency ? mRecencyMap[i] : i;
+ final var item = mItems.get(mappedIndex);
+ if (item.mImi.equals(imi) && item.mSubtypeId == subtypeIndex) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /** Dumps the state of the list into the given printer. */
+ private void dump(@NonNull Printer pw, @NonNull String prefix) {
+ pw.println(prefix + "Static order:");
+ for (int i = 0; i < mItems.size(); ++i) {
+ final var item = mItems.get(i);
+ pw.println(prefix + "i=" + i + " item=" + item);
+ }
+ pw.println(prefix + "Recency order:");
+ for (int i = 0; i < mRecencyMap.length; ++i) {
+ final int index = mRecencyMap[i];
+ final var item = mItems.get(index);
+ pw.println(prefix + "i=" + i + " item=" + item);
+ }
+ }
+ }
+
@VisibleForTesting
public static class ControllerImpl {
@@ -392,10 +609,23 @@
private final DynamicRotationList mSwitchingAwareRotationList;
@NonNull
private final StaticRotationList mSwitchingUnawareRotationList;
+ /** List of input methods and subtypes. */
+ @Nullable
+ private final RotationList mRotationList;
+ /** List of input methods and subtypes suitable for hardware keyboards. */
+ @Nullable
+ private final RotationList mHardwareRotationList;
+
+ /**
+ * Whether there was a user action since the last input method and subtype switch.
+ * Used to determine the switching behaviour for {@link #MODE_AUTO}.
+ */
+ private boolean mUserActionSinceSwitch;
@NonNull
public static ControllerImpl createFrom(@Nullable ControllerImpl currentInstance,
- @NonNull List<ImeSubtypeListItem> sortedEnabledItems) {
+ @NonNull List<ImeSubtypeListItem> sortedEnabledItems,
+ @NonNull List<ImeSubtypeListItem> hardwareKeyboardItems) {
final var switchingAwareImeSubtypes = filterImeSubtypeList(sortedEnabledItems,
true /* supportsSwitchingToNextInputMethod */);
final var switchingUnawareImeSubtypes = filterImeSubtypeList(sortedEnabledItems,
@@ -421,22 +651,55 @@
switchingUnawareRotationList = new StaticRotationList(switchingUnawareImeSubtypes);
}
- return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList);
+ final RotationList rotationList;
+ if (!Flags.imeSwitcherRevamp()) {
+ rotationList = null;
+ } else if (currentInstance != null && currentInstance.mRotationList != null
+ && Objects.equals(
+ currentInstance.mRotationList.mItems, sortedEnabledItems)) {
+ // Can reuse the current instance.
+ rotationList = currentInstance.mRotationList;
+ } else {
+ rotationList = new RotationList(sortedEnabledItems);
+ }
+
+ final RotationList hardwareRotationList;
+ if (!Flags.imeSwitcherRevamp()) {
+ hardwareRotationList = null;
+ } else if (currentInstance != null && currentInstance.mHardwareRotationList != null
+ && Objects.equals(
+ currentInstance.mHardwareRotationList.mItems, hardwareKeyboardItems)) {
+ // Can reuse the current instance.
+ hardwareRotationList = currentInstance.mHardwareRotationList;
+ } else {
+ hardwareRotationList = new RotationList(hardwareKeyboardItems);
+ }
+
+ return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList,
+ rotationList, hardwareRotationList);
}
private ControllerImpl(@NonNull DynamicRotationList switchingAwareRotationList,
- @NonNull StaticRotationList switchingUnawareRotationList) {
+ @NonNull StaticRotationList switchingUnawareRotationList,
+ @Nullable RotationList rotationList,
+ @Nullable RotationList hardwareRotationList) {
mSwitchingAwareRotationList = switchingAwareRotationList;
mSwitchingUnawareRotationList = switchingUnawareRotationList;
+ mRotationList = rotationList;
+ mHardwareRotationList = hardwareRotationList;
}
@Nullable
public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme,
- @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
+ @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+ @SwitchMode int mode, boolean forward) {
if (imi == null) {
return null;
}
- if (imi.supportsSwitchingToNextInputMethod()) {
+ if (Flags.imeSwitcherRevamp() && mRotationList != null) {
+ return mRotationList.next(imi, subtype, onlyCurrentIme,
+ isRecency(mode, forward), forward);
+ } else if (imi.supportsSwitchingToNextInputMethod()) {
return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
subtype);
} else {
@@ -445,11 +708,66 @@
}
}
- public void onUserActionLocked(@NonNull InputMethodInfo imi,
+ @Nullable
+ public ImeSubtypeListItem getNextInputMethodForHardware(boolean onlyCurrentIme,
+ @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+ @SwitchMode int mode, boolean forward) {
+ if (Flags.imeSwitcherRevamp() && mHardwareRotationList != null) {
+ return mHardwareRotationList.next(imi, subtype, onlyCurrentIme,
+ isRecency(mode, forward), forward);
+ }
+ return null;
+ }
+
+ /**
+ * Called when the user took an action that should update the recency of the current
+ * input method and subtype in the switching list.
+ *
+ * @param imi the currently selected input method.
+ * @param subtype the currently selected input method subtype, if any.
+ * @return {@code true} if the recency was updated, otherwise {@code false}.
+ * @see android.inputmethodservice.InputMethodServiceInternal#notifyUserActionIfNecessary()
+ */
+ public boolean onUserActionLocked(@NonNull InputMethodInfo imi,
@Nullable InputMethodSubtype subtype) {
- if (imi.supportsSwitchingToNextInputMethod()) {
+ boolean recencyUpdated = false;
+ if (Flags.imeSwitcherRevamp()) {
+ if (mRotationList != null) {
+ recencyUpdated |= mRotationList.setMostRecent(imi, subtype);
+ }
+ if (mHardwareRotationList != null) {
+ recencyUpdated |= mHardwareRotationList.setMostRecent(imi, subtype);
+ }
+ if (recencyUpdated) {
+ mUserActionSinceSwitch = true;
+ }
+ } else if (imi.supportsSwitchingToNextInputMethod()) {
mSwitchingAwareRotationList.onUserAction(imi, subtype);
}
+ return recencyUpdated;
+ }
+
+ /** Called when the input method and subtype was changed. */
+ public void onInputMethodSubtypeChanged() {
+ mUserActionSinceSwitch = false;
+ }
+
+ /**
+ * Whether the given mode and direction result in recency or static order.
+ *
+ * <p>{@link #MODE_AUTO} resolves to the recency order for the first forwards switch
+ * after an {@link #onUserActionLocked user action}, and otherwise to the static order.</p>
+ *
+ * @param mode the switching mode.
+ * @param forward the switching direction.
+ * @return {@code true} for the recency order, otherwise {@code false}.
+ */
+ private boolean isRecency(@SwitchMode int mode, boolean forward) {
+ if (mode == MODE_AUTO && mUserActionSinceSwitch && forward) {
+ return true;
+ } else {
+ return mode == MODE_RECENT;
+ }
}
@NonNull
@@ -473,6 +791,17 @@
mSwitchingAwareRotationList.dump(pw, prefix + " ");
pw.println(prefix + "mSwitchingUnawareRotationList:");
mSwitchingUnawareRotationList.dump(pw, prefix + " ");
+ if (Flags.imeSwitcherRevamp()) {
+ if (mRotationList != null) {
+ pw.println(prefix + "mRotationList:");
+ mRotationList.dump(pw, prefix + " ");
+ }
+ if (mHardwareRotationList != null) {
+ pw.println(prefix + "mHardwareRotationList:");
+ mHardwareRotationList.dump(pw, prefix + " ");
+ }
+ pw.println("User action since last switch: " + mUserActionSinceSwitch);
+ }
}
}
@@ -480,26 +809,71 @@
private ControllerImpl mController;
InputMethodSubtypeSwitchingController() {
- mController = ControllerImpl.createFrom(null, Collections.emptyList());
+ mController = ControllerImpl.createFrom(null, Collections.emptyList(),
+ Collections.emptyList());
}
+ /**
+ * Called when the user took an action that should update the recency of the current
+ * input method and subtype in the switching list.
+ *
+ * @param imi the currently selected input method.
+ * @param subtype the currently selected input method subtype, if any.
+ * @see android.inputmethodservice.InputMethodServiceInternal#notifyUserActionIfNecessary()
+ */
public void onUserActionLocked(@NonNull InputMethodInfo imi,
@Nullable InputMethodSubtype subtype) {
mController.onUserActionLocked(imi, subtype);
}
+ /** Called when the input method and subtype was changed. */
+ public void onInputMethodSubtypeChanged() {
+ mController.onInputMethodSubtypeChanged();
+ }
+
public void resetCircularListLocked(@NonNull Context context,
@NonNull InputMethodSettings settings) {
mController = ControllerImpl.createFrom(mController,
getSortedInputMethodAndSubtypeList(
false /* includeAuxiliarySubtypes */, false /* isScreenLocked */,
- false /* forImeMenu */, context, settings));
+ false /* forImeMenu */, context, settings),
+ getInputMethodAndSubtypeListForHardwareKeyboard(context, settings));
}
+ /**
+ * Gets the next input method and subtype, starting from the given ones, in the given direction.
+ *
+ * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+ * @param imi the input method to find the next value from.
+ * @param subtype the input method subtype to find the next value from, if any.
+ * @param mode the switching mode.
+ * @param forward whether to search search forwards or backwards in the list.
+ * @return the next input method and subtype if found, otherwise {@code null}.
+ */
@Nullable
public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
- @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
- return mController.getNextInputMethod(onlyCurrentIme, imi, subtype);
+ @Nullable InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+ @SwitchMode int mode, boolean forward) {
+ return mController.getNextInputMethod(onlyCurrentIme, imi, subtype, mode, forward);
+ }
+
+ /**
+ * Gets the next input method and subtype suitable for hardware keyboards, starting from the
+ * given ones, in the given direction.
+ *
+ * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+ * @param imi the input method to find the next value from.
+ * @param subtype the input method subtype to find the next value from, if any.
+ * @param mode the switching mode
+ * @param forward whether to search search forwards or backwards in the list.
+ * @return the next input method and subtype if found, otherwise {@code null}.
+ */
+ @Nullable
+ public ImeSubtypeListItem getNextInputMethodForHardware(boolean onlyCurrentIme,
+ @NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype,
+ @SwitchMode int mode, boolean forward) {
+ return mController.getNextInputMethodForHardware(onlyCurrentIme, imi, subtype, mode,
+ forward);
}
public void dump(@NonNull Printer pw, @NonNull String prefix) {
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 41aac32..770e12d 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -296,6 +296,12 @@
return mInner.isInputMethodPickerShownForTest();
}
+ @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS)
+ @Override
+ public void onImeSwitchButtonClickFromSystem(int displayId) {
+ mInner.onImeSwitchButtonClickFromSystem(displayId);
+ }
+
@Override
public InputMethodSubtype getCurrentInputMethodSubtype(int userId) {
return mInner.getCurrentInputMethodSubtype(userId);
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index 68a4172..2755a80 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -26,13 +26,14 @@
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.CameraCompatTaskInfo;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.ProtoLogGroup;
import com.android.window.flags.Flags;
/**
@@ -56,6 +57,9 @@
private boolean mIsCameraCompatTreatmentPending = false;
+ @Nullable
+ private Task mCameraTask;
+
CameraCompatFreeformPolicy(@NonNull DisplayContent displayContent,
@NonNull CameraStateMonitor cameraStateMonitor,
@NonNull ActivityRefresher activityRefresher) {
@@ -116,6 +120,7 @@
final int newCameraCompatMode = getCameraCompatMode(cameraActivity);
if (newCameraCompatMode != existingCameraCompatMode) {
mIsCameraCompatTreatmentPending = true;
+ mCameraTask = cameraActivity.getTask();
cameraActivity.mAppCompatController.getAppCompatCameraOverrides()
.setFreeformCameraCompatMode(newCameraCompatMode);
forceUpdateActivityAndTask(cameraActivity);
@@ -127,18 +132,22 @@
}
@Override
- public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity,
- @NonNull String cameraId) {
- if (isActivityForCameraIdRefreshing(cameraId)) {
- ProtoLog.v(ProtoLogGroup.WM_DEBUG_STATES,
- "Display id=%d is notified that Camera %s is closed but activity is"
- + " still refreshing. Rescheduling an update.",
- mDisplayContent.mDisplayId, cameraId);
- return false;
+ public boolean onCameraClosed(@NonNull String cameraId) {
+ // Top activity in the same task as the camera activity, or `null` if the task is
+ // closed.
+ final ActivityRecord topActivity = mCameraTask != null
+ ? mCameraTask.getTopActivity(/* isFinishing */ false, /* includeOverlays */ false)
+ : null;
+ if (topActivity != null) {
+ if (isActivityForCameraIdRefreshing(topActivity, cameraId)) {
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_STATES,
+ "Display id=%d is notified that Camera %s is closed but activity is"
+ + " still refreshing. Rescheduling an update.",
+ mDisplayContent.mDisplayId, cameraId);
+ return false;
+ }
}
- cameraActivity.mAppCompatController.getAppCompatCameraOverrides()
- .setFreeformCameraCompatMode(CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE);
- forceUpdateActivityAndTask(cameraActivity);
+ mCameraTask = null;
mIsCameraCompatTreatmentPending = false;
return true;
}
@@ -186,10 +195,9 @@
&& !activity.isEmbedded();
}
- private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) {
- final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
- /* considerKeyguardState= */ true);
- if (topActivity == null || !isTreatmentEnabledForActivity(topActivity)
+ private boolean isActivityForCameraIdRefreshing(@NonNull ActivityRecord topActivity,
+ @NonNull String cameraId) {
+ if (!isTreatmentEnabledForActivity(topActivity)
|| mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) {
return false;
}
diff --git a/services/core/java/com/android/server/wm/CameraStateMonitor.java b/services/core/java/com/android/server/wm/CameraStateMonitor.java
index a54141c..068fc00 100644
--- a/services/core/java/com/android/server/wm/CameraStateMonitor.java
+++ b/services/core/java/com/android/server/wm/CameraStateMonitor.java
@@ -61,9 +61,6 @@
@NonNull
private final Handler mHandler;
- @Nullable
- private ActivityRecord mCameraActivity;
-
// Bi-directional map between package names and active camera IDs since we need to 1) get a
// camera id by a package name when resizing the window; 2) get a package name by a camera id
// when camera connection is closed and we need to clean up our records.
@@ -91,13 +88,13 @@
@Override
public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) {
synchronized (mWmService.mGlobalLock) {
- notifyCameraOpened(cameraId, packageId);
+ notifyCameraOpenedWithDelay(cameraId, packageId);
}
}
@Override
public void onCameraClosed(@NonNull String cameraId) {
synchronized (mWmService.mGlobalLock) {
- notifyCameraClosed(cameraId);
+ notifyCameraClosedWithDelay(cameraId);
}
}
};
@@ -131,8 +128,8 @@
mCameraStateListeners.remove(listener);
}
- private void notifyCameraOpened(
- @NonNull String cameraId, @NonNull String packageName) {
+ private void notifyCameraOpenedWithDelay(@NonNull String cameraId,
+ @NonNull String packageName) {
// If an activity is restarting or camera is flipping, the camera connection can be
// quickly closed and reopened.
mScheduledToBeRemovedCameraIdSet.remove(cameraId);
@@ -142,25 +139,30 @@
// Some apps can’t handle configuration changes coming at the same time with Camera setup so
// delaying orientation update to accommodate for that.
mScheduledCompatModeUpdateCameraIdSet.add(cameraId);
- mHandler.postDelayed(
- () -> {
- synchronized (mWmService.mGlobalLock) {
- if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) {
- // Camera compat mode update has happened already or was cancelled
- // because camera was closed.
- return;
- }
- mCameraIdPackageBiMapping.put(packageName, cameraId);
- mCameraActivity = findCameraActivity(packageName);
- if (mCameraActivity == null || mCameraActivity.getTask() == null) {
- return;
- }
- notifyListenersCameraOpened(mCameraActivity, cameraId);
- }
- },
+ mHandler.postDelayed(() -> notifyCameraOpenedInternal(cameraId, packageName),
CAMERA_OPENED_LETTERBOX_UPDATE_DELAY_MS);
}
+ private void notifyCameraOpenedInternal(@NonNull String cameraId, @NonNull String packageName) {
+ synchronized (mWmService.mGlobalLock) {
+ if (!mScheduledCompatModeUpdateCameraIdSet.remove(cameraId)) {
+ // Camera compat mode update has happened already or was cancelled
+ // because camera was closed.
+ return;
+ }
+ mCameraIdPackageBiMapping.put(packageName, cameraId);
+ // If there are multiple activities of the same package name and none of
+ // them are the top running activity, we do not apply treatment (rather than
+ // guessing and applying it to the wrong activity).
+ final ActivityRecord cameraActivity =
+ findUniqueActivityWithPackageName(packageName);
+ if (cameraActivity == null || cameraActivity.getTask() == null) {
+ return;
+ }
+ notifyListenersCameraOpened(cameraActivity, cameraId);
+ }
+ }
+
private void notifyListenersCameraOpened(@NonNull ActivityRecord cameraActivity,
@NonNull String cameraId) {
for (int i = 0; i < mCameraStateListeners.size(); i++) {
@@ -174,7 +176,13 @@
}
}
- private void notifyCameraClosed(@NonNull String cameraId) {
+ /**
+ * Processes camera closed, and schedules notifying listeners.
+ *
+ * <p>The delay is introduced to avoid flickering when switching between front and back camera,
+ * and when an activity is refreshed due to camera compat treatment.
+ */
+ private void notifyCameraClosedWithDelay(@NonNull String cameraId) {
ProtoLog.v(WM_DEBUG_STATES,
"Display id=%d is notified that Camera %s is closed.",
mDisplayContent.mDisplayId, cameraId);
@@ -217,9 +225,10 @@
// Already reconnected to this camera, no need to clean up.
return;
}
- if (mCameraActivity != null && mCurrentListenerForCameraActivity != null) {
+
+ if (mCurrentListenerForCameraActivity != null) {
boolean closeSuccessful =
- mCurrentListenerForCameraActivity.onCameraClosed(mCameraActivity, cameraId);
+ mCurrentListenerForCameraActivity.onCameraClosed(cameraId);
if (closeSuccessful) {
mCameraIdPackageBiMapping.removeCameraId(cameraId);
mCurrentListenerForCameraActivity = null;
@@ -231,8 +240,14 @@
}
// TODO(b/335165310): verify that this works in multi instance and permission dialogs.
+ /**
+ * Finds a visible activity with the given package name.
+ *
+ * <p>If there are multiple visible activities with a given package name, and none of them are
+ * the `topRunningActivity`, returns null.
+ */
@Nullable
- private ActivityRecord findCameraActivity(@NonNull String packageName) {
+ private ActivityRecord findUniqueActivityWithPackageName(@NonNull String packageName) {
final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
/* considerKeyguardState= */ true);
if (topActivity != null && topActivity.packageName.equals(packageName)) {
@@ -277,11 +292,11 @@
// TODO(b/336474959): try to decouple `cameraId` from the listeners.
boolean onCameraOpened(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId);
/**
- * Notifies the compat listener that an activity has closed the camera.
+ * Notifies the compat listener that camera is closed.
*
* @return true if cleanup has been successful - the notifier might try again if false.
*/
// TODO(b/336474959): try to decouple `cameraId` from the listeners.
- boolean onCameraClosed(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId);
+ boolean onCameraClosed(@NonNull String cameraId);
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 9998e1a..1a0124a 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -342,12 +342,19 @@
}
@Override
- public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity,
- @NonNull String cameraId) {
+ public boolean onCameraClosed(@NonNull String cameraId) {
+ // Top activity in the same task as the camera activity, or `null` if the task is
+ // closed.
+ final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (topActivity == null) {
+ return true;
+ }
+
synchronized (this) {
// TODO(b/336474959): Once refresh is implemented in `CameraCompatFreeformPolicy`,
// consider checking this in CameraStateMonitor before notifying the listeners (this).
- if (isActivityForCameraIdRefreshing(cameraId)) {
+ if (isActivityForCameraIdRefreshing(topActivity, cameraId)) {
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Display id=%d is notified that camera is closed but activity is"
+ " still refreshing. Rescheduling an update.",
@@ -355,15 +362,15 @@
return false;
}
}
+
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Display id=%d is notified that Camera is closed, updating rotation.",
mDisplayContent.mDisplayId);
- final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
- /* considerKeyguardState= */ true);
- if (topActivity == null
- // Checking whether an activity in fullscreen rather than the task as this
- // camera compat treatment doesn't cover activity embedding.
- || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+ // Checking whether an activity in fullscreen rather than the task as this camera compat
+ // treatment doesn't cover activity embedding.
+ // TODO(b/350495350): Consider checking whether this activity is the camera activity, or
+ // whether the top activity has the same task as the one which opened camera.
+ if (topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
return true;
}
recomputeConfigurationForCameraCompatIfNeeded(topActivity);
@@ -372,14 +379,13 @@
}
// TODO(b/336474959): Do we need cameraId here?
- private boolean isActivityForCameraIdRefreshing(@NonNull String cameraId) {
- final ActivityRecord topActivity = mDisplayContent.topRunningActivity(
- /* considerKeyguardState= */ true);
- if (!isTreatmentEnabledForActivity(topActivity)
- || !mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) {
+ private boolean isActivityForCameraIdRefreshing(@NonNull ActivityRecord activity,
+ @NonNull String cameraId) {
+ if (!isTreatmentEnabledForActivity(activity)
+ || !mCameraStateMonitor.isCameraWithIdRunningForActivity(activity, cameraId)) {
return false;
}
- return mActivityRefresher.isActivityRefreshing(topActivity);
+ return mActivityRefresher.isActivityRefreshing(activity);
}
private void recomputeConfigurationForCameraCompatIfNeeded(
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 8f630af..ce68b86 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -21,6 +21,7 @@
import static com.android.compatibility.common.util.SystemUtil.eventually;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -33,18 +34,20 @@
import android.graphics.Insets;
import android.os.RemoteException;
import android.provider.Settings;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.Until;
import android.util.Log;
import android.view.WindowManagerGlobal;
+import android.view.WindowManagerPolicyConstants;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
+import androidx.annotation.NonNull;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObject2;
+import androidx.test.uiautomator.Until;
import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper;
import com.android.apps.inputmethod.simpleime.testing.TestActivity;
@@ -66,6 +69,10 @@
private static final String TAG = "SimpleIMSTest";
private static final String INPUT_METHOD_SERVICE_NAME = ".SimpleInputMethodService";
private static final String EDIT_TEXT_DESC = "Input box";
+ private static final String INPUT_METHOD_NAV_BACK_ID =
+ "android:id/input_method_nav_back";
+ private static final String INPUT_METHOD_NAV_IME_SWITCHER_ID =
+ "android:id/input_method_nav_ime_switcher";
private static final long TIMEOUT_IN_SECONDS = 3;
private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
"settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1";
@@ -697,6 +704,151 @@
assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
}
+ /**
+ * Verifies that clicking on the IME navigation bar back button hides the IME.
+ */
+ @Test
+ public void testBackButtonClick() throws Exception {
+ boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+ .hasNavigationBar(mInputMethodService.getDisplayId());
+ assumeTrue("Must have a navigation bar", hasNavigationBar);
+ assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled());
+
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ verifyInputViewStatusOnMainSync(
+ () -> {
+ // Ensure the IME navigation bar and the IME switch button are drawn.
+ mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+ InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
+ | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
+ );
+ assertThat(mActivity.showImeWithWindowInsetsController()).isTrue();
+ },
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ final var backButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_BACK_ID);
+ backButtonUiObject.click();
+ mInstrumentation.waitForIdleSync();
+
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
+ }
+
+ /**
+ * Verifies that long clicking on the IME navigation bar back button hides the IME.
+ */
+ @Test
+ public void testBackButtonLongClick() throws Exception {
+ boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+ .hasNavigationBar(mInputMethodService.getDisplayId());
+ assumeTrue("Must have a navigation bar", hasNavigationBar);
+ assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled());
+
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ verifyInputViewStatusOnMainSync(
+ () -> {
+ // Ensure the IME navigation bar and the IME switch button are drawn.
+ mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+ InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
+ | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
+ );
+ assertThat(mActivity.showImeWithWindowInsetsController()).isTrue();
+ },
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ final var backButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_BACK_ID);
+ backButtonUiObject.longClick();
+ mInstrumentation.waitForIdleSync();
+
+ assertThat(mInputMethodService.isInputViewShown()).isFalse();
+ }
+
+ /**
+ * Verifies that clicking on the IME switch button shows the Input Method Switcher Menu.
+ */
+ @Test
+ public void testImeSwitchButtonClick() throws Exception {
+ boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+ .hasNavigationBar(mInputMethodService.getDisplayId());
+ assumeTrue("Must have a navigation bar", hasNavigationBar);
+ assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled());
+
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ verifyInputViewStatusOnMainSync(
+ () -> {
+ // Ensure the IME navigation bar and the IME switch button are drawn.
+ mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+ InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
+ | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
+ );
+ assertThat(mActivity.showImeWithWindowInsetsController()).isTrue();
+ },
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ final var imm = mContext.getSystemService(InputMethodManager.class);
+
+ final var imeSwitchButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_IME_SWITCHER_ID);
+ imeSwitchButtonUiObject.click();
+ mInstrumentation.waitForIdleSync();
+
+ assertWithMessage("Input Method Switcher Menu is shown")
+ .that(isInputMethodPickerShown(imm))
+ .isTrue();
+
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Hide the Picker menu before finishing.
+ mUiDevice.pressBack();
+ }
+
+ /**
+ * Verifies that long clicking on the IME switch button shows the Input Method Switcher Menu.
+ */
+ @Test
+ public void testImeSwitchButtonLongClick() throws Exception {
+ boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+ .hasNavigationBar(mInputMethodService.getDisplayId());
+ assumeTrue("Must have a navigation bar", hasNavigationBar);
+ assumeTrue("Must be in gesture navigation mode", isGestureNavEnabled());
+
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ verifyInputViewStatusOnMainSync(
+ () -> {
+ // Ensure the IME navigation bar and the IME switch button are drawn.
+ mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+ InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
+ | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
+ );
+ assertThat(mActivity.showImeWithWindowInsetsController()).isTrue();
+ },
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ final var imm = mContext.getSystemService(InputMethodManager.class);
+
+ final var imeSwitchButtonUiObject = getUiObjectById(INPUT_METHOD_NAV_IME_SWITCHER_ID);
+ imeSwitchButtonUiObject.longClick();
+ mInstrumentation.waitForIdleSync();
+
+ assertWithMessage("Input Method Switcher Menu is shown")
+ .that(isInputMethodPickerShown(imm))
+ .isTrue();
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+ // Hide the Picker menu before finishing.
+ mUiDevice.pressBack();
+ }
+
private void verifyInputViewStatus(
Runnable runnable, boolean expected, boolean inputViewStarted)
throws InterruptedException {
@@ -844,6 +996,32 @@
return SystemUtil.runShellCommandOrThrow(cmd);
}
+ /**
+ * Checks if the Input Method Switcher Menu is shown. This runs by adopting the Shell's
+ * permission to ensure we have TEST_INPUT_METHOD permission.
+ */
+ private static boolean isInputMethodPickerShown(@NonNull InputMethodManager imm) {
+ return SystemUtil.runWithShellPermissionIdentity(imm::isInputMethodPickerShown);
+ }
+
+ @NonNull
+ private UiObject2 getUiObjectById(@NonNull String id) {
+ final var uiObject = mUiDevice.wait(
+ Until.findObject(By.res(id)),
+ TimeUnit.SECONDS.toMillis(TIMEOUT_IN_SECONDS));
+ assertThat(uiObject).isNotNull();
+ return uiObject;
+ }
+
+ /**
+ * Returns {@code true} if the navigation mode is gesture nav, and {@code false} otherwise.
+ */
+ private boolean isGestureNavEnabled() {
+ return mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_navBarInteractionMode)
+ == WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
+ }
+
private void clickOnEditorText() {
// Find the editText and click it.
UiObject2 editTextUiObject =
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index e81cf9d..dc03732 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -16,15 +16,26 @@
package com.android.server.inputmethod;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_AUTO;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_RECENT;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.MODE_STATIC;
+import static com.android.server.inputmethod.InputMethodSubtypeSwitchingController.SwitchMode;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.content.ComponentName;
import android.content.pm.ApplicationInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
@@ -35,6 +46,7 @@
import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ControllerImpl;
import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
+import org.junit.Rule;
import org.junit.Test;
import java.util.ArrayList;
@@ -51,6 +63,9 @@
private static final String SYSTEM_LOCALE = "en_US";
private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@NonNull
private static InputMethodSubtype createTestSubtype(@NonNull String locale) {
return new InputMethodSubtypeBuilder()
@@ -170,7 +185,7 @@
subtype = createTestSubtype(currentItem.mSubtypeName.toString());
}
final ImeSubtypeListItem nextIme = controller.getNextInputMethod(onlyCurrentIme,
- currentItem.mImi, subtype);
+ currentItem.mImi, subtype, MODE_STATIC, true /* forward */);
assertEquals(nextItem, nextIme);
}
@@ -185,15 +200,16 @@
}
}
- private void onUserAction(@NonNull ControllerImpl controller,
+ private boolean onUserAction(@NonNull ControllerImpl controller,
@NonNull ImeSubtypeListItem subtypeListItem) {
InputMethodSubtype subtype = null;
if (subtypeListItem.mSubtypeName != null) {
subtype = createTestSubtype(subtypeListItem.mSubtypeName.toString());
}
- controller.onUserActionLocked(subtypeListItem.mImi, subtype);
+ return controller.onUserActionLocked(subtypeListItem.mImi, subtype);
}
+ @RequiresFlagsDisabled(Flags.FLAG_IME_SWITCHER_REVAMP)
@Test
public void testControllerImpl() {
final List<ImeSubtypeListItem> disabledItems = createDisabledImeSubtypes();
@@ -213,7 +229,7 @@
final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
final ControllerImpl controller = ControllerImpl.createFrom(
- null /* currentInstance */, enabledItems);
+ null /* currentInstance */, enabledItems, new ArrayList<>());
// switching-aware loop
assertRotationOrder(controller, false /* onlyCurrentIme */,
@@ -257,6 +273,7 @@
disabledSubtypeUnawareIme, null);
}
+ @RequiresFlagsDisabled(Flags.FLAG_IME_SWITCHER_REVAMP)
@Test
public void testControllerImplWithUserAction() {
final List<ImeSubtypeListItem> enabledItems = createEnabledImeSubtypes();
@@ -270,7 +287,7 @@
final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
final ControllerImpl controller = ControllerImpl.createFrom(
- null /* currentInstance */, enabledItems);
+ null /* currentInstance */, enabledItems, new ArrayList<>());
// === switching-aware loop ===
assertRotationOrder(controller, false /* onlyCurrentIme */,
@@ -320,7 +337,7 @@
// Rotation order should be preserved when created with the same subtype list.
final List<ImeSubtypeListItem> sameEnabledItems = createEnabledImeSubtypes();
final ControllerImpl newController = ControllerImpl.createFrom(controller,
- sameEnabledItems);
+ sameEnabledItems, new ArrayList<>());
assertRotationOrder(newController, false /* onlyCurrentIme */,
subtypeAwareIme, latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
assertRotationOrder(newController, false /* onlyCurrentIme */,
@@ -332,7 +349,7 @@
latinIme_en_us, latinIme_fr, subtypeAwareIme, switchingUnawareLatinIme_en_uk,
switchUnawareJapaneseIme_ja_jp, subtypeUnawareIme);
final ControllerImpl anotherController = ControllerImpl.createFrom(controller,
- differentEnabledItems);
+ differentEnabledItems, new ArrayList<>());
assertRotationOrder(anotherController, false /* onlyCurrentIme */,
latinIme_en_us, latinIme_fr, subtypeAwareIme);
assertRotationOrder(anotherController, false /* onlyCurrentIme */,
@@ -370,6 +387,7 @@
assertFalse(item_en_us_allcaps.mIsSystemLanguage);
}
+ @RequiresFlagsDisabled(Flags.FLAG_IME_SWITCHER_REVAMP)
@SuppressWarnings("SelfComparison")
@Test
public void testImeSubtypeListComparator() {
@@ -471,4 +489,739 @@
assertNotEquals(ime2, ime1);
}
}
+
+ /** Verifies the static mode. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testModeStatic() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var english = items.get(0);
+ final var french = items.get(1);
+ final var italian = items.get(2);
+ final var simple = items.get(3);
+ final var latinIme = List.of(english, french, italian);
+ final var simpleIme = List.of(simple);
+
+ final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var hardwareEnglish = hardwareItems.get(0);
+ final var hardwareFrench = hardwareItems.get(1);
+ final var hardwareItalian = hardwareItems.get(2);
+ final var hardwareSimple = hardwareItems.get(3);
+ final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian);
+ final var hardwareSimpleIme = List.of(hardwareSimple);
+
+ final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+ hardwareItems);
+
+ final int mode = MODE_STATIC;
+
+ // Static mode matches the given items order.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ items, List.of(latinIme, simpleIme));
+
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ // Set french IME as most recent.
+ assertTrue("Recency updated for french IME", onUserAction(controller, french));
+
+ // Static mode is not influenced by recency updates on non-hardware item.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ items, List.of(latinIme, simpleIme));
+
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ assertTrue("Recency updated for french hardware IME",
+ onUserAction(controller, hardwareFrench));
+
+ // Static mode is not influenced by recency updates on hardware item.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ items, List.of(latinIme, simpleIme));
+
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+ }
+
+ /** Verifies the recency mode. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testModeRecent() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var english = items.get(0);
+ final var french = items.get(1);
+ final var italian = items.get(2);
+ final var simple = items.get(3);
+ final var latinIme = List.of(english, french, italian);
+ final var simpleIme = List.of(simple);
+
+ final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var hardwareEnglish = hardwareItems.get(0);
+ final var hardwareFrench = hardwareItems.get(1);
+ final var hardwareItalian = hardwareItems.get(2);
+ final var hardwareSimple = hardwareItems.get(3);
+ final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian);
+ final var hardwareSimpleIme = List.of(hardwareSimple);
+
+ final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+ hardwareItems);
+
+ final int mode = MODE_RECENT;
+
+ // Recency order is initialized to static order.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ items, List.of(latinIme, simpleIme));
+
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ assertTrue("Recency updated for french IME", onUserAction(controller, french));
+ final var recencyItems = List.of(french, english, italian, simple);
+ final var recencyLatinIme = List.of(french, english, italian);
+ final var recencySimpleIme = List.of(simple);
+
+ // The order of non-hardware items is updated.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+ // The order of hardware items remains unchanged for an action on a non-hardware item.
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ assertFalse("Recency not updated again for same IME", onUserAction(controller, french));
+
+ // The order of non-hardware items remains unchanged.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+ // The order of hardware items remains unchanged.
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ assertTrue("Recency updated for french hardware IME",
+ onUserAction(controller, hardwareFrench));
+
+ final var recencyHardwareItems =
+ List.of(hardwareFrench, hardwareEnglish, hardwareItalian, hardwareSimple);
+ final var recencyHardwareLatinIme =
+ List.of(hardwareFrench, hardwareEnglish, hardwareItalian);
+ final var recencyHardwareSimpleIme = List.of(hardwareSimple);
+
+ // The order of non-hardware items is unchanged.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+ // The order of hardware items is updated.
+ assertNextOrder(controller, true /* forHardware */, mode,
+ recencyHardwareItems, List.of(recencyHardwareLatinIme, recencyHardwareSimpleIme));
+ }
+
+ /** Verifies the auto mode. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testModeAuto() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var english = items.get(0);
+ final var french = items.get(1);
+ final var italian = items.get(2);
+ final var simple = items.get(3);
+ final var latinIme = List.of(english, french, italian);
+ final var simpleIme = List.of(simple);
+
+ final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var hardwareEnglish = hardwareItems.get(0);
+ final var hardwareFrench = hardwareItems.get(1);
+ final var hardwareItalian = hardwareItems.get(2);
+ final var hardwareSimple = hardwareItems.get(3);
+ final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian);
+ final var hardwareSimpleIme = List.of(hardwareSimple);
+
+ final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+ hardwareItems);
+
+ final int mode = MODE_AUTO;
+
+ // Auto mode resolves to static order initially.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ items, List.of(latinIme, simpleIme));
+
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ // User action on french IME.
+ assertTrue("Recency updated for french IME", onUserAction(controller, french));
+
+ final var recencyItems = List.of(french, english, italian, simple);
+ final var recencyLatinIme = List.of(french, english, italian);
+ final var recencySimpleIme = List.of(simple);
+
+ // Auto mode resolves to recency order for the first forward after user action, and to
+ // static order for the backwards direction.
+ assertNextOrder(controller, false /* forHardware */, mode, true /* forward */,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+ assertNextOrder(controller, false /* forHardware */, mode, false /* forward */,
+ items.reversed(), List.of(latinIme.reversed(), simpleIme.reversed()));
+
+ // Auto mode resolves to recency order for the first forward after user action,
+ // but the recency was not updated for hardware items, so it's equivalent to static order.
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ // Change IME, reset user action having happened.
+ controller.onInputMethodSubtypeChanged();
+
+ // Auto mode resolves to static order as there was no user action since changing IMEs.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ items, List.of(latinIme, simpleIme));
+
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ // User action on french IME again.
+ assertFalse("Recency not updated again for same IME", onUserAction(controller, french));
+
+ // Auto mode still resolves to static order, as a user action on the currently most
+ // recent IME has no effect.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ items, List.of(latinIme, simpleIme));
+
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ // User action on hardware french IME.
+ assertTrue("Recency updated for french hardware IME",
+ onUserAction(controller, hardwareFrench));
+
+ final var recencyHardware =
+ List.of(hardwareFrench, hardwareEnglish, hardwareItalian, hardwareSimple);
+ final var recencyHardwareLatin =
+ List.of(hardwareFrench, hardwareEnglish, hardwareItalian);
+ final var recencyHardwareSimple = List.of(hardwareSimple);
+
+ // Auto mode resolves to recency order for the first forward direction after a user action
+ // on a hardware IME, and to static order for the backwards direction.
+ assertNextOrder(controller, false /* forHardware */, mode, true /* forward */,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+ assertNextOrder(controller, false /* forHardware */, mode, false /* forward */,
+ items.reversed(), List.of(latinIme.reversed(), simpleIme.reversed()));
+
+ assertNextOrder(controller, true /* forHardware */, mode, true /* forward */,
+ recencyHardware, List.of(recencyHardwareLatin, recencyHardwareSimple));
+
+ assertNextOrder(controller, true /* forHardware */, mode, false /* forward */,
+ hardwareItems.reversed(),
+ List.of(hardwareLatinIme.reversed(), hardwareSimpleIme.reversed()));
+ }
+
+ /**
+ * Verifies that the recency order is preserved only when updating with an equal list of items.
+ */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testUpdateList() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(items, "SimpleIme", "SimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var english = items.get(0);
+ final var french = items.get(1);
+ final var italian = items.get(2);
+ final var simple = items.get(3);
+
+ final var latinIme = List.of(english, french, italian);
+ final var simpleIme = List.of(simple);
+
+ final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(hardwareItems, "HardwareLatinIme", "HardwareLatinIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+ addTestImeSubtypeListItems(hardwareItems, "HardwareSimpleIme", "HardwareSimpleIme",
+ null, true /* supportsSwitchingToNextInputMethod */);
+
+ final var hardwareEnglish = hardwareItems.get(0);
+ final var hardwareFrench = hardwareItems.get(1);
+ final var hardwareItalian = hardwareItems.get(2);
+ final var hardwareSimple = hardwareItems.get(3);
+
+ final var hardwareLatinIme = List.of(hardwareEnglish, hardwareFrench, hardwareItalian);
+ final var hardwareSimpleIme = List.of(hardwareSimple);
+
+ final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+ hardwareItems);
+
+ final int mode = MODE_RECENT;
+
+ // Recency order is initialized to static order.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ items, List.of(latinIme, simpleIme));
+
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ // User action on french IME.
+ assertTrue("Recency updated for french IME", onUserAction(controller, french));
+
+ final var equalItems = new ArrayList<>(items);
+ final var otherItems = new ArrayList<>(items);
+ otherItems.remove(simple);
+
+ final var equalController = ControllerImpl.createFrom(controller, equalItems,
+ hardwareItems);
+ final var otherController = ControllerImpl.createFrom(controller, otherItems,
+ hardwareItems);
+
+ final var recencyItems = List.of(french, english, italian, simple);
+ final var recencyLatinIme = List.of(french, english, italian);
+ final var recencySimpleIme = List.of(simple);
+
+ assertNextOrder(controller, false /* forHardware */, mode,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+ // The order of equal non-hardware items is unchanged.
+ assertNextOrder(equalController, false /* forHardware */, mode,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+ // The order of other hardware items is reset.
+ assertNextOrder(otherController, false /* forHardware */, mode,
+ latinIme, List.of(latinIme));
+
+ // The order of hardware remains unchanged.
+ assertNextOrder(controller, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ assertNextOrder(equalController, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ assertNextOrder(otherController, true /* forHardware */, mode,
+ hardwareItems, List.of(hardwareLatinIme, hardwareSimpleIme));
+
+ assertTrue("Recency updated for french hardware IME",
+ onUserAction(controller, hardwareFrench));
+
+ final var equalHardwareItems = new ArrayList<>(hardwareItems);
+ final var otherHardwareItems = new ArrayList<>(hardwareItems);
+ otherHardwareItems.remove(hardwareSimple);
+
+ final var equalHardwareController = ControllerImpl.createFrom(controller, items,
+ equalHardwareItems);
+ final var otherHardwareController = ControllerImpl.createFrom(controller, items,
+ otherHardwareItems);
+
+ final var recencyHardwareItems =
+ List.of(hardwareFrench, hardwareEnglish, hardwareItalian, hardwareSimple);
+ final var recencyHardwareLatinIme =
+ List.of(hardwareFrench, hardwareEnglish, hardwareItalian);
+ final var recencyHardwareSimpleIme = List.of(hardwareSimple);
+
+ // The order of non-hardware items remains unchanged.
+ assertNextOrder(controller, false /* forHardware */, mode,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+ assertNextOrder(equalHardwareController, false /* forHardware */, mode,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+ assertNextOrder(otherHardwareController, false /* forHardware */, mode,
+ recencyItems, List.of(recencyLatinIme, recencySimpleIme));
+
+ assertNextOrder(controller, true /* forHardware */, mode,
+ recencyHardwareItems, List.of(recencyHardwareLatinIme, recencyHardwareSimpleIme));
+
+ // The order of equal hardware items is unchanged.
+ assertNextOrder(equalHardwareController, true /* forHardware */, mode,
+ recencyHardwareItems, List.of(recencyHardwareLatinIme, recencyHardwareSimpleIme));
+
+ // The order of other hardware items is reset.
+ assertNextOrder(otherHardwareController, true /* forHardware */, mode,
+ hardwareLatinIme, List.of(hardwareLatinIme));
+ }
+
+ /** Verifies that switch aware and switch unaware IMEs are combined together. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testSwitchAwareAndUnawareCombined() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "switchAware", "switchAware",
+ null, true /* supportsSwitchingToNextInputMethod*/);
+ addTestImeSubtypeListItems(items, "switchUnaware", "switchUnaware",
+ null, false /* supportsSwitchingToNextInputMethod*/);
+
+ final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(hardwareItems, "hardwareSwitchAware", "hardwareSwitchAware",
+ null, true /* supportsSwitchingToNextInputMethod*/);
+ addTestImeSubtypeListItems(hardwareItems, "hardwareSwitchUnaware", "hardwareSwitchUnaware",
+ null, false /* supportsSwitchingToNextInputMethod*/);
+
+ final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+ hardwareItems);
+
+ for (int mode = MODE_STATIC; mode <= MODE_AUTO; mode++) {
+ assertNextOrder(controller, false /* forHardware */, false /* onlyCurrentIme */,
+ mode, true /* forward */, items);
+ assertNextOrder(controller, false /* forHardware */, false /* onlyCurrentIme */,
+ mode, false /* forward */, items.reversed());
+
+ assertNextOrder(controller, true /* forHardware */, false /* onlyCurrentIme */,
+ mode, true /* forward */, hardwareItems);
+ assertNextOrder(controller, true /* forHardware */, false /* onlyCurrentIme */,
+ mode, false /* forward */, hardwareItems.reversed());
+ }
+ }
+
+ /** Verifies that an empty controller can't take any actions. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testEmptyList() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var controller = ControllerImpl.createFrom(null /* currentInstance */, List.of(),
+ List.of());
+
+ assertNoAction(controller, false /* forHardware */, items);
+ assertNoAction(controller, true /* forHardware */, hardwareItems);
+ }
+
+ /** Verifies that a controller with a single item can't take any actions. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testSingleItemList() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var controller = ControllerImpl.createFrom(null /* currentInstance */,
+ List.of(items.get(0)), List.of(hardwareItems.get(0)));
+
+ assertNoAction(controller, false /* forHardware */, items);
+ assertNoAction(controller, true /* forHardware */, hardwareItems);
+ }
+
+ /** Verifies that a controller can't take any actions for unknown items. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testUnknownItems() {
+ final var items = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ final var unknownItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(unknownItems, "UnknownIme", "UnknownIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ final var unknownHardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(unknownHardwareItems, "HardwareUnknownIme", "HardwareUnknownIme",
+ List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
+ hardwareItems);
+
+ assertNoAction(controller, false /* forHardware */, unknownItems);
+ assertNoAction(controller, true /* forHardware */, unknownHardwareItems);
+ }
+
+ /** Verifies that the IME name does influence the comparison order. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testCompareImeName() {
+ final var component = new ComponentName("com.example.ime", "Ime");
+ final var imeX = createTestItem(component, "ImeX", "A", "en_US", 0);
+ final var imeY = createTestItem(component, "ImeY", "A", "en_US", 0);
+
+ assertTrue("Smaller IME name should be smaller.", imeX.compareTo(imeY) < 0);
+ assertTrue("Larger IME name should be larger.", imeY.compareTo(imeX) > 0);
+ }
+
+ /** Verifies that the IME ID does influence the comparison order. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testCompareImeId() {
+ final var component1 = new ComponentName("com.example.ime1", "Ime");
+ final var component2 = new ComponentName("com.example.ime2", "Ime");
+ final var ime1 = createTestItem(component1, "Ime", "A", "en_US", 0);
+ final var ime2 = createTestItem(component2, "Ime", "A", "en_US", 0);
+
+ assertTrue("Smaller IME ID should be smaller.", ime1.compareTo(ime2) < 0);
+ assertTrue("Larger IME ID should be larger.", ime2.compareTo(ime1) > 0);
+ }
+
+ /** Verifies that comparison on self returns an equal order. */
+ @SuppressWarnings("SelfComparison")
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testCompareSelf() {
+ final var component = new ComponentName("com.example.ime", "Ime");
+ final var item = createTestItem(component, "Ime", "A", "en_US", 0);
+
+ assertEquals("Item should have the same order to itself.", 0, item.compareTo(item));
+ }
+
+ /** Verifies that comparison on an equivalent item returns an equal order. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testCompareEquivalent() {
+ final var component = new ComponentName("com.example.ime", "Ime");
+ final var item = createTestItem(component, "Ime", "A", "en_US", 0);
+ final var equivalent = createTestItem(component, "Ime", "A", "en_US", 0);
+
+ assertEquals("Equivalent items should have the same order.", 0, item.compareTo(equivalent));
+ }
+
+ /**
+ * Verifies that the system locale and system language do not the influence comparison order.
+ */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testCompareSystemLocaleSystemLanguage() {
+ final var component = new ComponentName("com.example.ime", "Ime");
+ final var japanese = createTestItem(component, "Ime", "A", "ja_JP", 0);
+ final var systemLanguage = createTestItem(component, "Ime", "A", "en_GB", 0);
+ final var systemLocale = createTestItem(component, "Ime", "A", "en_US", 0);
+
+ assertFalse(japanese.mIsSystemLanguage);
+ assertFalse(japanese.mIsSystemLocale);
+ assertTrue(systemLanguage.mIsSystemLanguage);
+ assertFalse(systemLanguage.mIsSystemLocale);
+ assertTrue(systemLocale.mIsSystemLanguage);
+ assertTrue(systemLocale.mIsSystemLocale);
+
+ assertEquals("System language shouldn't influence comparison over non-system language.",
+ 0, japanese.compareTo(systemLanguage));
+ assertEquals("System locale shouldn't influence comparison over non-system locale.",
+ 0, japanese.compareTo(systemLocale));
+ assertEquals("System locale shouldn't influence comparison over system language.",
+ 0, systemLanguage.compareTo(systemLocale));
+ }
+
+ /** Verifies that the subtype name does not influence the comparison order. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testCompareSubtypeName() {
+ final var component = new ComponentName("com.example.ime", "Ime");
+ final var subtypeA = createTestItem(component, "Ime", "A", "en_US", 0);
+ final var subtypeB = createTestItem(component, "Ime", "B", "en_US", 0);
+
+ assertEquals("Subtype name shouldn't influence comparison.",
+ 0, subtypeA.compareTo(subtypeB));
+ }
+
+ /** Verifies that the subtype index does not influence the comparison order. */
+ @RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Test
+ public void testCompareSubtypeIndex() {
+ final var component = new ComponentName("com.example.ime", "Ime");
+ final var subtype0 = createTestItem(component, "Ime1", "A", "en_US", 0);
+ final var subtype1 = createTestItem(component, "Ime1", "A", "en_US", 1);
+
+ assertEquals("Subtype index shouldn't influence comparison.",
+ 0, subtype0.compareTo(subtype1));
+ }
+
+ /**
+ * Verifies that the controller's next item order matches the given one, and cycles back at
+ * the end, both across all IMEs, and also per each IME. If a single item is given, verifies
+ * that no next item is returned.
+ *
+ * @param controller the controller to use for finding the next items.
+ * @param forHardware whether to find the next hardware item, or software item.
+ * @param mode the switching mode.
+ * @param forward whether to search forwards or backwards in the list.
+ * @param allItems the list of items across all IMEs.
+ * @param perImeItems the list of lists of items per IME.
+ */
+ private static void assertNextOrder(@NonNull ControllerImpl controller, boolean forHardware,
+ @SwitchMode int mode, boolean forward, @NonNull List<ImeSubtypeListItem> allItems,
+ @NonNull List<List<ImeSubtypeListItem>> perImeItems) {
+ assertNextOrder(controller, forHardware, false /* onlyCurrentIme */, mode,
+ forward, allItems);
+
+ for (var imeItems : perImeItems) {
+ assertNextOrder(controller, forHardware, true /* onlyCurrentIme */, mode,
+ forward, imeItems);
+ }
+ }
+
+ /**
+ * Verifies that the controller's next item order matches the given one, and cycles back at
+ * the end, both across all IMEs, and also per each IME. This checks the forward direction
+ * with the given items, and the backwards order with the items reversed. If a single item is
+ * given, verifies that no next item is returned.
+ *
+ * @param controller the controller to use for finding the next items.
+ * @param forHardware whether to find the next hardware item, or software item.
+ * @param mode the switching mode.
+ * @param allItems the list of items across all IMEs.
+ * @param perImeItems the list of lists of items per IME.
+ */
+ private static void assertNextOrder(@NonNull ControllerImpl controller, boolean forHardware,
+ @SwitchMode int mode, @NonNull List<ImeSubtypeListItem> allItems,
+ @NonNull List<List<ImeSubtypeListItem>> perImeItems) {
+ assertNextOrder(controller, forHardware, false /* onlyCurrentIme */, mode,
+ true /* forward */, allItems);
+ assertNextOrder(controller, forHardware, false /* onlyCurrentIme */, mode,
+ false /* forward */, allItems.reversed());
+
+ for (var imeItems : perImeItems) {
+ assertNextOrder(controller, forHardware, true /* onlyCurrentIme */, mode,
+ true /* forward */, imeItems);
+ assertNextOrder(controller, forHardware, true /* onlyCurrentIme */, mode,
+ false /* forward */, imeItems.reversed());
+ }
+ }
+
+ /**
+ * Verifies that the controller's next item order (starting from the first one in {@code items}
+ * matches the given on, and cycles back at the end. If a single item is given, verifies that
+ * no next item is returned.
+ *
+ * @param controller the controller to use for finding the next items.
+ * @param forHardware whether to find the next hardware item, or software item.
+ * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+ * @param mode the switching mode.
+ * @param forward whether to search forwards or backwards in the list.
+ * @param items the list of items to verify, in the expected order.
+ */
+ private static void assertNextOrder(@NonNull ControllerImpl controller,
+ boolean forHardware, boolean onlyCurrentIme, @SwitchMode int mode, boolean forward,
+ @NonNull List<ImeSubtypeListItem> items) {
+ final int numItems = items.size();
+ if (numItems == 0) {
+ return;
+ } else if (numItems == 1) {
+ // Single item controllers should never return a next item.
+ assertNextItem(controller, forHardware, onlyCurrentIme, mode, forward, items.get(0),
+ null /* expectedNext*/);
+ return;
+ }
+
+ var item = items.get(0);
+
+ final var expectedNextItems = new ArrayList<>(items);
+ // Add first item in the last position of expected order, to ensure the order is cyclic.
+ expectedNextItems.add(item);
+
+ final var nextItems = new ArrayList<>();
+ // Add first item in the first position of actual order, to ensure the order is cyclic.
+ nextItems.add(item);
+
+ // Compute the nextItems starting from the first given item, and compare the order.
+ for (int i = 0; i < numItems; i++) {
+ item = getNextItem(controller, forHardware, onlyCurrentIme, mode, forward, item);
+ assertNotNull("Next item shouldn't be null.", item);
+ nextItems.add(item);
+ }
+
+ assertEquals("Rotation order doesn't match.", expectedNextItems, nextItems);
+ }
+
+ /**
+ * Verifies that the controller gets the expected next value from the given item.
+ *
+ * @param controller the controller to sue for finding the next value.
+ * @param forHardware whether to find the next hardware item, or software item.
+ * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+ * @param mode the switching mode.
+ * @param forward whether to search forwards or backwards in the list.
+ * @param item the item to find the next value from.
+ * @param expectedNext the expected next value.
+ */
+ private static void assertNextItem(@NonNull ControllerImpl controller,
+ boolean forHardware, boolean onlyCurrentIme, @SwitchMode int mode, boolean forward,
+ @NonNull ImeSubtypeListItem item, @Nullable ImeSubtypeListItem expectedNext) {
+ final var nextItem = getNextItem(controller, forHardware, onlyCurrentIme, mode, forward,
+ item);
+ assertEquals("Next item doesn't match.", expectedNext, nextItem);
+ }
+
+ /**
+ * Gets the next value from the given item.
+ *
+ * @param controller the controller to use for finding the next value.
+ * @param forHardware whether to find the next hardware item, or software item.
+ * @param onlyCurrentIme whether to consider only subtypes of the current input method.
+ * @param mode the switching mode.
+ * @param forward whether to search forwards or backwards in the list.
+ * @param item the item to find the next value from.
+ * @return the next item found, otherwise {@code null}.
+ */
+ @Nullable
+ private static ImeSubtypeListItem getNextItem(@NonNull ControllerImpl controller,
+ boolean forHardware, boolean onlyCurrentIme, @SwitchMode int mode, boolean forward,
+ @NonNull ImeSubtypeListItem item) {
+ final var subtype = item.mSubtypeName != null
+ ? createTestSubtype(item.mSubtypeName.toString()) : null;
+ return forHardware
+ ? controller.getNextInputMethodForHardware(
+ onlyCurrentIme, item.mImi, subtype, mode, forward)
+ : controller.getNextInputMethod(
+ onlyCurrentIme, item.mImi, subtype, mode, forward);
+ }
+
+ /**
+ * Verifies that no next items can be found, and the recency cannot be updated for the
+ * given items.
+ *
+ * @param controller the controller to verify the items on.
+ * @param forHardware whether to try finding the next hardware item, or software item.
+ * @param items the list of items to verify.
+ */
+ private void assertNoAction(@NonNull ControllerImpl controller, boolean forHardware,
+ @NonNull List<ImeSubtypeListItem> items) {
+ for (var item : items) {
+ for (int mode = MODE_STATIC; mode <= MODE_AUTO; mode++) {
+ assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode,
+ false /* forward */, item, null /* expectedNext */);
+ assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode,
+ true /* forward */, item, null /* expectedNext */);
+ assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode,
+ false /* forward */, item, null /* expectedNext */);
+ assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode,
+ true /* forward */, item, null /* expectedNext */);
+ }
+
+ assertFalse("User action shouldn't have updated the recency.",
+ onUserAction(controller, item));
+ }
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index 564c29f..11e6d90 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -23,7 +23,6 @@
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
@@ -176,6 +175,7 @@
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity);
mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
callOnActivityConfigurationChanging(mActivity);
@@ -185,34 +185,6 @@
}
@Test
- public void testReconnectedToDifferentCamera_activatesCameraCompatModeAndRefresh()
- throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
-
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_2, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
-
- assertInCameraCompatMode();
- assertActivityRefreshRequested(/* refreshRequested */ true);
- }
-
- @Test
- public void testCameraDisconnected_deactivatesCameraCompatMode() {
- configureActivityAndDisplay(SCREEN_ORIENTATION_PORTRAIT, ORIENTATION_LANDSCAPE,
- WINDOWING_MODE_FREEFORM);
- // Open camera and test for compat treatment
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode();
-
- // Close camera and test for revert
- mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
-
- assertNotInCameraCompatMode();
- }
-
- @Test
public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
index e468fd8..1c8dc05 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
@@ -256,8 +256,7 @@
}
@Override
- public boolean onCameraClosed(@NonNull ActivityRecord cameraActivity,
- @NonNull String cameraId) {
+ public boolean onCameraClosed(@NonNull String cameraId) {
mOnCameraClosedCounter++;
boolean returnValue = mOnCameraClosedReturnValue;
// If false, return false only the first time, so it doesn't fall in the infinite retry
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 238f2af..5121f667 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -119,7 +119,7 @@
): UiObject2? {
if (
wmHelper.getWindow(innerHelper)?.windowingMode !=
- WindowingMode.WINDOWING_MODE_FREEFORM.value
+ WindowingMode.WINDOWING_MODE_FREEFORM.value
)
error("expected a freeform window with caption but window is not in freeform mode")
val captions =
@@ -147,7 +147,17 @@
val endX = startX + horizontalChange
// drag the specified corner of the window to the end coordinate.
- device.drag(startX, startY, endX, endY, 100)
+ dragWindow(startX, startY, endX, endY, wmHelper, device)
+ }
+
+ /** Drag a window from a source coordinate to a destination coordinate. */
+ fun dragWindow(
+ startX: Int, startY: Int,
+ endX: Int, endY: Int,
+ wmHelper: WindowManagerStateHelper,
+ device: UiDevice
+ ) {
+ device.drag(startX, startY, endX, endY, /* steps= */ 100)
wmHelper
.StateSyncBuilder()
.withAppTransitionIdle()
diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
index d0148fb..abfe549 100644
--- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
+++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
@@ -136,11 +136,20 @@
assumeTrue(enableVectorCursors())
assumeTrue(enableVectorCursorA11ySettings())
+ val theme: Resources.Theme = context.getResources().newTheme()
+ theme.setTo(context.getTheme())
+ theme.applyStyle(
+ PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK),
+ /* force= */ true)
+ theme.applyStyle(
+ PointerIcon.vectorStrokeStyleToResource(
+ PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE),
+ /* force= */ true)
val pointerScale = 2f
val pointerIcon =
PointerIcon.getLoadedSystemIcon(
- context,
+ ContextThemeWrapper(context, theme),
PointerIcon.TYPE_ARROW,
/* useLargeIcons= */ false,
pointerScale)