Revamp IME switcher menu

This revamps the UI of the IME switcher menu, to make it more modern,
and provide a seamless IME and subtype switching experience from the
system, regardless of the selected IME.

This also reworks the internals of the InputMethodMenuController to
decouple it from the InputMethodManagerService, and simplify it where
possible.

Test: atest
  InputMethodManagerTest#testShowInputMethodPicker
  InputMethodManagerTest#testInputMethodPickerSwitchIme
  InputMethodPickerTest#testInputMethodPicker_hidesUntrustedOverlays
  InputMethodPickerTest#testShowInputMethodPicker_noDismissWhenOverlayPopup
  InputMethodPickerTest#testShowImePickerOnExternalDisplay
  ImeInsetsVisibilityTest#testEditTextPositionAndPersistWhenAboveImeWindowShown
  InputMethodServiceLifecycleTest#testImeSwitchingWithoutWindowFocusAfterDisplayOffOnFull
  InputMethodServiceLifecycleTest#testImeSwitchingWithoutWindowFocusAfterDisplayOffOnInstant
Bug: 311791923
Flag: android.view.inputmethod.ime_switcher_revamp
Change-Id: I1005cb6b10682f3e91a7ed847c290fcc206b5faa
diff --git a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
index a3beaf4..209f323 100644
--- a/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
+++ b/core/java/android/inputmethodservice/navigationbar/NavigationBarView.java
@@ -216,7 +216,11 @@
                 oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection();
 
         if (densityChange || dirChange) {
-            mImeSwitcherIcon = getDrawable(com.android.internal.R.drawable.ic_ime_switcher);
+            final int switcherResId = Flags.imeSwitcherRevamp()
+                    ? com.android.internal.R.drawable.ic_ime_switcher_new
+                    : com.android.internal.R.drawable.ic_ime_switcher;
+
+            mImeSwitcherIcon = getDrawable(switcherResId);
         }
         if (orientationChange || densityChange || dirChange) {
             mBackIcon = getBackDrawable();
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index 098f655..0e66f7a 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -891,12 +891,13 @@
     @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP_API)
     @Nullable
     public Intent createImeLanguageSettingsActivityIntent() {
-        if (TextUtils.isEmpty(mLanguageSettingsActivityName)) {
+        final var activityName = !TextUtils.isEmpty(mLanguageSettingsActivityName)
+                ? mLanguageSettingsActivityName : mSettingsActivityName;
+        if (TextUtils.isEmpty(activityName)) {
             return null;
         }
         return new Intent(ACTION_IME_LANGUAGE_SETTINGS).setComponent(
-                new ComponentName(getServiceInfo().packageName,
-                        mLanguageSettingsActivityName)
+                new ComponentName(getServiceInfo().packageName, activityName)
         );
     }
 
diff --git a/core/java/com/android/internal/widget/MaxHeightFrameLayout.java b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java
new file mode 100644
index 0000000..d65dddd
--- /dev/null
+++ b/core/java/com/android/internal/widget/MaxHeightFrameLayout.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Px;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import com.android.internal.R;
+
+/**
+ * This custom subclass of FrameLayout enforces that its calculated height be no larger than the
+ * given maximum height (if any).
+ *
+ * @hide
+ */
+public class MaxHeightFrameLayout extends FrameLayout {
+
+    private int mMaxHeight = Integer.MAX_VALUE;
+
+    public MaxHeightFrameLayout(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public MaxHeightFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public MaxHeightFrameLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.MaxHeightFrameLayout, defStyleAttr, defStyleRes);
+        saveAttributeDataForStyleable(context, R.styleable.MaxHeightFrameLayout,
+                attrs, a, defStyleAttr, defStyleRes);
+
+        setMaxHeight(a.getDimensionPixelSize(R.styleable.MaxHeightFrameLayout_maxHeight,
+                Integer.MAX_VALUE));
+    }
+
+    /**
+     * Gets the maximum height of this view, in pixels.
+     *
+     * @see #setMaxHeight(int)
+     *
+     * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight
+     */
+    @Px
+    public int getMaxHeight() {
+        return mMaxHeight;
+    }
+
+    /**
+     * Sets the maximum height this view can have.
+     *
+     * @param maxHeight the maximum height, in pixels
+     *
+     * @see #getMaxHeight()
+     *
+     * @attr ref android.R.styleable#MaxHeightFrameLayout_maxHeight
+     */
+    public void setMaxHeight(@Px int maxHeight) {
+        mMaxHeight = maxHeight;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (MeasureSpec.getSize(heightMeasureSpec) > mMaxHeight) {
+            final int mode = MeasureSpec.getMode(heightMeasureSpec);
+            heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, mode);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+}
diff --git a/core/res/res/drawable/ic_ime_switcher_new.xml b/core/res/res/drawable/ic_ime_switcher_new.xml
new file mode 100644
index 0000000..04f4a25
--- /dev/null
+++ b/core/res/res/drawable/ic_ime_switcher_new.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="20dp"
+        android:height="20dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z"
+        android:fillColor="#FFFFFFFF"/>
+</vector>
diff --git a/core/res/res/drawable/input_method_switch_button.xml b/core/res/res/drawable/input_method_switch_button.xml
new file mode 100644
index 0000000..396d81e
--- /dev/null
+++ b/core/res/res/drawable/input_method_switch_button.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:insetTop="6dp"
+    android:insetBottom="6dp">
+    <ripple android:color="?android:attr/colorControlHighlight">
+        <item android:id="@android:id/mask">
+            <shape android:shape="rectangle">
+                <corners android:radius="28dp"/>
+                <solid android:color="@color/white"/>
+            </shape>
+        </item>
+
+        <item>
+            <shape android:shape="rectangle">
+                <corners android:radius="28dp"/>
+                <solid android:color="@color/transparent"/>
+                <stroke android:color="?attr/materialColorPrimary"
+                    android:width="1dp"/>
+                <padding android:left="16dp"
+                    android:top="8dp"
+                    android:right="16dp"
+                    android:bottom="8dp"/>
+            </shape>
+        </item>
+    </ripple>
+</inset>
diff --git a/core/res/res/drawable/input_method_switch_item_background.xml b/core/res/res/drawable/input_method_switch_item_background.xml
new file mode 100644
index 0000000..eb7a246
--- /dev/null
+++ b/core/res/res/drawable/input_method_switch_item_background.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/list_highlight_material">
+    <item android:id="@id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="28dp"/>
+            <solid android:color="@color/white"/>
+        </shape>
+    </item>
+
+    <item>
+        <selector>
+            <item android:state_activated="true">
+                <shape android:shape="rectangle">
+                    <corners android:radius="28dp"/>
+                    <solid android:color="?attr/materialColorSecondaryContainer"/>
+                </shape>
+            </item>
+        </selector>
+    </item>
+</ripple>
diff --git a/core/res/res/layout/input_method_switch_dialog_new.xml b/core/res/res/layout/input_method_switch_dialog_new.xml
new file mode 100644
index 0000000..5a4d6b1
--- /dev/null
+++ b/core/res/res/layout/input_method_switch_dialog_new.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <com.android.internal.widget.MaxHeightFrameLayout
+        android:layout_width="320dp"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:maxHeight="373dp">
+
+        <com.android.internal.widget.RecyclerView
+            android:id="@+id/list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingVertical="8dp"
+            android:clipToPadding="false"
+            android:layoutManager="com.android.internal.widget.LinearLayoutManager"/>
+
+    </com.android.internal.widget.MaxHeightFrameLayout>
+
+    <LinearLayout
+        style="?android:attr/buttonBarStyle"
+        android:id="@+id/button_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:paddingHorizontal="16dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="16dp"
+        android:visibility="gone">
+
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"/>
+
+        <Button
+            style="?attr/buttonBarButtonStyle"
+            android:id="@+id/button1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/input_method_switch_button"
+            android:layout_gravity="end"
+            android:text="@string/input_method_language_settings"
+            android:fontFamily="google-sans-text"
+            android:textAppearance="?attr/textAppearance"
+            android:visibility="gone"/>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/core/res/res/layout/input_method_switch_item_new.xml b/core/res/res/layout/input_method_switch_item_new.xml
new file mode 100644
index 0000000..16a97c4
--- /dev/null
+++ b/core/res/res/layout/input_method_switch_item_new.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingHorizontal="16dp"
+    android:paddingBottom="8dp">
+
+    <View
+        android:id="@+id/divider"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="?attr/materialColorSurfaceVariant"
+        android:layout_marginStart="20dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="24dp"
+        android:layout_marginBottom="12dp"
+        android:visibility="gone"/>
+
+    <TextView
+        android:id="@+id/header_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="8dp"
+        android:ellipsize="end"
+        android:singleLine="true"
+        android:fontFamily="google-sans-text"
+        android:textAppearance="?attr/textAppearance"
+        android:textColor="?attr/materialColorPrimary"
+        android:visibility="gone"/>
+
+    <LinearLayout
+        android:id="@+id/list_item"
+        android:layout_width="match_parent"
+        android:layout_height="72dp"
+        android:background="@drawable/input_method_switch_item_background"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        android:paddingStart="20dp"
+        android:paddingEnd="24dp">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:gravity="start|center_vertical"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/text"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ellipsize="marquee"
+                android:singleLine="true"
+                android:fontFamily="google-sans-text"
+                android:textAppearance="?attr/textAppearanceListItem"/>
+
+        </LinearLayout>
+
+        <ImageView
+            android:id="@+id/image"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:gravity="center_vertical"
+            android:layout_marginStart="12dp"
+            android:src="@drawable/ic_check_24dp"
+            android:tint="?attr/materialColorOnSurface"
+            android:visibility="gone"/>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 0975eda..7cc9e13 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5243,6 +5243,11 @@
              the VISIBLE or INVISIBLE state when measuring. Defaults to false. -->
         <attr name="measureAllChildren" format="boolean" />
     </declare-styleable>
+    <!-- @hide -->
+    <declare-styleable name="MaxHeightFrameLayout">
+        <!-- An optional argument to supply a maximum height for this view. -->
+        <attr name="maxHeight" format="dimension" />
+    </declare-styleable>
     <declare-styleable name="ExpandableListView">
         <!-- Indicator shown beside the group View. This can be a stateful Drawable. -->
         <attr name="groupIndicator" format="reference" />
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 46b15416..ec865f6 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3880,6 +3880,8 @@
 
     <!-- Title of the pop-up dialog in which the user switches keyboard, also known as input method. -->
     <string name="select_input_method">Choose input method</string>
+    <!-- Button to access the language settings of the current input method. [CHAR LIMIT=50]-->
+    <string name="input_method_language_settings">Language Settings</string>
     <!-- Summary text of a toggle switch to enable/disable use of the IME while a physical
          keyboard is connected -->
     <string name="show_ime">Keep it on screen while physical keyboard is active</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c50b961..fcafdae 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1577,6 +1577,8 @@
   <java-symbol type="layout" name="input_method" />
   <java-symbol type="layout" name="input_method_extract_view" />
   <java-symbol type="layout" name="input_method_switch_item" />
+  <java-symbol type="layout" name="input_method_switch_item_new" />
+  <java-symbol type="layout" name="input_method_switch_dialog_new" />
   <java-symbol type="layout" name="input_method_switch_dialog_title" />
   <java-symbol type="layout" name="js_prompt" />
   <java-symbol type="layout" name="list_content_simple" />
@@ -2552,6 +2554,7 @@
   <java-symbol type="dimen" name="input_method_nav_key_button_ripple_max_width" />
   <java-symbol type="drawable" name="ic_ime_nav_back" />
   <java-symbol type="drawable" name="ic_ime_switcher" />
+  <java-symbol type="drawable" name="ic_ime_switcher_new" />
   <java-symbol type="id" name="input_method_nav_back" />
   <java-symbol type="id" name="input_method_nav_buttons" />
   <java-symbol type="id" name="input_method_nav_center_group" />
@@ -5400,6 +5403,7 @@
   <java-symbol type="style" name="Theme.DeviceDefault.DialogWhenLarge" />
   <java-symbol type="style" name="Theme.DeviceDefault.DocumentsUI" />
   <java-symbol type="style" name="Theme.DeviceDefault.InputMethod" />
+  <java-symbol type="style" name="Theme.DeviceDefault.InputMethodSwitcherDialog" />
   <java-symbol type="style" name="Theme.DeviceDefault.Light.DarkActionBar" />
   <java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.FixedSize" />
   <java-symbol type="style" name="Theme.DeviceDefault.Light.Dialog.MinWidth" />
diff --git a/core/res/res/values/themes_device_defaults.xml b/core/res/res/values/themes_device_defaults.xml
index 382ff04..f5c6738 100644
--- a/core/res/res/values/themes_device_defaults.xml
+++ b/core/res/res/values/themes_device_defaults.xml
@@ -6179,4 +6179,10 @@
         <item name="colorListDivider">@color/list_divider_opacity_device_default_light</item>
         <item name="opacityListDivider">@color/list_divider_opacity_device_default_light</item>
     </style>
+
+    <!--  Device default theme for the Input Method Switcher dialog.  -->
+    <style name="Theme.DeviceDefault.InputMethodSwitcherDialog" parent="Theme.DeviceDefault.Dialog.Alert.DayNight">
+        <item name="windowMinWidthMajor">@null</item>
+        <item name="windowMinWidthMinor">@null</item>
+    </style>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
index 1dbd500..c4abcd2 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBarView.java
@@ -54,6 +54,7 @@
 import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.inputmethod.Flags;
 import android.widget.FrameLayout;
 
 import androidx.annotation.Nullable;
@@ -285,8 +286,11 @@
 
         // Set up the context group of buttons
         mContextualButtonGroup = new ContextualButtonGroup(R.id.menu_container);
+        final int switcherResId = Flags.imeSwitcherRevamp()
+                ? com.android.internal.R.drawable.ic_ime_switcher_new
+                : R.drawable.ic_ime_switcher_default;
         final ContextualButton imeSwitcherButton = new ContextualButton(R.id.ime_switcher,
-                mLightContext, R.drawable.ic_ime_switcher_default);
+                mLightContext, switcherResId);
         final ContextualButton accessibilityButton =
                 new ContextualButton(R.id.accessibility_button, mLightContext,
                         R.drawable.ic_sysbar_accessibility_button);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
