[UW] Add high-level layout for instance switcher v2
Screenshot: https://screenshot.googleplex.com/7RdUiLtDzrdZSpy
Note: This update is flag-guarded and will be done in a series of CLs.
Bug: 414687867
Change-Id: Ia476169f3d3bde03da5623e3b57171de9a6345bd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6520037
Reviewed-by: Jinsuk Kim <[email protected]>
Commit-Queue: Aishwarya Rajesh <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1458137}
diff --git a/chrome/browser/ui/android/multiwindow/BUILD.gn b/chrome/browser/ui/android/multiwindow/BUILD.gn
index 38f1a4f..dcf4a2f 100644
--- a/chrome/browser/ui/android/multiwindow/BUILD.gn
+++ b/chrome/browser/ui/android/multiwindow/BUILD.gn
@@ -37,10 +37,13 @@
"//components/browser_ui/widget/android:java",
"//components/favicon/android:java",
"//components/feature_engagement/public:public_java",
+ "//third_party/android_deps:material_design_java",
"//third_party/androidx:androidx_annotation_annotation_java",
"//third_party/androidx:androidx_appcompat_appcompat_resources_java",
"//third_party/androidx:androidx_core_core_java",
+ "//third_party/androidx:androidx_recyclerview_recyclerview_java__classes",
"//ui/android:ui_no_recycler_view_java",
+ "//ui/android:ui_recycler_view_java",
"//ui/android:ui_utils_java",
"//url:gurl_java",
]
@@ -52,8 +55,10 @@
"java/res/drawable/checkmark_24dp.xml",
"java/res/drawable/circle_green.xml",
"java/res/layout/close_confirmation_dialog.xml",
+ "java/res/layout/instance_switcher_active_list.xml",
"java/res/layout/instance_switcher_cmd_item.xml",
"java/res/layout/instance_switcher_dialog.xml",
+ "java/res/layout/instance_switcher_dialog_v2.xml",
"java/res/layout/instance_switcher_item.xml",
"java/res/layout/instance_switcher_list.xml",
"java/res/layout/target_selector_dialog.xml",
diff --git a/chrome/browser/ui/android/multiwindow/java/res/layout/instance_switcher_active_list.xml b/chrome/browser/ui/android/multiwindow/java/res/layout/instance_switcher_active_list.xml
new file mode 100644
index 0000000..1e90466
--- /dev/null
+++ b/chrome/browser/ui/android/multiwindow/java/res/layout/instance_switcher_active_list.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2025 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/active_instance_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:divider="@null"/>
+</merge>
diff --git a/chrome/browser/ui/android/multiwindow/java/res/layout/instance_switcher_dialog_v2.xml b/chrome/browser/ui/android/multiwindow/java/res/layout/instance_switcher_dialog_v2.xml
new file mode 100644
index 0000000..252ba321
--- /dev/null
+++ b/chrome/browser/ui/android/multiwindow/java/res/layout/instance_switcher_dialog_v2.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2025 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingHorizontal="@dimen/instance_switcher_dialog_content_padding"
+ android:paddingTop="@dimen/instance_switcher_dialog_content_padding">
+
+ <com.google.android.material.tabs.TabLayout
+ android:id="@+id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/instance_switcher_dialog_list_item_padding"
+ style="@style/TabLayoutStyle">
+ <com.google.android.material.tabs.TabItem
+ android:id="@+id/active_instances"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <com.google.android.material.tabs.TabItem
+ android:id="@+id/inactive_instances"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </com.google.android.material.tabs.TabLayout>
+
+ <FrameLayout
+ android:id="@+id/instance_list_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <include layout="@layout/instance_switcher_active_list"/>
+ </FrameLayout>
+</LinearLayout>
diff --git a/chrome/browser/ui/android/multiwindow/java/res/values/dimens.xml b/chrome/browser/ui/android/multiwindow/java/res/values/dimens.xml
index 39079cf..71d16a6 100644
--- a/chrome/browser/ui/android/multiwindow/java/res/values/dimens.xml
+++ b/chrome/browser/ui/android/multiwindow/java/res/values/dimens.xml
@@ -8,4 +8,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<dimen name="confirmation_dialog_width">328dp</dimen>
<dimen name="confirmation_dialog_side_margin">16dp</dimen>
+ <dimen name="instance_switcher_dialog_content_padding">16dp</dimen>
+ <dimen name="instance_switcher_dialog_list_item_padding">2dp</dimen>
</resources>
diff --git a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinator.java b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinator.java
index ce01788..655e3bd 100644
--- a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinator.java
+++ b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinator.java
@@ -19,11 +19,17 @@
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.tabs.TabLayout.Tab;
import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.build.annotations.NullMarked;
import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.components.browser_ui.widget.BrowserUiListMenuUtils;
@@ -39,6 +45,7 @@
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.ModelListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.modelutil.SimpleRecyclerViewAdapter;
import org.chromium.ui.widget.Toast;
import java.lang.annotation.Retention;
@@ -74,6 +81,7 @@
private final ModelList mModelList = new ModelList();
private final UiUtils mUiUtils;
private final View mDialogView;
+ private @Nullable TabLayout mTabHeaderRow;
private @Nullable PropertyModel mDialog;
private @Nullable InstanceInfo mItemToDelete;
@@ -124,23 +132,51 @@
mUiUtils = new UiUtils(mContext, iconBridge);
mNewWindowAction = newWindowAction;
- ModelListAdapter adapter = new ModelListAdapter(mModelList);
- // TODO: Extend modern_list_item_view.xml to replace instance_switcher_item.xml
- adapter.registerType(
- EntryType.INSTANCE,
- parentView ->
- LayoutInflater.from(mContext)
- .inflate(R.layout.instance_switcher_item, null),
- InstanceSwitcherItemViewBinder::bind);
- adapter.registerType(
- EntryType.COMMAND,
- parentView ->
- LayoutInflater.from(mContext)
- .inflate(R.layout.instance_switcher_cmd_item, null),
- InstanceSwitcherItemViewBinder::bind);
- mDialogView = LayoutInflater.from(context).inflate(R.layout.instance_switcher_dialog, null);
- ListView listView = (ListView) mDialogView.findViewById(R.id.list_view);
- listView.setAdapter(adapter);
+ if (ChromeFeatureList.isEnabled(ChromeFeatureList.INSTANCE_SWITCHER_V2)) {
+ var adapter = new SimpleRecyclerViewAdapter(mModelList);
+ adapter.registerType(
+ EntryType.INSTANCE,
+ parentView ->
+ LayoutInflater.from(mContext)
+ .inflate(R.layout.instance_switcher_item, null),
+ InstanceSwitcherItemViewBinder::bind);
+ adapter.registerType(
+ EntryType.COMMAND,
+ parentView ->
+ LayoutInflater.from(mContext)
+ .inflate(R.layout.instance_switcher_cmd_item, null),
+ InstanceSwitcherItemViewBinder::bind);
+
+ mDialogView =
+ LayoutInflater.from(context)
+ .inflate(R.layout.instance_switcher_dialog_v2, null);
+ mTabHeaderRow = mDialogView.findViewById(R.id.tabs);
+ View listContainer = mDialogView.findViewById(R.id.instance_list_container);
+ RecyclerView recyclerView = listContainer.findViewById(R.id.active_instance_list);
+ recyclerView.setLayoutManager(
+ new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false));
+ recyclerView.setAdapter(adapter);
+ } else {
+ ModelListAdapter adapter = new ModelListAdapter(mModelList);
+ // TODO: Extend modern_list_item_view.xml to replace instance_switcher_item.xml
+ adapter.registerType(
+ EntryType.INSTANCE,
+ parentView ->
+ LayoutInflater.from(mContext)
+ .inflate(R.layout.instance_switcher_item, null),
+ InstanceSwitcherItemViewBinder::bind);
+ adapter.registerType(
+ EntryType.COMMAND,
+ parentView ->
+ LayoutInflater.from(mContext)
+ .inflate(R.layout.instance_switcher_cmd_item, null),
+ InstanceSwitcherItemViewBinder::bind);
+
+ mDialogView =
+ LayoutInflater.from(context).inflate(R.layout.instance_switcher_dialog, null);
+ ListView listView = (ListView) mDialogView.findViewById(R.id.list_view);
+ listView.setAdapter(adapter);
+ }
}
private void show(List<InstanceInfo> items, boolean newWindowEnabled) {
@@ -153,6 +189,7 @@
mNewWindowModel = new PropertyModel(InstanceSwitcherItemProperties.ALL_KEYS);
enableNewWindowCommand(newWindowEnabled);
mModelList.add(new ModelListAdapter.ListItem(EntryType.COMMAND, mNewWindowModel));
+ updateTabTitle(items.size(), items.size());
mDialog = createDialog(mDialogView);
mModalDialogManager.showDialog(mDialog, ModalDialogType.APP);
@@ -281,6 +318,9 @@
RecordUserAction.record("Android.WindowManager.CloseWindow");
// Removing an instance enables the new window item.
enableNewWindowCommand(true);
+ // Number of instances is one less than the list size to exclude the new window item.
+ int numInstances = mModelList.size() - 1;
+ updateTabTitle(numInstances, numInstances);
}
private static boolean canSkipConfirm(InstanceInfo item) {
@@ -328,4 +368,16 @@
});
dialog.show();
}
+
+ private void updateTabTitle(int numActiveInstances, int numInactiveInstances) {
+ if (mTabHeaderRow == null) return;
+ Tab activeTab = mTabHeaderRow.getTabAt(0);
+ Tab inactiveTab = mTabHeaderRow.getTabAt(1);
+ assumeNonNull(activeTab);
+ assumeNonNull(inactiveTab);
+ activeTab.setText(
+ mContext.getString(R.string.instance_switcher_tabs_active, numActiveInstances));
+ inactiveTab.setText(
+ mContext.getString(R.string.instance_switcher_tabs_inactive, numInactiveInstances));
+ }
}
diff --git a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinatorTest.java b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinatorTest.java
index c29d0c9..aafcc4b 100644
--- a/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinatorTest.java
+++ b/chrome/browser/ui/android/multiwindow/java/src/org/chromium/chrome/browser/multiwindow/InstanceSwitcherCoordinatorTest.java
@@ -8,6 +8,7 @@
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
import static androidx.test.espresso.matcher.ViewMatchers.Visibility.GONE;
@@ -38,6 +39,9 @@
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.base.test.util.Features.DisableFeatures;
+import org.chromium.base.test.util.Features.EnableFeatures;
+import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
@@ -53,6 +57,7 @@
/** Unit tests for {@link InstanceSwitcherCoordinator}. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
+@DisableFeatures(ChromeFeatureList.INSTANCE_SWITCHER_V2)
public class InstanceSwitcherCoordinatorTest {
@Rule
public BaseActivityTestRule<BlankUiTestActivity> mActivityTestRule =
@@ -125,6 +130,40 @@
@Test
@SmallTest
+ @EnableFeatures(ChromeFeatureList.INSTANCE_SWITCHER_V2)
+ public void testOpenWindow_InstanceSwitcherV2() throws Exception {
+ InstanceInfo[] instances =
+ new InstanceInfo[] {
+ new InstanceInfo(
+ 0, 57, InstanceInfo.Type.CURRENT, "url0", "title0", 1, 0, false, 0),
+ new InstanceInfo(
+ 1, 58, InstanceInfo.Type.OTHER, "ur11", "title1", 2, 0, false, 0),
+ new InstanceInfo(
+ 2, 59, InstanceInfo.Type.OTHER, "url2", "title2", 0, 0, false, 0)
+ };
+ final CallbackHelper itemClickCallbackHelper = new CallbackHelper();
+ final int itemClickCount = itemClickCallbackHelper.getCallCount();
+ Callback<InstanceInfo> openCallback = (item) -> itemClickCallbackHelper.notifyCalled();
+ ThreadUtils.runOnUiThreadBlocking(
+ () -> {
+ InstanceSwitcherCoordinator.showDialog(
+ mActivityTestRule.getActivity(),
+ mModalDialogManager,
+ mIconBridge,
+ openCallback,
+ null,
+ null,
+ false,
+ Arrays.asList(instances));
+ });
+ onView(withId(R.id.active_instance_list))
+ .inRoot(isDialog())
+ .perform(actionOnItemAtPosition(1, click()));
+ itemClickCallbackHelper.waitForCallback(itemClickCount);
+ }
+
+ @Test
+ @SmallTest
public void testNewWindow() throws Exception {
InstanceInfo[] instances =
new InstanceInfo[] {
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
index 67af9b59..bc3f230 100644
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -5704,6 +5704,12 @@
<message name="IDS_INSTANCE_SWITCHER_HEADER" desc="The header of multi-instance switcher dialog.">
Manage windows
</message>
+ <message name="IDS_INSTANCE_SWITCHER_TABS_ACTIVE" desc="The title of the active instances tab header on the multi-instance switcher dialog.">
+ Active (<ph name="ITEM_COUNT">%1$s<ex>2</ex></ph>)
+ </message>
+ <message name="IDS_INSTANCE_SWITCHER_TABS_INACTIVE" desc="The title of the inactive instances tab header on the multi-instance switcher dialog.">
+ Inactive (<ph name="ITEM_COUNT">%1$s<ex>2</ex></ph>)
+ </message>
<message name="IDS_INSTANCE_SWITCHER_ENTRY_EMPTY_WINDOW" desc="Title of an instance entry that has no tabs in it.">
Empty window
</message>
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_INSTANCE_SWITCHER_TABS_ACTIVE.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_INSTANCE_SWITCHER_TABS_ACTIVE.png.sha1
new file mode 100644
index 0000000..8f774c3
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_INSTANCE_SWITCHER_TABS_ACTIVE.png.sha1
@@ -0,0 +1 @@
+5cbad4c960c257f0f4282adc18c9d11e70587c69
\ No newline at end of file
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_INSTANCE_SWITCHER_TABS_INACTIVE.png.sha1 b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_INSTANCE_SWITCHER_TABS_INACTIVE.png.sha1
new file mode 100644
index 0000000..8f774c3
--- /dev/null
+++ b/chrome/browser/ui/android/strings/android_chrome_strings_grd/IDS_INSTANCE_SWITCHER_TABS_INACTIVE.png.sha1
@@ -0,0 +1 @@
+5cbad4c960c257f0f4282adc18c9d11e70587c69
\ No newline at end of file