Merge "Revamp IME switcher menu" into main
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 85af7ab..fbd9ac0 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);
+            }
+        }
+    }
+}