index f61ca61..c82e5be 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java
@@ -16,6 +16,8 @@
 
 package com.android.server.inputmethod;
 
+import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID;
+
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.annotation.NonNull;
@@ -110,7 +112,7 @@
             InlineSuggestionsRequestInfo requestInfo, InlineSuggestionsRequestCallback cb);
 
     /**
-     * Force switch to the enabled input method by {@code imeId} for current user. If the input
+     * Force switch to the enabled input method by {@code imeId} for the current user. If the input
      * method with {@code imeId} is not enabled or not installed, do nothing.
      *
      * @param imeId  the input method ID to be switched to
@@ -119,7 +121,25 @@
      * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available
      * to be switched.
      */
-    public abstract boolean switchToInputMethod(String imeId, @UserIdInt int userId);
+    public boolean switchToInputMethod(@NonNull String imeId, @UserIdInt int userId) {
+        return switchToInputMethod(imeId, NOT_A_SUBTYPE_ID, userId);
+    }
+
+    /**
+     * Force switch to the enabled input method by {@code imeId} for the current user. If the input
+     * method with {@code imeId} is not enabled or not installed, do nothing. If {@code subtypeId}
+     * is also supplied (not {@link InputMethodUtils#NOT_A_SUBTYPE_ID}) and valid, also switches to
+     * it, otherwise the system decides the most sensible default subtype to use.
+     *
+     * @param imeId the input method ID to be switched to
+     * @param subtypeId the input method subtype ID to be switched to
+     * @param userId the user ID to be queried
+     * @return {@code true} if the current input method was successfully switched to the input
+     * method by {@code imeId}; {@code false} the input method with {@code imeId} is not available
+     * to be switched.
+     */
+    public abstract boolean switchToInputMethod(@NonNull String imeId, int subtypeId,
+            @UserIdInt int userId);
 
     /**
      * Force enable or disable the input method associated with {@code imeId} for given user. If
@@ -211,6 +231,15 @@
     public abstract void updateImeWindowStatus(boolean disableImeIcon, int displayId);
 
     /**
+     * Updates and reports whether the IME switcher button should be shown, regardless whether
+     * SystemUI or the IME is responsible for drawing it and the corresponding navigation bar.
+     *
+     * @param displayId the display for which to update the IME switcher button visibility.
+     * @param userId    the user for which to update the IME switcher button visibility.
+     */
+    public abstract void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId);
+
+    /**
      * Finish stylus handwriting by calling {@link InputMethodService#finishStylusHandwriting()} if
      * there is an ongoing handwriting session.
      */
