[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;
     }