[Data Sharing] Add data sharing tab related functionalities
Add tab manager to manage different tab related flows.
Add tab observer to observe tab changes and notify the tab manager.
Low-Coverage-Reason: TESTS_IN_SEPARATE_CL
Bug: b/351040067
Change-Id: If77be007695307e2ec6d4c129895ea4d8c9d36c8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5719131
Auto-Submit: Hailey Wang <haileywang@google.com>
Reviewed-by: Siddhartha S <ssid@chromium.org>
Commit-Queue: Hailey Wang <haileywang@google.com>
Cr-Commit-Position: refs/heads/main@{#1335780}
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index df81e92..1e698ae 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -22,6 +22,7 @@
import("//chrome/browser/commerce/subscriptions/android/java_sources.gni")
import(
"//chrome/browser/commerce/subscriptions/test/android/test_java_sources.gni")
+import("//chrome/browser/data_sharing/java_sources.gni")
import("//chrome/browser/page_info/buildflags.gni")
import("//chrome/browser/share/android/java_sources.gni")
import("//chrome/chrome_paks.gni")
@@ -795,6 +796,8 @@
srcjar_deps += [ ":chrome_jni_headers" ]
+ sources += data_sharing_java_sources
+
# Add the actual implementation where necessary so that downstream targets
# can provide their own implementations.
jar_excluded_patterns = [ "*/AppHooksImpl.class" ]
diff --git a/chrome/android/java_sources.gni b/chrome/android/java_sources.gni
index c1736ca3..d73760e 100644
--- a/chrome/android/java_sources.gni
+++ b/chrome/android/java_sources.gni
@@ -13,6 +13,7 @@
import("//chrome/browser/commerce/price_tracking/android/test_java_sources.gni")
import(
"//chrome/browser/commerce/subscriptions/test/android/test_java_sources.gni")
+import("//chrome/browser/data_sharing/java_sources.gni")
import("//chrome/browser/share/android/test_java_sources.gni")
import("//chrome/browser/tab_group/javatests/tab_groups_test_java_sources.gni")
import("//chrome/browser/tab_group_sync/android/test_java_sources.gni")
@@ -41,6 +42,7 @@
chrome_junit_test_java_deps += commerce_subscriptions_junit_test_deps
chrome_test_java_sources += commerce_merchant_viewer_java_test_sources
chrome_test_java_sources += tab_group_sync_test_java_sources
+chrome_junit_test_java_sources += data_sharing_java_tests
if (enable_arcore) {
chrome_java_sources += [
diff --git a/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabManager.java b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabManager.java
new file mode 100644
index 0000000..ed345ba
--- /dev/null
+++ b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabManager.java
@@ -0,0 +1,157 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.data_sharing;
+
+import org.chromium.base.Callback;
+import org.chromium.base.supplier.ObservableSupplier;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
+import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncServiceFactory;
+import org.chromium.components.data_sharing.DataSharingService;
+import org.chromium.components.data_sharing.GroupToken;
+import org.chromium.components.data_sharing.ParseURLStatus;
+import org.chromium.components.data_sharing.PeopleGroupActionOutcome;
+import org.chromium.components.signin.base.CoreAccountInfo;
+import org.chromium.components.signin.identitymanager.ConsentLevel;
+import org.chromium.components.signin.identitymanager.IdentityManager;
+import org.chromium.components.tab_group_sync.SavedTabGroup;
+import org.chromium.components.tab_group_sync.TabGroupSyncService;
+import org.chromium.url.GURL;
+
+import java.util.List;
+
+/**
+ * This class is responsible for handling communication from the UI to multiple data sharing
+ * services.
+ */
+public class DataSharingTabManager {
+ private ObservableSupplier<Profile> mProfileSupplier;
+ private DataSharingTabSwitcherDelegate mDataSharingTabSwitcherDelegate;
+ private List<DataSharingTabObserver> mTabGroupObserversList;
+ private Callback<Profile> mProfileObserver;
+
+ /**
+ * Constructor for a new {@link DataSharingTabManager} object.
+ *
+ * @param tabSwitcherDelegate The delegate used to communicate with the tab switcher.
+ * @param profileSupplier The supplier of the currently applicable profile.
+ */
+ public DataSharingTabManager(
+ DataSharingTabSwitcherDelegate tabSwitcherDelegate,
+ ObservableSupplier<Profile> profileSupplier) {
+ mDataSharingTabSwitcherDelegate = tabSwitcherDelegate;
+
+ mProfileSupplier = profileSupplier;
+ assert mProfileSupplier != null;
+ }
+
+ /**
+ * Initiate the join flow. If successful, the associated tab group view will be opened.
+ *
+ * @param dataSharingURL The URL associated with the join invitation.
+ */
+ public void initiateJoinFlow(GURL dataSharingURL) {
+ if (mProfileSupplier.get() != null) {
+ initiateJoinFlowWithProfile(dataSharingURL);
+ return;
+ }
+
+ assert mProfileObserver == null;
+ mProfileObserver =
+ profile -> {
+ mProfileSupplier.removeObserver(mProfileObserver);
+ initiateJoinFlowWithProfile(dataSharingURL);
+ };
+
+ mProfileSupplier.addObserver(mProfileObserver);
+ }
+
+ private void initiateJoinFlowWithProfile(GURL dataSharingURL) {
+ TabGroupSyncService tabGroupSyncService =
+ TabGroupSyncServiceFactory.getForProfile(mProfileSupplier.get());
+ DataSharingService dataSharingService =
+ DataSharingServiceFactory.getForProfile(mProfileSupplier.get());
+ assert tabGroupSyncService != null;
+ assert dataSharingService != null;
+
+ DataSharingService.ParseURLResult parseResult =
+ dataSharingService.parseDataSharingURL(dataSharingURL);
+ if (parseResult.status != ParseURLStatus.SUCCESS) {
+ // TODO(b/354003616): Show error dialog.
+ return;
+ }
+
+ GroupToken groupToken = parseResult.groupToken;
+ // Verify that tabgroup does not already exist.
+ SavedTabGroup existingGroup =
+ getTabGroupForCollabId(groupToken.groupId, tabGroupSyncService);
+ if (existingGroup != null) {
+ Integer tabId = existingGroup.savedTabs.get(0).localId;
+ assert tabId != null;
+ openTabGroupWithTabId(tabId);
+ return;
+ }
+
+ // TODO(b/354003616): Show loading dialog while waiting for tab.
+
+ DataSharingTabObserver observer = new DataSharingTabObserver(groupToken.groupId, this);
+
+ mTabGroupObserversList.add(observer);
+ tabGroupSyncService.addObserver(observer);
+
+ IdentityManager identityManager =
+ IdentityServicesProvider.get().getIdentityManager(mProfileSupplier.get());
+ CoreAccountInfo coreAccountInfo =
+ identityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN);
+
+ dataSharingService.inviteMember(
+ groupToken.groupId,
+ coreAccountInfo.getEmail(),
+ result -> {
+ if (result != PeopleGroupActionOutcome.SUCCESS) {
+ // TODO(b/354003616): Stop showing loading dialog. Show error dialog.
+ return;
+ }
+ });
+ }
+
+ SavedTabGroup getTabGroupForCollabId(
+ String collaborationId, TabGroupSyncService tabGroupSyncService) {
+ for (String syncGroupId : tabGroupSyncService.getAllGroupIds()) {
+ SavedTabGroup savedTabGroup = tabGroupSyncService.getGroup(syncGroupId);
+ assert !savedTabGroup.savedTabs.isEmpty();
+ if (savedTabGroup.collaborationId.equals(collaborationId)) {
+ return savedTabGroup;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Open a tab group.
+ *
+ * @param tabId The tab id of the first tab in the group.
+ */
+ void openTabGroupWithTabId(Integer tabId) {
+ // TODO(b/354003616): Verify that the loading dialog is gone.
+ mDataSharingTabSwitcherDelegate.openTabGroupWithTabId(tabId);
+ }
+
+ /**
+ * Stop observing a data sharing tab group.
+ *
+ * @param observer The observer to be removed.
+ */
+ public void deleteObserver(DataSharingTabObserver observer) {
+ TabGroupSyncService tabGroupSyncService =
+ TabGroupSyncServiceFactory.getForProfile(mProfileSupplier.get());
+ mTabGroupObserversList.remove(observer);
+
+ if (tabGroupSyncService != null) {
+ tabGroupSyncService.removeObserver(observer);
+ }
+ }
+}
diff --git a/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabManagerUnitTest.java b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabManagerUnitTest.java
new file mode 100644
index 0000000..7936424
--- /dev/null
+++ b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabManagerUnitTest.java
@@ -0,0 +1,88 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.data_sharing;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import org.chromium.base.Token;
+import org.chromium.base.supplier.ObservableSupplier;
+import org.chromium.base.supplier.ObservableSupplierImpl;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncServiceFactory;
+import org.chromium.components.data_sharing.DataSharingService;
+import org.chromium.components.data_sharing.GroupToken;
+import org.chromium.components.data_sharing.ParseURLStatus;
+import org.chromium.components.tab_group_sync.LocalTabGroupId;
+import org.chromium.components.tab_group_sync.SavedTabGroup;
+import org.chromium.components.tab_group_sync.SavedTabGroupTab;
+import org.chromium.components.tab_group_sync.TabGroupSyncService;
+
+/** Unit test for {@link DataSharingTabManager} */
+@RunWith(BaseRobolectricTestRunner.class)
+public class DataSharingTabManagerUnitTest {
+ @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ private static final String GROUP_ID = "group_id";
+ private static final LocalTabGroupId LOCAL_ID = new LocalTabGroupId(Token.createRandom());
+ private static final Integer TAB_ID = 123;
+
+ @Mock private TabGroupSyncService mTabGroupSyncService;
+ @Mock private DataSharingService mDataSharingService;
+ @Mock private DataSharingTabSwitcherDelegate mDataSharingTabSwitcherDelegate;
+ @Mock private Profile mProfile;
+
+ private DataSharingTabManager mDataSharingTabManager;
+
+ @Before
+ public void setUp() {
+ DataSharingServiceFactory.setForTesting(mDataSharingService);
+ TabGroupSyncServiceFactory.setForTesting(mTabGroupSyncService);
+ ObservableSupplier<Profile> profileSupplier = new ObservableSupplierImpl<Profile>(mProfile);
+ mDataSharingTabManager =
+ new DataSharingTabManager(mDataSharingTabSwitcherDelegate, profileSupplier);
+ }
+
+ @Test
+ public void testInvalidURL() {
+ doReturn(new DataSharingService.ParseURLResult(null, ParseURLStatus.UNKNOWN))
+ .when(mDataSharingService)
+ .parseDataSharingURL(any());
+ mDataSharingTabManager.initiateJoinFlow(null);
+ }
+
+ @Test
+ public void testInviteFlowWithExistingTab() {
+ doReturn(
+ new DataSharingService.ParseURLResult(
+ new GroupToken(GROUP_ID, "accessToken"), ParseURLStatus.SUCCESS))
+ .when(mDataSharingService)
+ .parseDataSharingURL(any());
+
+ String[] tabId = new String[] {GROUP_ID};
+ doReturn(tabId).when(mTabGroupSyncService).getAllGroupIds();
+
+ SavedTabGroup savedTabGroup = new SavedTabGroup();
+ savedTabGroup.collaborationId = GROUP_ID;
+ savedTabGroup.localId = LOCAL_ID;
+ SavedTabGroupTab savedTabGroupTab = new SavedTabGroupTab();
+ savedTabGroupTab.localId = TAB_ID;
+ savedTabGroup.savedTabs.add(savedTabGroupTab);
+ doReturn(savedTabGroup).when(mTabGroupSyncService).getGroup(GROUP_ID);
+
+ mDataSharingTabManager.initiateJoinFlow(null);
+ verify(mDataSharingTabSwitcherDelegate).openTabGroupWithTabId(TAB_ID);
+ }
+}
diff --git a/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabObserver.java b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabObserver.java
new file mode 100644
index 0000000..97d9946f
--- /dev/null
+++ b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabObserver.java
@@ -0,0 +1,50 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.data_sharing;
+
+import org.chromium.components.tab_group_sync.LocalTabGroupId;
+import org.chromium.components.tab_group_sync.SavedTabGroup;
+import org.chromium.components.tab_group_sync.TabGroupSyncService;
+import org.chromium.components.tab_group_sync.TriggerSource;
+
+import java.lang.ref.WeakReference;
+
+/** This class is responsible for observing tab activities for data sharing services. */
+class DataSharingTabObserver implements TabGroupSyncService.Observer {
+ private String mDataSharingGroupId;
+ private WeakReference<DataSharingTabManager> mDataSharingTabManager;
+
+ public DataSharingTabObserver(
+ String dataSharingGroupId, DataSharingTabManager dataSharingTabManager) {
+ mDataSharingGroupId = dataSharingGroupId;
+ mDataSharingTabManager = new WeakReference<DataSharingTabManager>(dataSharingTabManager);
+ }
+
+ @Override
+ public void onInitialized() {}
+
+ @Override
+ public void onTabGroupAdded(SavedTabGroup group, @TriggerSource int source) {
+ if (mDataSharingGroupId.equals(group.collaborationId)) {
+ DataSharingTabManager dataSharingTabManager = mDataSharingTabManager.get();
+
+ if (dataSharingTabManager != null) {
+ Integer tabId = group.savedTabs.get(0).localId;
+ assert tabId != null;
+ dataSharingTabManager.openTabGroupWithTabId(tabId);
+ dataSharingTabManager.deleteObserver(this);
+ }
+ }
+ }
+
+ @Override
+ public void onTabGroupUpdated(SavedTabGroup group, @TriggerSource int source) {}
+
+ @Override
+ public void onTabGroupRemoved(LocalTabGroupId localId, @TriggerSource int source) {}
+
+ @Override
+ public void onTabGroupRemoved(String syncId, @TriggerSource int source) {}
+}
diff --git a/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabObserverUnitTest.java b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabObserverUnitTest.java
new file mode 100644
index 0000000..07a5844f
--- /dev/null
+++ b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabObserverUnitTest.java
@@ -0,0 +1,55 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.data_sharing;
+
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import org.chromium.base.Token;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.components.tab_group_sync.LocalTabGroupId;
+import org.chromium.components.tab_group_sync.SavedTabGroup;
+import org.chromium.components.tab_group_sync.SavedTabGroupTab;
+import org.chromium.components.tab_group_sync.TriggerSource;
+
+/** Unit test for {@link DataSharingTabObserver} */
+@RunWith(BaseRobolectricTestRunner.class)
+public class DataSharingTabObserverUnitTest {
+ @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ private static final String COLLABORATION_ID = "data_sharing";
+ private static final LocalTabGroupId LOCAL_ID = new LocalTabGroupId(Token.createRandom());
+ private static final Integer TAB_ID = 123;
+
+ @Mock private DataSharingTabManager mDataSharingTabManager;
+
+ private DataSharingTabObserver mDataSharingTabObserver;
+
+ @Before
+ public void setUp() {
+ mDataSharingTabObserver =
+ new DataSharingTabObserver(COLLABORATION_ID, mDataSharingTabManager);
+ }
+
+ @Test
+ public void testInvalidURL() {
+ SavedTabGroup savedTabGroup = new SavedTabGroup();
+ savedTabGroup.collaborationId = COLLABORATION_ID;
+ savedTabGroup.localId = LOCAL_ID;
+ SavedTabGroupTab savedTabGroupTab = new SavedTabGroupTab();
+ savedTabGroupTab.localId = TAB_ID;
+ savedTabGroup.savedTabs.add(savedTabGroupTab);
+ mDataSharingTabObserver.onTabGroupAdded(savedTabGroup, TriggerSource.REMOTE);
+ verify(mDataSharingTabManager).openTabGroupWithTabId(TAB_ID);
+ verify(mDataSharingTabManager).deleteObserver(mDataSharingTabObserver);
+ }
+}
diff --git a/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabSwitcherDelegate.java b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabSwitcherDelegate.java
new file mode 100644
index 0000000..5cabe5e
--- /dev/null
+++ b/chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabSwitcherDelegate.java
@@ -0,0 +1,15 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.data_sharing;
+
+/** An interface to handle actions on the TabSwitcher. */
+public interface DataSharingTabSwitcherDelegate {
+ /**
+ * Open the tab group dialog of the given tab group id.
+ *
+ * @param id The tabId of the first tab in the group.
+ */
+ public void openTabGroupWithTabId(Integer id);
+}
diff --git a/chrome/browser/data_sharing/java_sources.gni b/chrome/browser/data_sharing/java_sources.gni
new file mode 100644
index 0000000..1317b36
--- /dev/null
+++ b/chrome/browser/data_sharing/java_sources.gni
@@ -0,0 +1,16 @@
+# Copyright 2024 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This is only for utilities used by the UI mediators.
+# TODO(ssid): Move this into a module and remove UI deps from here.
+data_sharing_java_sources = [
+ "//chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabManager.java",
+ "//chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabObserver.java",
+ "//chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabSwitcherDelegate.java",
+]
+
+data_sharing_java_tests = [
+ "//chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabManagerUnitTest.java",
+ "//chrome/browser/data_sharing/android/java/src/org/chromium/chrome/browser/data_sharing/DataSharingTabObserverUnitTest.java",
+]
diff --git a/components/data_sharing/public/android/java/src/org/chromium/components/data_sharing/DataSharingService.java b/components/data_sharing/public/android/java/src/org/chromium/components/data_sharing/DataSharingService.java
index c791086..8facaae 100644
--- a/components/data_sharing/public/android/java/src/org/chromium/components/data_sharing/DataSharingService.java
+++ b/components/data_sharing/public/android/java/src/org/chromium/components/data_sharing/DataSharingService.java
@@ -65,7 +65,7 @@
/** Result of the action */
public final @ParseURLStatus int status;
- ParseURLResult(GroupToken groupToken, int status) {
+ public ParseURLResult(GroupToken groupToken, int status) {
this.groupToken = groupToken;
this.status = status;
}
diff --git a/components/data_sharing/public/android/java/src/org/chromium/components/data_sharing/GroupToken.java b/components/data_sharing/public/android/java/src/org/chromium/components/data_sharing/GroupToken.java
index e9418b1..09f2d59a 100644
--- a/components/data_sharing/public/android/java/src/org/chromium/components/data_sharing/GroupToken.java
+++ b/components/data_sharing/public/android/java/src/org/chromium/components/data_sharing/GroupToken.java
@@ -13,7 +13,13 @@
public final String groupId;
public final String accessToken;
- GroupToken(String groupId, String accessToken) {
+ /**
+ * Constructor for a {@link GroupToken} object.
+ *
+ * @param groupId The ID associated with the group.
+ * @param accessToken The access token associated with the group.
+ */
+ public GroupToken(String groupId, String accessToken) {
this.groupId = groupId;
this.accessToken = accessToken;
}