@@ -290,7 +319,8 @@
                 }
 
                 @Override
-                public boolean switchToInputMethod(String imeId, @UserIdInt int userId) {
+                public boolean switchToInputMethod(@NonNull String imeId, int subtypeId,
+                        @UserIdInt int userId) {
                     return false;
                 }
 
@@ -335,6 +365,10 @@
                 }
 
                 @Override
+                public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) {
+                }
+
+                @Override
                 public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
                         IAccessibilityInputMethodSession session, @UserIdInt int userId) {
                 }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 0b3aaef..2eef50c 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -181,6 +181,7 @@
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.input.InputManagerInternal;
 import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener;
+import com.android.server.inputmethod.InputMethodMenuControllerNew.MenuItem;
 import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.statusbar.StatusBarManagerInternal;
@@ -360,6 +361,7 @@
     private final UserManagerInternal mUserManagerInternal;
     @MultiUserUnawareField
     private final InputMethodMenuController mMenuController;
+    private final InputMethodMenuControllerNew mMenuControllerNew;
 
     @GuardedBy("ImfLock.class")
     @MultiUserUnawareField
@@ -566,7 +568,9 @@
         }
         switch (key) {
             case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: {
-                mMenuController.updateKeyboardFromSettingsLocked();
+                if (!Flags.imeSwitcherRevamp()) {
+                    mMenuController.updateKeyboardFromSettingsLocked();
+                }
                 break;
             }
             case Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE: {
@@ -631,7 +635,15 @@
                         }
                     }
                 }
