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);
+ }
+ }
+ }
+}