-                mMenuController.hideInputMethodMenu();
+                if (Flags.imeSwitcherRevamp()) {
+                    synchronized (ImfLock.class) {
+                        final var bindingController = getInputMethodBindingController(senderUserId);
+                        mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(),
+                                senderUserId);
+                    }
+                } else {
+                    mMenuController.hideInputMethodMenu();
+                }
             } else {
                 Slog.w(TAG, "Unexpected intent " + intent);
             }
@@ -1171,6 +1183,8 @@
                             : bindingControllerFactory);
 
             mMenuController = new InputMethodMenuController(this);
+            mMenuControllerNew = Flags.imeSwitcherRevamp()
+                    ? new InputMethodMenuControllerNew() : null;
             mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
             mVisibilityApplier = new DefaultImeVisibilityApplier(this);
 
@@ -1782,7 +1796,11 @@
                     ImeTracker.PHASE_SERVER_WAIT_IME);
             userData.mCurStatsToken = null;
             // TODO: Make mMenuController multi-user aware
-            mMenuController.hideInputMethodMenuLocked();
+            if (Flags.imeSwitcherRevamp()) {
+                mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId);
+            } else {
+                mMenuController.hideInputMethodMenuLocked();
+            }
         }
     }
 
@@ -2599,7 +2617,12 @@
         if (!mShowOngoingImeSwitcherForPhones) return false;
         // When the IME switcher dialog is shown, the IME switcher button should be hidden.
         // TODO(b/305849394): Make mMenuController multi-user aware.
-        if (mMenuController.getSwitchingDialogLocked() != null) return false;
+        final boolean switcherMenuShowing = Flags.imeSwitcherRevamp()
+                ? mMenuControllerNew.isShowing()
+                : mMenuController.getSwitchingDialogLocked() != null;
+        if (switcherMenuShowing) {
+            return false;
+        }
         // When we are switching IMEs, the IME switcher button should be hidden.
         final var bindingController = getInputMethodBindingController(userId);
         if (!Objects.equals(bindingController.getCurId(),
@@ -2614,7 +2637,7 @@
                 || (visibility & InputMethodService.IME_INVISIBLE) != 0) {
             return false;
         }
-        if (mWindowManagerInternal.isHardKeyboardAvailable()) {
+        if (mWindowManagerInternal.isHardKeyboardAvailable() && !Flags.imeSwitcherRevamp()) {
             // When physical keyboard is attached, we show the ime switcher (or notification if
             // NavBar is not available) because SHOW_IME_WITH_HARD_KEYBOARD settings currently
             // exists in the IME switcher dialog.  Might be OK to remove this condition once
@@ -2625,6 +2648,15 @@
         }
 
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
+        if (Flags.imeSwitcherRevamp()) {
+            // The IME switcher button should be shown when the current IME specified a
+            // language settings activity.
+            final var curImi = settings.getMethodMap().get(settings.getSelectedInputMethod());
+            if (curImi != null && curImi.createImeLanguageSettingsActivityIntent() != null) {
+                return true;
+            }
+        }
+
         return hasMultipleSubtypesForSwitcher(false /* nonAuxOnly */, settings);
     }
 
@@ -2794,7 +2826,10 @@
             }
             final var curId = bindingController.getCurId();
             // TODO(b/305849394): Make mMenuController multi-user aware.
-            if (mMenuController.getSwitchingDialogLocked() != null
+            final boolean switcherMenuShowing = Flags.imeSwitcherRevamp()
+                    ? mMenuControllerNew.isShowing()
+                    : mMenuController.getSwitchingDialogLocked() != null;
+            if (switcherMenuShowing
                     || !Objects.equals(curId, bindingController.getSelectedMethodId())) {
                 // When the IME switcher dialog is shown, or we are switching IMEs,
                 // the back button should be in the default state (as if the IME is not shown).
@@ -2813,7 +2848,9 @@
     @GuardedBy("ImfLock.class")
     void updateFromSettingsLocked(boolean enabledMayChange, @UserIdInt int userId) {
         updateInputMethodsFromSettingsLocked(enabledMayChange, userId);
-        mMenuController.updateKeyboardFromSettingsLocked();
+        if (!Flags.imeSwitcherRevamp()) {
+            mMenuController.updateKeyboardFromSettingsLocked();
+        }
     }
 
     /**
@@ -3979,10 +4016,70 @@
     @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD)
     public boolean isInputMethodPickerShownForTest() {
         synchronized (ImfLock.class) {
-            return mMenuController.isisInputMethodPickerShownForTestLocked();
+            return Flags.imeSwitcherRevamp()
+                    ? mMenuControllerNew.isShowing()
+                    : mMenuController.isisInputMethodPickerShownForTestLocked();
         }
     }
 
+    /**
+     * Gets the list of Input Method Switcher Menu items and the index of the selected item.
+     *
+     * @param items             the list of input method and subtype items.
+     * @param selectedImeId     the ID of the selected input method.
+     * @param selectedSubtypeId the ID of the selected input method subtype,
+     *                          or {@link #NOT_A_SUBTYPE_ID} if no subtype is selected.
+     * @param userId            the ID of the user for which to get the menu items.
+     * @return the list of menu items, and the index of the selected item,
+     * or {@code -1} if no item is selected.
+     */
+    @GuardedBy("ImfLock.class")
+    @NonNull
+    private Pair<List<MenuItem>, Integer> getInputMethodPickerItems(
+            @NonNull List<ImeSubtypeListItem> items, @Nullable String selectedImeId,
+            int selectedSubtypeId, @UserIdInt int userId) {
+        final var bindingController = getInputMethodBindingController(userId);
+        final var settings = InputMethodSettingsRepository.get(userId);
+
+        if (selectedSubtypeId == NOT_A_SUBTYPE_ID) {
+            // TODO(b/351124299): Check if this fallback logic is still necessary.
+            final var curSubtype = bindingController.getCurrentInputMethodSubtype();
+            if (curSubtype != null) {
+                final var curMethodId = bindingController.getSelectedMethodId();
+                final var curImi = settings.getMethodMap().get(curMethodId);
+                selectedSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(
+                        curImi, curSubtype.hashCode());
+            }
+        }
+
+        // No item is selected by default. When we have a list of explicitly enabled
+        // subtypes, the implicit subtype is no longer listed. If the implicit one
+        // is still selected, no items will be shown as selected.
+        int selectedIndex = -1;
+        String prevImeId = null;
+        final var menuItems = new ArrayList<MenuItem>();
+        for (int i = 0; i < items.size(); i++) {
+            final var item = items.get(i);
+            final var imeId = item.mImi.getId();
+            if (imeId.equals(selectedImeId)) {
+                final int subtypeId = item.mSubtypeId;
+                // Check if this is the selected IME-subtype pair.
+                if ((subtypeId == 0 && selectedSubtypeId == NOT_A_SUBTYPE_ID)
+                        || subtypeId == NOT_A_SUBTYPE_ID
+                        || subtypeId == selectedSubtypeId) {
+                    selectedIndex = i;
+                }
+            }
+            final boolean hasHeader = !imeId.equals(prevImeId);
+            final boolean hasDivider = hasHeader && prevImeId != null;
+            prevImeId = imeId;
+            menuItems.add(new MenuItem(item.mImeName, item.mSubtypeName, item.mImi, item.mSubtypeId,
+                    hasHeader, hasDivider));
+        }
+
+        return new Pair<>(menuItems, selectedIndex);
+    }
+
     @BinderThread
     private void onImeSwitchButtonClickFromClient(@NonNull IBinder token, int displayId,
             @NonNull UserData userData) {
@@ -4625,7 +4722,10 @@
             proto.write(IS_INTERACTIVE, mIsInteractive);
             proto.write(BACK_DISPOSITION, bindingController.getBackDisposition());
             proto.write(IME_WINDOW_VISIBILITY, bindingController.getImeWindowVis());
-            proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard());
+            if (!Flags.imeSwitcherRevamp()) {
+                proto.write(SHOW_IME_WITH_HARD_KEYBOARD,
+                        mMenuController.getShowImeWithHardKeyboard());
+            }
             proto.write(CONCURRENT_MULTI_USER_MODE_ENABLED, mConcurrentMultiUserModeEnabled);
             proto.end(token);
         }
@@ -4931,8 +5031,9 @@
                 synchronized (ImfLock.class) {
                     final InputMethodSettings settings =
                             InputMethodSettingsRepository.get(mCurrentUserId);
+                    final int userId = settings.getUserId();
                     final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
-                            && mWindowManagerInternal.isKeyguardSecure(settings.getUserId());
+                            && mWindowManagerInternal.isKeyguardSecure(userId);
                     final String lastInputMethodId = settings.getSelectedInputMethod();
                     int lastInputMethodSubtypeId =
                             settings.getSelectedInputMethodSubtypeId(lastInputMethodId);
@@ -4945,12 +5046,35 @@
                         Slog.w(TAG, "Show switching menu failed, imList is empty,"
                                 + " showAuxSubtypes: " + showAuxSubtypes
                                 + " isScreenLocked: " + isScreenLocked
-                                + " userId: " + settings.getUserId());
+                                + " userId: " + userId);
                         return false;
                     }
 
-                    mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
-                            lastInputMethodId, lastInputMethodSubtypeId, imList);
+                    if (Flags.imeSwitcherRevamp()) {
+                        if (DEBUG) {
+                            Slog.v(TAG, "Show IME switcher menu,"
+                                    + " showAuxSubtypes=" + showAuxSubtypes
+                                    + " displayId=" + displayId
+                                    + " preferredInputMethodId=" + lastInputMethodId
+                                    + " preferredInputMethodSubtypeId=" + lastInputMethodSubtypeId);
+                        }
+
+                        final var itemsAndIndex = getInputMethodPickerItems(imList,
+                                lastInputMethodId, lastInputMethodSubtypeId, userId);
+                        final var menuItems = itemsAndIndex.first;
+                        final int selectedIndex = itemsAndIndex.second;
+
+                        if (selectedIndex == -1) {
+                            Slog.w(TAG, "Switching menu shown with no item selected"
+                                    + ", IME id: " + lastInputMethodId
+                                    + ", subtype index: " + lastInputMethodSubtypeId);
+                        }
+
+                        mMenuControllerNew.show(menuItems, selectedIndex, displayId, userId);
+                    } else {
+                        mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
+                                lastInputMethodId, lastInputMethodSubtypeId, imList);
+                    }
                 }
                 return true;
 
@@ -5021,7 +5145,9 @@
 
             // --------------------------------------------------------------
             case MSG_HARD_KEYBOARD_SWITCH_CHANGED:
-                mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1);
+                if (!Flags.imeSwitcherRevamp()) {
+                    mMenuController.handleHardKeyboardStatusChange(msg.arg1 == 1);
+                }
                 synchronized (ImfLock.class) {
                     sendOnNavButtonFlagsChangedToAllImesLocked();
                 }
@@ -5591,7 +5717,8 @@
     }
 
     @GuardedBy("ImfLock.class")
-    private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) {
+    private boolean switchToInputMethodLocked(@NonNull String imeId, int subtypeId,
+            @UserIdInt int userId) {
         final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
         if (mConcurrentMultiUserModeEnabled || userId == mCurrentUserId) {
             if (!settings.getMethodMap().containsKey(imeId)
@@ -5599,7 +5726,7 @@
                     .contains(settings.getMethodMap().get(imeId))) {
                 return false; // IME is not found or not enabled.
             }
-            setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID, userId);
+            setInputMethodLocked(imeId, subtypeId, userId);
             return true;
         }
         if (!settings.getMethodMap().containsKey(imeId)
@@ -5608,6 +5735,7 @@
             return false; // IME is not found or not enabled.
         }
         settings.putSelectedInputMethod(imeId);
+        // For non-current user, only reset subtypeId (instead of setting the given one).
         settings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
         return true;
     }
@@ -5753,9 +5881,10 @@
         }
 
         @Override
-        public boolean switchToInputMethod(String imeId, @UserIdInt int userId) {
+        public boolean switchToInputMethod(@NonNull String imeId, int subtypeId,
+                @UserIdInt int userId) {
             synchronized (ImfLock.class) {
-                return switchToInputMethodLocked(imeId, userId);
+                return switchToInputMethodLocked(imeId, subtypeId, userId);
             }
         }
 
@@ -5852,7 +5981,12 @@
                 // input target changed, in case seeing the dialog dismiss flickering during
                 // the next focused window starting the input connection.
                 if (mLastImeTargetWindow != userData.mImeBindingState.mFocusedWindow) {
-                    mMenuController.hideInputMethodMenuLocked();
+                    if (Flags.imeSwitcherRevamp()) {
+                        final var bindingController = getInputMethodBindingController(userId);
+                        mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId);
+                    } else {
+                        mMenuController.hideInputMethodMenuLocked();
+                    }
                 }
             }
         }
@@ -5871,6 +6005,15 @@
         }
 
         @Override
+        public void updateShouldShowImeSwitcher(int displayId, @UserIdInt int userId) {
+            synchronized (ImfLock.class) {
+                updateSystemUiLocked(userId);
+                final var userData = getUserData(userId);
+                sendOnNavButtonFlagsChangedLocked(userData);
+            }
+        }
+
+        @Override
         public void onSessionForAccessibilityCreated(int accessibilityConnectionId,
                 IAccessibilityInputMethodSession session, @UserIdInt int userId) {
             synchronized (ImfLock.class) {
@@ -6192,6 +6335,10 @@
                     };
             mUserDataRepository.forAllUserData(userDataDump);
 
+            if (Flags.imeSwitcherRevamp()) {
+                p.println("  menuControllerNew:");
+                mMenuControllerNew.dump(p, "  ");
+            }
             p.println("  mCurToken=" + bindingController.getCurToken());
             p.println("  mCurTokenDisplayId=" + bindingController.getCurTokenDisplayId());
             p.println("  mCurHostInputToken=" + bindingController.getCurHostInputToken());
@@ -6638,7 +6785,7 @@
                         continue;
                     }
                     boolean failedToSelectUnknownIme = !switchToInputMethodLocked(imeId,
-                            userId);
+                            NOT_A_SUBTYPE_ID, userId);
                     if (failedToSelectUnknownIme) {
                         error.print("Unknown input method ");
                         error.print(imeId);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
new file mode 100644
index 0000000..cbb1807
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuControllerNew.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+
+import static com.android.server.inputmethod.InputMethodManagerService.DEBUG;
+import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_ID;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Printer;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodInfo;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.widget.RecyclerView;
+
+import java.util.List;
+
+/**
+ * Controller for showing and hiding the Input Method Switcher Menu.
+ */
+final class InputMethodMenuControllerNew {
+
+    private static final String TAG = InputMethodMenuControllerNew.class.getSimpleName();
+
+    /**
+     * The horizontal offset from the menu to the edge of the screen corresponding
+     * to {@link Gravity#END}.
+     */
+    private static final int HORIZONTAL_OFFSET = 16;
+
+    /** The title of the window, used for debugging. */
+    private static final String WINDOW_TITLE = "IME Switcher Menu";
+
+    private final InputMethodDialogWindowContext mDialogWindowContext =
+            new InputMethodDialogWindowContext();
+
+    @Nullable
+    private AlertDialog mDialog;
+
+    @Nullable
+    private List<MenuItem> mMenuItems;
+
+    /**
+     * Shows the Input Method Switcher Menu, with a list of IMEs and their subtypes.
+     *
+     * @param items         the list of menu items.
+     * @param selectedIndex the index of the menu item that is selected.
+     *                      If no other IMEs are enabled, this index will be out of reach.
+     * @param displayId     the ID of the display where the menu was requested.
+     * @param userId        the ID of the user that requested the menu.
+     */
+    void show(@NonNull List<MenuItem> items, int selectedIndex, int displayId,
+            @UserIdInt int userId) {
+        // Hide the menu in case it was already showing.
+        hide(displayId, userId);
+
+        final Context dialogWindowContext = mDialogWindowContext.get(displayId);
+        final var builder = new AlertDialog.Builder(dialogWindowContext,
+                com.android.internal.R.style.Theme_DeviceDefault_InputMethodSwitcherDialog);
+        final var inflater = LayoutInflater.from(builder.getContext());
+
+        // Create the content view.
+        final View contentView = inflater
+                .inflate(com.android.internal.R.layout.input_method_switch_dialog_new, null);
+        contentView.setAccessibilityPaneTitle(
+                dialogWindowContext.getText(com.android.internal.R.string.select_input_method));
+        builder.setView(contentView);
+
+        final DialogInterface.OnClickListener onClickListener = (dialog, which) -> {
+            if (which != selectedIndex) {
+                final var item = items.get(which);
+                InputMethodManagerInternal.get()
+                        .switchToInputMethod(item.mImi.getId(), item.mSubtypeId, userId);
+            }
+            hide(displayId, userId);
+        };
+
+        final var selectedImi = selectedIndex >= 0 ? items.get(selectedIndex).mImi : null;
+        final var languageSettingsIntent = selectedImi != null
+                ? selectedImi.createImeLanguageSettingsActivityIntent() : null;
+        final boolean hasLanguageSettingsButton = languageSettingsIntent != null;
+        if (hasLanguageSettingsButton) {
+            final View buttonBar = contentView
+                    .requireViewById(com.android.internal.R.id.button_bar);
+            buttonBar.setVisibility(View.VISIBLE);
+
+            languageSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            final Button languageSettingsButton = contentView
+                    .requireViewById(com.android.internal.R.id.button1);
+            languageSettingsButton.setVisibility(View.VISIBLE);
+            languageSettingsButton.setOnClickListener(v -> {
+                v.getContext().startActivity(languageSettingsIntent);
+                hide(displayId, userId);
+            });
+        }
+
+        // Create the current IME subtypes list.
+        final RecyclerView recyclerView = contentView
+                .requireViewById(com.android.internal.R.id.list);
+        recyclerView.setAdapter(new Adapter(items, selectedIndex, inflater, onClickListener));
+        // Scroll to the currently selected IME.
+        recyclerView.scrollToPosition(selectedIndex);
+        // Indicate that the list can be scrolled.
+        recyclerView.setScrollIndicators(
+                hasLanguageSettingsButton ? View.SCROLL_INDICATOR_BOTTOM : 0);
+
+        builder.setOnCancelListener(dialog -> hide(displayId, userId));
+        mMenuItems = items;
+        mDialog = builder.create();
+        mDialog.setCanceledOnTouchOutside(true);
+        final Window w = mDialog.getWindow();
+        w.setHideOverlayWindows(true);
+        final WindowManager.LayoutParams attrs = w.getAttributes();
+        // Use an alternate token for the dialog for that window manager can group the token
+        // with other IME windows based on type vs. grouping based on whichever token happens
+        // to get selected by the system later on.
+        attrs.token = dialogWindowContext.getWindowContextToken();
+        attrs.gravity = Gravity.getAbsoluteGravity(Gravity.BOTTOM | Gravity.END,
+                dialogWindowContext.getResources().getConfiguration().getLayoutDirection());
+        attrs.x = HORIZONTAL_OFFSET;
+        attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+        attrs.type = WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
+        // Used for debugging only, not user visible.
+        attrs.setTitle(WINDOW_TITLE);
+        w.setAttributes(attrs);
+
+        mDialog.show();
+        InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId);
+    }
+
+    /**
+     * Hides the Input Method Switcher Menu.
+     *
+     * @param displayId the ID of the display from where the menu should be hidden.
+     * @param userId    the ID of the user for which the menu should be hidden.
+     */
+    void hide(int displayId, @UserIdInt int userId) {
+        if (DEBUG) Slog.v(TAG, "Hide IME switcher menu.");
+
+        mMenuItems = null;
+        // Cannot use dialog.isShowing() here, as the cancel listener flow already resets mShowing.
+        if (mDialog != null) {
+            mDialog.dismiss();
+            mDialog = null;
+
+            InputMethodManagerInternal.get().updateShouldShowImeSwitcher(displayId, userId);
+        }
+    }
+
+    /**
+     * Returns whether the Input Method Switcher Menu is showing.
+     */
+    boolean isShowing() {
+        return mDialog != null && mDialog.isShowing();
+    }
+
+    void dump(@NonNull Printer pw, @NonNull String prefix) {
+        final boolean showing = isShowing();
+        pw.println(prefix + "  isShowing: " + showing);
+
+        if (showing) {
+            pw.println(prefix + "  menuItems: " + mMenuItems);
+        }
+    }
+
+    /**
+     * Item to be shown in the Input Method Switcher Menu, containing an input method and
+     * optionally an input method subtype.
+     */
+    static class MenuItem {
+
+        /** The name of the input method. */
+        @NonNull
+        private final CharSequence mImeName;
+
+        /**
+         * The name of the input method subtype, or {@code null} if this item doesn't have a
+         * subtype.
+         */
+        @Nullable
+        private final CharSequence mSubtypeName;
+
+        /** The info of the input method. */
+        @NonNull
+        private final InputMethodInfo mImi;
+
+        /**
+         * The index of the subtype in the input method's array of subtypes,
+         * or {@link InputMethodUtils#NOT_A_SUBTYPE_ID} if this item doesn't have a subtype.
+         */
+        @IntRange(from = NOT_A_SUBTYPE_ID)
+        private final int mSubtypeId;
+
+        /** Whether this item has a group header (only the first item of each input method). */
+        private final boolean mHasHeader;
+
+        /**
+         * Whether this item should has a group divider (same as {@link #mHasHeader},
+         * excluding the first IME).
+         */
+        private final boolean mHasDivider;
+
+        MenuItem(@NonNull CharSequence imeName, @Nullable CharSequence subtypeName,
+                @NonNull InputMethodInfo imi, @IntRange(from = NOT_A_SUBTYPE_ID) int subtypeId,
+                boolean hasHeader, boolean hasDivider) {
+            mImeName = imeName;
+            mSubtypeName = subtypeName;
+            mImi = imi;
+            mSubtypeId = subtypeId;
+            mHasHeader = hasHeader;
+            mHasDivider = hasDivider;
+        }
+
+        @Override
+        public String toString() {
+            return "MenuItem{"
+                    + "mImeName=" + mImeName
+                    + " mSubtypeName=" + mSubtypeName
+                    + " mSubtypeId=" + mSubtypeId
+                    + " mHasHeader=" + mHasHeader
+                    + " mHasDivider=" + mHasDivider
+                    + "}";
+        }
+    }
+
+    private static class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
+
+        /** The list of items to show. */
+        @NonNull
+        private final List<MenuItem> mItems;
+        /** The index of the selected item. */
+        private final int mSelectedIndex;
+        @NonNull
+        private final LayoutInflater mInflater;
+        @NonNull
+        private final DialogInterface.OnClickListener mOnClickListener;
+
+        Adapter(@NonNull List<MenuItem> items, int selectedIndex,
+                @NonNull LayoutInflater inflater,
+                @NonNull DialogInterface.OnClickListener onClickListener) {
+            mItems = items;
+            mSelectedIndex = selectedIndex;
+            mInflater = inflater;
+            mOnClickListener = onClickListener;
+        }
+
+        @Override
+        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            final View view = mInflater.inflate(
+                    com.android.internal.R.layout.input_method_switch_item_new, parent, false);
+
+            return new ViewHolder(view, mOnClickListener);
+        }
+
+        @Override
+        public void onBindViewHolder(ViewHolder holder, int position) {
+            holder.bind(mItems.get(position), position == mSelectedIndex /* isSelected */);
+        }
+
+        @Override
+        public int getItemCount() {
+            return mItems.size();
+        }
+
+        private static class ViewHolder extends RecyclerView.ViewHolder {
+
+            /** The container of the item. */
+            @NonNull
+            private final View mContainer;
+            /** The name of the item. */
+            @NonNull
+            private final TextView mName;
+            /** Indicator for the selected status of the item. */
+            @NonNull
+            private final ImageView mCheckmark;
+            /** The group header optionally drawn above the item. */
+            @NonNull
+            private final TextView mHeader;
+            /** The group divider optionally drawn above the item. */
+            @NonNull
+            private final View mDivider;
+
+            private ViewHolder(@NonNull View itemView,
+                    @NonNull DialogInterface.OnClickListener onClickListener) {
+                super(itemView);
+
+                mContainer = itemView.requireViewById(com.android.internal.R.id.list_item);
+                mName = itemView.requireViewById(com.android.internal.R.id.text);
+                mCheckmark = itemView.requireViewById(com.android.internal.R.id.image);
+                mHeader = itemView.requireViewById(com.android.internal.R.id.header_text);
+                mDivider = itemView.requireViewById(com.android.internal.R.id.divider);
+
+                mContainer.setOnClickListener((v) ->
+                        onClickListener.onClick(null /* dialog */, getAdapterPosition()));
+            }
+
+            /**
+             * Binds the given item to the current view.
+             *
+             * @param item       the item to bind.
+             * @param isSelected whether this is selected.
+             */
+            private void bind(@NonNull MenuItem item, boolean isSelected) {
+                // Use the IME name for subtypes with an empty subtype name.
+                final var name = TextUtils.isEmpty(item.mSubtypeName)
+                        ? item.mImeName : item.mSubtypeName;
+                mContainer.setActivated(isSelected);
+                // Activated is the correct state, but we also set selected for accessibility info.
+                mContainer.setSelected(isSelected);
+                mName.setSelected(isSelected);
+                mName.setText(name);
+                mCheckmark.setVisibility(isSelected ? View.VISIBLE : View.GONE);
+                mHeader.setText(item.mImeName);
+                mHeader.setVisibility(item.mHasHeader ? View.VISIBLE : View.GONE);
+                mDivider.setVisibility(item.mHasDivider ? View.VISIBLE : View.GONE);
+            }
+        }
+    }
+}