| // 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. |
| |
| #include "components/collaboration/internal/collaboration_controller.h" |
| |
| #include <optional> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "components/collaboration/internal/metrics.h" |
| #include "components/collaboration/public/collaboration_flow_type.h" |
| #include "components/collaboration/public/collaboration_utils.h" |
| #include "components/data_sharing/public/data_sharing_service.h" |
| #include "components/data_sharing/public/group_data.h" |
| #include "components/data_sharing/public/logger.h" |
| #include "components/data_sharing/public/logger_common.mojom.h" |
| #include "components/data_sharing/public/logger_utils.h" |
| #include "components/saved_tab_groups/public/saved_tab_group.h" |
| #include "components/sync/base/collaboration_id.h" |
| #include "components/sync/service/sync_service.h" |
| |
| namespace collaboration { |
| |
| using metrics::CollaborationServiceFlowEvent; |
| using metrics::CollaborationServiceJoinEvent; |
| using metrics::CollaborationServiceShareOrManageEvent; |
| |
| class ControllerState; |
| |
| namespace { |
| |
| constexpr char kDebugCollaborationStateString[] = |
| "Collaboration State Change\n" |
| " New State: %s\n" |
| " Previous State: %s\n" |
| " Error Type: %s\n"; |
| |
| using ErrorInfo = CollaborationControllerDelegate::ErrorInfo; |
| using Outcome = CollaborationControllerDelegate::Outcome; |
| using ResultCallback = CollaborationControllerDelegate::ResultCallback; |
| using GroupDataOrFailureOutcome = |
| data_sharing::DataSharingService::GroupDataOrFailureOutcome; |
| using StateId = CollaborationController::StateId; |
| using Flow = CollaborationController::Flow; |
| using ServiceStatusUpdate = CollaborationService::Observer::ServiceStatusUpdate; |
| |
| constexpr base::TimeDelta kTimeoutWaitingForDataSharingGroup = |
| base::Seconds(20); |
| |
| std::string GetStateIdString(StateId state) { |
| switch (state) { |
| case StateId::kPending: |
| return "Pending"; |
| case StateId::kWaitingForPolicyUpdate: |
| return "WaitingForPolicyUpdate"; |
| case StateId::kAuthenticating: |
| return "Authenticating"; |
| case StateId::kWaitingForServicesToInitialize: |
| return "WaitingForServicesToInitialize"; |
| case StateId::kCheckingFlowRequirements: |
| return "CheckingFlowRequirements"; |
| case StateId::kAddingUserToGroup: |
| return "AddingUserToGroup"; |
| case StateId::kWaitingForSyncAndDataSharingGroup: |
| return "WaitingForSyncAndDataSharingGroup"; |
| case StateId::kOpeningLocalTabGroup: |
| return "OpeningLocalTabGroup"; |
| case StateId::kShowingShareScreen: |
| return "ShowingShareScreen"; |
| case StateId::kMakingTabGroupShared: |
| return "MakingTabGroupShared"; |
| case StateId::kSharingTabGroupUrl: |
| return "SharingTabGroupUrl"; |
| case StateId::kShowingManageScreen: |
| return "ShowingManageScreen"; |
| case CollaborationController::StateId::kLeavingGroup: |
| return "LeavingGroup"; |
| case CollaborationController::StateId::kDeletingGroup: |
| return "DeletingGroup"; |
| case StateId::kCleaningUpSharedTabGroup: |
| return "CleaningUpSharedTabGroup"; |
| case StateId::kCancel: |
| return "Cancel"; |
| case StateId::kError: |
| return "Error"; |
| } |
| } |
| |
| std::string CreateStateTransitionLogString( |
| StateId previous, |
| StateId current, |
| const CollaborationControllerDelegate::ErrorInfo& error) { |
| std::string error_str = |
| error.type() != CollaborationControllerDelegate::ErrorInfo::Type::kUnknown |
| ? error.GetLogString() |
| : "[N/A]"; |
| std::string log = base::StringPrintf(kDebugCollaborationStateString, |
| GetStateIdString(current), |
| GetStateIdString(previous), error_str); |
| |
| return log; |
| } |
| |
| } // namespace |
| |
| // This is base class for each state and handles the logic for the state. |
| // TODO(crbug.com/389953812): Consider consolidating metric recording into the |
| // base class. Provide a utility function to handle state specific metrics. |
| class ControllerState { |
| public: |
| ControllerState(StateId id, CollaborationController* controller) |
| : id_(id), controller_(controller) {} |
| virtual ~ControllerState() = default; |
| |
| // Called when entering the state. |
| virtual void OnEnter(const ErrorInfo& error) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| } |
| |
| // Called to process the outcome of an external event. |
| virtual void ProcessOutcome(Outcome outcome) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| switch (outcome) { |
| case Outcome::kSuccess: |
| OnProcessingFinishedWithSuccess(); |
| return; |
| case Outcome::kCancel: |
| controller_->Exit(); |
| return; |
| case Outcome::kFailure: |
| // The following outcomes should only be used by specific state. |
| case Outcome::kGroupLeftOrDeleted: |
| HandleError(); |
| return; |
| } |
| } |
| |
| // Called when an error happens during the state. |
| virtual void HandleError() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->TransitionTo(StateId::kError, |
| ErrorInfo(ErrorInfo::Type::kGenericError)); |
| } |
| |
| virtual void HandleErrorWithMetrics( |
| CollaborationServiceJoinEvent event, |
| CollaborationServiceFlowEvent flow_event) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| RecordJoinEvent(GetLogger(), event); |
| RecordCollaborationFlowEvent(GetLogger(), controller_->flow().type, |
| flow_event); |
| HandleError(); |
| } |
| |
| virtual void HandleErrorWithType(ErrorInfo::Type type) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->TransitionTo(StateId::kError, |
| ErrorInfo(type, controller_->flow().type)); |
| } |
| |
| // Called when the state outcome processing is finished. |
| virtual void OnProcessingFinishedWithSuccess() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| } |
| |
| // Called when exiting the state. |
| virtual void OnExit() { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); } |
| |
| StateId id() const { return id_; } |
| |
| protected: |
| bool IsTabGroupInSync(const data_sharing::GroupId& group_id) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| const std::vector<tab_groups::SavedTabGroup>& all_groups = |
| controller_->tab_group_sync_service()->GetAllGroups(); |
| for (const auto& group : all_groups) { |
| if (group.collaboration_id().has_value() && |
| group.collaboration_id().value() == |
| syncer::CollaborationId(group_id.value())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| std::optional<data_sharing::GroupId> GetGroupIdFromEitherId( |
| const tab_groups::EitherGroupID& either_id) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| std::optional<tab_groups::SavedTabGroup> tab_group = |
| controller_->tab_group_sync_service()->GetGroup(either_id); |
| |
| if (tab_group.has_value() && tab_group->collaboration_id().has_value()) { |
| auto group_id = tab_group->collaboration_id().value().value(); |
| return data_sharing::GroupId(group_id); |
| } |
| |
| return std::nullopt; |
| } |
| |
| bool IsPeopleGroupInDataSharing(const data_sharing::GroupId& group_id) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| return controller_->collaboration_service()->GetCurrentUserRoleForGroup( |
| group_id) != data_sharing::MemberRole::kUnknown; |
| } |
| |
| data_sharing::Logger* GetLogger() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| return controller_->data_sharing_service()->GetLogger(); |
| } |
| |
| THREAD_CHECKER(thread_checker_); |
| const StateId id_; |
| const raw_ptr<CollaborationController> controller_; |
| base::WeakPtrFactory<ControllerState> weak_ptr_factory_{this}; |
| }; |
| |
| namespace { |
| |
| class PendingState : public ControllerState { |
| public: |
| PendingState(StateId id, |
| CollaborationController* controller, |
| base::OnceClosure exit_callback) |
| : ControllerState(id, controller), |
| exit_callback_(std::move(exit_callback)) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| controller_->delegate()->PrepareFlowUI( |
| std::move(exit_callback_), |
| base::BindOnce(&PendingState::ProcessOutcome, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| if (controller_->flow().type == FlowType::kJoin) { |
| // Handle URL parsing errors. |
| if (!controller_->flow().join_token().IsValid()) { |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kParsingFailure); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinParsingFailure); |
| HandleErrorWithType(ErrorInfo::Type::kInvalidUrl); |
| return; |
| } |
| } |
| |
| ServiceStatus status = |
| controller_->collaboration_service()->GetServiceStatus(); |
| // Handle disabled by versioning. |
| // TODO(haileywang@): Refactor error handling for share/join flows and |
| // record metrics. |
| if (status.collaboration_status == |
| CollaborationStatus::kVersionOutOfDateShowUpdateChromeUi) { |
| HandleErrorWithType(ErrorInfo::Type::kUpdateChromeUiForVersionOutOfDate); |
| return; |
| } |
| // Handle disabled by policy. |
| if (status.collaboration_status == CollaborationStatus::kDisabledPending || |
| status.collaboration_status == |
| CollaborationStatus::kDisabledForPolicy) { |
| controller_->TransitionTo(StateId::kWaitingForPolicyUpdate); |
| return; |
| } |
| |
| // Verify authentication status. |
| if (!status.IsAuthenticationValid()) { |
| controller_->TransitionTo(StateId::kAuthenticating); |
| return; |
| } |
| |
| controller_->TransitionTo(StateId::kWaitingForServicesToInitialize); |
| } |
| |
| private: |
| // Will be invalid after OnEnter() is called. |
| base::OnceClosure exit_callback_; |
| }; |
| |
| class WaitingForPolicyUpdateState : public ControllerState, |
| public CollaborationService::Observer { |
| public: |
| WaitingForPolicyUpdateState(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| ServiceStatus status = |
| controller_->collaboration_service()->GetServiceStatus(); |
| if (status.collaboration_status == CollaborationStatus::kDisabledPending) { |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kAccountInfoNotReadyOnSignin, |
| CollaborationServiceShareOrManageEvent::kAccountInfoNotReadyOnSignin); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kAccountInfoNotReadyOnSignin); |
| pending_status_change_observer_.Observe( |
| controller_->collaboration_service()); |
| return; |
| } |
| |
| // If neither sign in nor sync has been disabled by the enterprise and the |
| // user is not trying to join, allow it. |
| bool signin_enabled = status.signin_status != SigninStatus::kSigninDisabled; |
| bool sync_enabled = |
| status.sync_status != SyncStatus::kSyncDisabledByEnterprise; |
| bool is_join_flow = controller_->flow().type == FlowType::kJoin; |
| if (signin_enabled && sync_enabled && !is_join_flow) { |
| OnProcessingFinishedWithSuccess(); |
| return; |
| } |
| |
| HandleError(); |
| } |
| |
| void HandleError() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| ServiceStatus status = |
| controller_->collaboration_service()->GetServiceStatus(); |
| |
| if (status.signin_status == SigninStatus::kSigninDisabled) { |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kDevicePolicyDisableSignin, |
| CollaborationServiceShareOrManageEvent::kDevicePolicyDisableSignin); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kDevicePolicyDisableSignin); |
| HandleErrorWithType(ErrorInfo::Type::kSigninDisabledByPolicy); |
| return; |
| } |
| |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kManagedAccountSignin, |
| CollaborationServiceShareOrManageEvent::kManagedAccountSignin); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kManagedAccountSignin); |
| |
| if (status.sync_status == SyncStatus::kSyncDisabledByEnterprise) { |
| HandleErrorWithType(ErrorInfo::Type::kSyncDisabledByPolicy); |
| } else if (controller_->flow().type == FlowType::kJoin) { |
| HandleErrorWithType(ErrorInfo::Type::kSharingDisabledByPolicy); |
| } |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| ServiceStatus status = |
| controller_->collaboration_service()->GetServiceStatus(); |
| if (status.IsAuthenticationValid()) { |
| controller_->TransitionTo(StateId::kCheckingFlowRequirements); |
| return; |
| } |
| controller_->TransitionTo(StateId::kAuthenticating); |
| } |
| |
| // CollaborationService::Observer implementation. |
| void OnServiceStatusChanged(const ServiceStatusUpdate& update) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| ServiceStatus status = update.new_status; |
| switch (status.collaboration_status) { |
| case CollaborationStatus::kDisabledPending: |
| break; |
| case CollaborationStatus::kDisabled: |
| case CollaborationStatus::kDisabledForPolicy: |
| case CollaborationStatus::kVersionOutOfDate: |
| HandleError(); |
| break; |
| case CollaborationStatus::kVersionOutOfDateShowUpdateChromeUi: |
| HandleErrorWithType( |
| ErrorInfo::Type::kUpdateChromeUiForVersionOutOfDate); |
| break; |
| case CollaborationStatus::kAllowedToJoin: |
| case CollaborationStatus::kEnabledJoinOnly: |
| case CollaborationStatus::kEnabledCreateAndJoin: |
| OnProcessingFinishedWithSuccess(); |
| break; |
| } |
| } |
| |
| private: |
| base::ScopedObservation<CollaborationService, CollaborationService::Observer> |
| pending_status_change_observer_{this}; |
| }; |
| |
| class AuthenticatingState : public ControllerState, |
| public CollaborationService::Observer { |
| public: |
| AuthenticatingState(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| start_time_ = base::Time::Now(); |
| FlowType flow_type = controller_->flow().type; |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kNotSignedIn, |
| CollaborationServiceShareOrManageEvent::kNotSignedIn); |
| RecordCollaborationFlowEvent(GetLogger(), flow_type, |
| CollaborationServiceFlowEvent::kNotSignedIn); |
| |
| controller_->delegate()->ShowAuthenticationUi( |
| flow_type, base::BindOnce(&AuthenticatingState::ProcessOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ProcessOutcome(Outcome outcome) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (Outcome::kCancel == outcome) { |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kCanceledNotSignedIn, |
| CollaborationServiceShareOrManageEvent::kCanceledNotSignedIn); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kCanceledNotSignedIn); |
| } |
| |
| ControllerState::ProcessOutcome(outcome); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| ServiceStatus status = |
| controller_->collaboration_service()->GetServiceStatus(); |
| if (!status.IsAllowedToJoin()) { |
| controller_->TransitionTo(StateId::kWaitingForPolicyUpdate); |
| return; |
| } |
| |
| FlowType flow_type = controller_->flow().type; |
| if (!status.IsAuthenticationValid()) { |
| // Set up the timeout exit task. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&AuthenticatingState::HandleError, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::Minutes(30)); |
| collaboration_service_observer_.Observe( |
| controller_->collaboration_service()); |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kSigninVerificationFailed, |
| CollaborationServiceShareOrManageEvent::kSigninVerificationFailed); |
| RecordCollaborationFlowEvent( |
| GetLogger(), flow_type, |
| CollaborationServiceFlowEvent::kSigninVerificationFailed); |
| return; |
| } |
| |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kSigninVerified, |
| CollaborationServiceShareOrManageEvent::kSigninVerified); |
| RecordCollaborationFlowEvent( |
| GetLogger(), flow_type, CollaborationServiceFlowEvent::kSigninVerified); |
| // TODO(crbug.com/380957996): Handle signin/sync changes during a flow. |
| FinishAndTransition(); |
| } |
| |
| // CollaborationService::Observer implementation. |
| void OnServiceStatusChanged(const ServiceStatusUpdate& update) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| ServiceStatus status = update.new_status; |
| if (!status.IsAllowedToJoin()) { |
| controller_->TransitionTo(StateId::kWaitingForPolicyUpdate); |
| return; |
| } |
| |
| if (status.IsAuthenticationValid()) { |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kSigninVerifiedInObserver, |
| CollaborationServiceShareOrManageEvent::kSigninVerifiedInObserver); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kSigninVerifiedInObserver); |
| FinishAndTransition(); |
| } |
| } |
| |
| private: |
| void FinishAndTransition() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| RecordLatency( |
| GetLogger(), |
| metrics::CollaborationServiceStep::kAuthenticationInitToSuccess, |
| base::Time::Now() - start_time_); |
| controller_->delegate()->NotifySignInAndSyncStatusChange(); |
| controller_->TransitionTo(StateId::kWaitingForServicesToInitialize); |
| } |
| |
| base::Time start_time_; |
| base::ScopedObservation<CollaborationService, CollaborationService::Observer> |
| collaboration_service_observer_{this}; |
| |
| base::WeakPtrFactory<AuthenticatingState> local_weak_ptr_factory_{this}; |
| }; |
| |
| class WaitingForServicesToInitialize |
| : public ControllerState, |
| public tab_groups::TabGroupSyncService::Observer, |
| public data_sharing::DataSharingService::Observer { |
| public: |
| WaitingForServicesToInitialize(StateId id, |
| CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| // ControllerState implementation. |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| start_time_ = base::Time::Now(); |
| // Timeout waiting. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| &WaitingForServicesToInitialize::HandleErrorWithMetrics, |
| weak_ptr_factory_.GetWeakPtr(), |
| CollaborationServiceJoinEvent::kTimeoutWaitingForServicesReady, |
| CollaborationServiceFlowEvent::kJoinTimeoutWaitingForServicesReady), |
| base::Seconds(5)); |
| // TODO(crbug.com/392791204): Wait for tab group sync to be ready. |
| is_data_sharing_ready_ = |
| controller_->data_sharing_service()->IsGroupDataModelLoaded(); |
| if (!is_data_sharing_ready_) { |
| data_sharing_observer_.Observe(controller_->data_sharing_service()); |
| } else { |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kDataSharingReadyWhenStarted, |
| CollaborationServiceShareOrManageEvent::kDataSharingReadyWhenStarted); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kDataSharingReadyWhenStarted); |
| } |
| tab_group_sync_observer_.Observe(controller_->tab_group_sync_service()); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| RecordLatency( |
| GetLogger(), |
| metrics::CollaborationServiceStep::kWaitingForServicesInitialization, |
| base::Time::Now() - start_time_); |
| controller_->TransitionTo(StateId::kCheckingFlowRequirements); |
| } |
| |
| // TabGroupSyncService::Observer implementation. |
| void OnInitialized() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kTabGroupServiceReady, |
| CollaborationServiceShareOrManageEvent::kTabGroupServiceReady); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kTabGroupServiceReady); |
| is_tab_group_sync_ready_ = true; |
| MaybeProceed(); |
| } |
| |
| // DataSharingService::Observer implementation. |
| void OnGroupDataModelLoaded() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kDataSharingServiceReadyObserved, |
| CollaborationServiceShareOrManageEvent:: |
| kDataSharingServiceReadyObserved); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kDataSharingServiceReadyObserved); |
| |
| is_data_sharing_ready_ = true; |
| MaybeProceed(); |
| } |
| |
| private: |
| void MaybeProceed() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (is_tab_group_sync_ready_ && is_data_sharing_ready_) { |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kAllServicesReadyForFlow, |
| CollaborationServiceShareOrManageEvent::kAllServicesReadyForFlow); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kAllServicesReadyForFlow); |
| OnProcessingFinishedWithSuccess(); |
| } |
| } |
| |
| base::Time start_time_; |
| bool is_tab_group_sync_ready_{false}; |
| bool is_data_sharing_ready_{false}; |
| base::ScopedObservation<tab_groups::TabGroupSyncService, |
| tab_groups::TabGroupSyncService::Observer> |
| tab_group_sync_observer_{this}; |
| base::ScopedObservation<data_sharing::DataSharingService, |
| data_sharing::DataSharingService::Observer> |
| data_sharing_observer_{this}; |
| }; |
| |
| class CheckingFlowRequirementsState : public ControllerState { |
| public: |
| CheckingFlowRequirementsState(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| RecordJoinOrShareOrManageEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceJoinEvent::kFlowRequirementsMet, |
| CollaborationServiceShareOrManageEvent::kFlowRequirementsMet); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kFlowRequirementsMet); |
| switch (controller_->flow().type) { |
| case FlowType::kJoin: |
| CheckJoinFlowRequirements(); |
| break; |
| case FlowType::kShareOrManage: |
| CheckShareFlowRequirements(); |
| break; |
| case FlowType::kLeaveOrDelete: |
| CheckLeaveOrDeleteFlowRequirements(); |
| break; |
| } |
| } |
| |
| private: |
| void CheckJoinFlowRequirements() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| const data_sharing::GroupId group_id = |
| controller_->flow().join_token().group_id; |
| // Check if user is already part of the group. |
| if (IsPeopleGroupInDataSharing(group_id)) { |
| if (IsTabGroupInSync(group_id)) { |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kOpenedExistingGroup); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinOpenedExistingGroup); |
| controller_->TransitionTo(StateId::kOpeningLocalTabGroup); |
| return; |
| } |
| |
| RecordJoinEvent( |
| GetLogger(), |
| CollaborationServiceJoinEvent::kFoundCollaborationWithoutTabGroup); |
| RecordCollaborationFlowEvent(GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent:: |
| kJoinFoundCollaborationWithoutTabGroup); |
| controller_->TransitionTo(StateId::kWaitingForSyncAndDataSharingGroup); |
| return; |
| } |
| controller_->TransitionTo(StateId::kAddingUserToGroup); |
| } |
| |
| void CheckShareFlowRequirements() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| std::optional<tab_groups::SavedTabGroup> sync_group = |
| controller_->tab_group_sync_service()->GetGroup( |
| controller_->flow().either_id()); |
| if (!sync_group.has_value()) { |
| RecordShareOrManageEvent( |
| GetLogger(), |
| CollaborationServiceShareOrManageEvent::kSyncedTabGroupNotFound); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kSyncedTabGroupNotFound); |
| HandleError(); |
| return; |
| } |
| |
| if (sync_group.value().is_shared_tab_group()) { |
| controller_->TransitionTo(StateId::kShowingManageScreen); |
| return; |
| } |
| |
| controller_->TransitionTo(StateId::kShowingShareScreen); |
| } |
| |
| void CheckLeaveOrDeleteFlowRequirements() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| auto group_id_opt = GetGroupIdFromEitherId(controller_->flow().either_id()); |
| if (!group_id_opt.has_value()) { |
| HandleError(); |
| return; |
| } |
| data_sharing::MemberRole role = |
| controller_->collaboration_service()->GetCurrentUserRoleForGroup( |
| group_id_opt.value()); |
| if (role == data_sharing::MemberRole::kMember) { |
| controller_->TransitionTo(StateId::kLeavingGroup); |
| } else if (role == data_sharing::MemberRole::kOwner) { |
| controller_->TransitionTo(StateId::kDeletingGroup); |
| } else { |
| HandleError(); |
| } |
| } |
| |
| base::WeakPtrFactory<CheckingFlowRequirementsState> local_weak_ptr_factory_{ |
| this}; |
| }; |
| |
| class AddingUserToGroupState : public ControllerState { |
| public: |
| AddingUserToGroupState(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->data_sharing_service()->ReadNewGroup( |
| controller_->flow().join_token(), |
| base::BindOnce( |
| &AddingUserToGroupState::ProcessGroupDataOrFailureOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| controller_->data_sharing_service()->GetSharedEntitiesPreview( |
| controller_->flow().join_token(), |
| base::BindOnce( |
| &AddingUserToGroupState::ProcessSharedDataPreviewOrFailureOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ProcessOutcome(Outcome outcome) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| CHECK_EQ(controller_->flow().type, FlowType::kJoin) |
| << "Only the join flow can transition into the AddingUserToGroup " |
| "state."; |
| |
| switch (outcome) { |
| case Outcome::kSuccess: |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kAddedUserToGroup); |
| RecordLatency(GetLogger(), |
| metrics::CollaborationServiceStep::kFullJoinFlowSuccess, |
| base::Time::Now() - controller_->flow_start_time()); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinAddedUserToGroup); |
| break; |
| case Outcome::kFailure: |
| RecordJoinEvent( |
| GetLogger(), |
| CollaborationServiceJoinEvent::kFailedAddingUserToGroup); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinFailedAddingUserToGroup); |
| |
| break; |
| case Outcome::kCancel: |
| RecordJoinEvent(GetLogger(), CollaborationServiceJoinEvent::kCanceled); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinCanceled); |
| break; |
| case Outcome::kGroupLeftOrDeleted: |
| NOTREACHED() << "kGroupLeftOrDeleted should not happen in " |
| "AddingUserToGroupState"; |
| } |
| |
| ControllerState::ProcessOutcome(outcome); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| RecordJoinEvent(GetLogger(), CollaborationServiceJoinEvent::kAccepted); |
| RecordCollaborationFlowEvent(GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinAccepted); |
| |
| const data_sharing::GroupId group_id = |
| controller_->flow().join_token().group_id; |
| if (IsTabGroupInSync(group_id) && IsPeopleGroupInDataSharing(group_id)) { |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kGroupExistsWhenJoined); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinGroupExistsWhenJoined); |
| controller_->TransitionTo(StateId::kOpeningLocalTabGroup); |
| return; |
| } |
| |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kOpenedNewGroup); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinOpenedNewGroup); |
| controller_->TransitionTo(StateId::kWaitingForSyncAndDataSharingGroup); |
| } |
| |
| private: |
| void ProcessSharedDataPreviewOrFailureOutcome( |
| const data_sharing::DataSharingService::SharedDataPreviewOrFailureOutcome& |
| preview_outcome) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| preview_data_ = preview_outcome; |
| MaybeProceedJoinFlow(); |
| } |
| |
| // Called to process the outcome of data sharing read event. |
| void ProcessGroupDataOrFailureOutcome( |
| const GroupDataOrFailureOutcome& group_outcome) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| read_group_data_ = group_outcome; |
| MaybeProceedJoinFlow(); |
| } |
| |
| void MaybeProceedJoinFlow() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| // Data sharing outcome is not ready yet. |
| if (!preview_data_.has_value() || !read_group_data_.has_value()) { |
| return; |
| } |
| |
| data_sharing::DataSharingService::SharedDataPreviewOrFailureOutcome |
| preview_outcome = preview_data_.value(); |
| GroupDataOrFailureOutcome group_outcome = read_group_data_.value(); |
| |
| // Check if user is already in group. |
| if (group_outcome.has_value()) { |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kReadNewGroupSuccess); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinReadNewGroupSuccess); |
| } |
| |
| if (group_outcome.has_value() && |
| GetCurrentUserRoleForGroup(controller_->identity_manager(), |
| group_outcome.value()) != |
| data_sharing::MemberRole::kUnknown) { |
| RecordJoinEvent( |
| GetLogger(), |
| CollaborationServiceJoinEvent::kReadNewGroupUserIsAlreadyMember); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinReadNewGroupUserIsAlreadyMember); |
| controller_->TransitionTo(StateId::kWaitingForSyncAndDataSharingGroup); |
| return; |
| } |
| |
| // Handle preview failures first. |
| if (!preview_outcome.has_value()) { |
| switch (preview_outcome.error()) { |
| case data_sharing::DataSharingService::DataPreviewActionFailure:: |
| kGroupFull: |
| RecordJoinEvent( |
| GetLogger(), |
| CollaborationServiceJoinEvent::kPreviewGroupFullError); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinPreviewGroupFullError); |
| HandleErrorWithType(ErrorInfo::Type::kGroupFull); |
| return; |
| case data_sharing::DataSharingService::DataPreviewActionFailure:: |
| kGroupClosedByOrganizationPolicy: |
| HandleErrorWithType( |
| ErrorInfo::Type::kGroupClosedByOrganizationPolicy); |
| return; |
| default: |
| break; |
| } |
| } |
| |
| if (!preview_outcome.has_value() || |
| !preview_outcome.value().shared_tab_group_preview.has_value()) { |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kPreviewFailure); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinPreviewFailure); |
| HandleErrorWithType(ErrorInfo::Type::kInvalidUrl); |
| return; |
| } |
| |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kPreviewSuccess); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinPreviewSuccess); |
| |
| // Handle read group failure next. |
| if (!group_outcome.has_value()) { |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kReadNewGroupFailed); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinReadNewGroupFailed); |
| HandleErrorWithType(ErrorInfo::Type::kInvalidUrl); |
| return; |
| } |
| |
| // All checks are successful. Continue the join flow. |
| controller_->delegate()->ShowJoinDialog( |
| controller_->flow().join_token(), preview_outcome.value(), |
| base::BindOnce(&AddingUserToGroupState::ProcessOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| std::optional< |
| data_sharing::DataSharingService::SharedDataPreviewOrFailureOutcome> |
| preview_data_{std::nullopt}; |
| std::optional<GroupDataOrFailureOutcome> read_group_data_{std::nullopt}; |
| |
| base::WeakPtrFactory<AddingUserToGroupState> local_weak_ptr_factory_{this}; |
| }; |
| |
| class WaitingForSyncAndDataSharingGroup |
| : public ControllerState, |
| public tab_groups::TabGroupSyncService::Observer, |
| public data_sharing::DataSharingService::Observer { |
| public: |
| WaitingForSyncAndDataSharingGroup(StateId id, |
| CollaborationController* controller) |
| : ControllerState(id, controller) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| start_time_ = base::Time::Now(); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| &WaitingForSyncAndDataSharingGroup::HandleErrorWithMetrics, |
| weak_ptr_factory_.GetWeakPtr(), |
| CollaborationServiceJoinEvent:: |
| kTimeoutWaitingForSyncAndDataSharingGroup, |
| CollaborationServiceFlowEvent:: |
| kJoinTimeoutWaitingForSyncAndDataSharingGroup), |
| kTimeoutWaitingForDataSharingGroup); |
| } |
| |
| // ControllerState implementation. |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| RecordLatency(GetLogger(), |
| metrics::CollaborationServiceStep:: |
| kTabGroupFetchedAfterPeopleGroupJoined, |
| base::Time::Now() - start_time_); |
| controller_->TransitionTo(StateId::kOpeningLocalTabGroup); |
| } |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| const data_sharing::GroupId group_id = |
| controller_->flow().join_token().group_id; |
| bool tab_group_exists = IsTabGroupInSync(group_id); |
| bool people_group_exists = IsPeopleGroupInDataSharing(group_id); |
| |
| if (tab_group_exists && people_group_exists) { |
| OnProcessingFinishedWithSuccess(); |
| return; |
| } |
| |
| if (!tab_group_exists) { |
| tab_group_sync_observer_.Observe(controller_->tab_group_sync_service()); |
| } |
| |
| if (!people_group_exists) { |
| data_sharing_observer_.Observe(controller_->data_sharing_service()); |
| // Force update data sharing service. |
| controller_->data_sharing_service()->ReadGroupDeprecated( |
| group_id, base::DoNothing()); |
| } |
| } |
| |
| // TabGroupSyncService::Observer implementation. |
| void OnTabGroupAdded(const tab_groups::SavedTabGroup& group, |
| tab_groups::TriggerSource source) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| const data_sharing::GroupId group_id = |
| controller_->flow().join_token().group_id; |
| if (group.is_shared_tab_group() && |
| group.collaboration_id().value() == |
| syncer::CollaborationId(group_id.value()) && |
| IsPeopleGroupInDataSharing(group_id)) { |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kTabGroupFetched); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinTabGroupFetched); |
| ProcessOutcome(Outcome::kSuccess); |
| } |
| } |
| |
| // DataSharingService::Observer implementation. |
| void OnGroupAdded(const data_sharing::GroupData& group_data, |
| const base::Time& event_time) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| const data_sharing::GroupId group_id = |
| controller_->flow().join_token().group_id; |
| if (group_data.group_token.group_id.value() == group_id.value() && |
| IsTabGroupInSync(group_id)) { |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kPeopleGroupFetched); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinPeopleGroupFetched); |
| ProcessOutcome(Outcome::kSuccess); |
| } |
| } |
| |
| private: |
| base::Time start_time_; |
| base::ScopedObservation<tab_groups::TabGroupSyncService, |
| tab_groups::TabGroupSyncService::Observer> |
| tab_group_sync_observer_{this}; |
| base::ScopedObservation<data_sharing::DataSharingService, |
| data_sharing::DataSharingService::Observer> |
| data_sharing_observer_{this}; |
| }; |
| |
| class OpeningLocalTabGroupState : public ControllerState { |
| public: |
| OpeningLocalTabGroupState(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| // Only the join flow has a valid `group_id`. |
| CHECK_EQ(controller_->flow().type, FlowType::kJoin); |
| |
| RecordJoinEvent(GetLogger(), |
| CollaborationServiceJoinEvent::kPromoteTabGroup); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kJoinPromoteTabGroup); |
| controller_->delegate()->PromoteTabGroup( |
| controller_->flow().join_token().group_id, |
| base::BindOnce(&OpeningLocalTabGroupState::ProcessOutcome, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->Exit(); |
| } |
| }; |
| |
| class ShowingShareScreen : public ControllerState { |
| public: |
| ShowingShareScreen(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| CHECK_EQ(controller_->flow().type, FlowType::kShareOrManage); |
| RecordShareOrManageEvent( |
| GetLogger(), CollaborationServiceShareOrManageEvent::kShareDialogShown); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kShareDialogShown); |
| |
| controller_->delegate()->ShowShareDialog( |
| controller_->flow().either_id(), |
| base::BindOnce(&ShowingShareScreen::OnCollaborationIdCreated, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->TransitionTo(StateId::kMakingTabGroupShared); |
| } |
| |
| private: |
| void OnCollaborationIdCreated( |
| Outcome outcome, |
| std::optional<data_sharing::GroupToken> group_token) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| // TODO(haileywang): The following code imitate old behavior to not break |
| // tests. Follow new behavior once all platform adjust to new share |
| // behavior. |
| if (outcome == Outcome::kFailure) { |
| RecordShareOrManageEvent( |
| GetLogger(), |
| CollaborationServiceShareOrManageEvent::kCollaborationIdMissing); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kCollaborationIdMissing); |
| HandleError(); |
| return; |
| } |
| |
| if (outcome == Outcome::kCancel) { |
| RecordShareOrManageEvent(GetLogger(), |
| CollaborationServiceShareOrManageEvent:: |
| kCollaborationIdShareCanceled); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kCollaborationIdShareCanceled); |
| controller_->Exit(); |
| return; |
| } |
| |
| if (!group_token) { |
| RecordShareOrManageEvent(GetLogger(), |
| CollaborationServiceShareOrManageEvent:: |
| kCollaborationIdEmptyGroupToken); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kCollaborationIdEmptyGroupToken); |
| controller_->Exit(); |
| return; |
| } |
| |
| if (!group_token.value().IsValid()) { |
| RecordShareOrManageEvent( |
| GetLogger(), |
| CollaborationServiceShareOrManageEvent::kCollaborationIdInvalid); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kCollaborationIdInvalid); |
| controller_->Exit(); |
| return; |
| } |
| |
| controller_->flow().set_share_token(group_token.value()); |
| RecordShareOrManageEvent( |
| GetLogger(), |
| CollaborationServiceShareOrManageEvent::kCollaborationGroupCreated); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kCollaborationGroupCreated); |
| ProcessOutcome(outcome); |
| } |
| |
| base::WeakPtrFactory<ShowingShareScreen> local_weak_ptr_factory_{this}; |
| }; |
| |
| class MakingTabGroupShared : public ControllerState { |
| public: |
| MakingTabGroupShared(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| start_time_ = base::Time::Now(); |
| CHECK_EQ(controller_->flow().type, FlowType::kShareOrManage); |
| |
| std::optional<tab_groups::SavedTabGroup> group = |
| controller_->tab_group_sync_service()->GetGroup( |
| controller_->flow().either_id()); |
| if (!group.has_value()) { |
| RecordShareOrManageEvent(GetLogger(), |
| CollaborationServiceShareOrManageEvent:: |
| kTabGroupMissingBeforeMigration); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kTabGroupMissingBeforeMigration); |
| HandleError(); |
| return; |
| } |
| |
| const std::optional<tab_groups::LocalTabGroupID>& local_group_id = |
| group.value().local_group_id(); |
| CHECK(local_group_id.has_value()); |
| |
| const data_sharing::GroupToken& group_token = |
| controller_->flow().share_token(); |
| |
| controller_->tab_group_sync_service()->MakeTabGroupShared( |
| local_group_id.value(), |
| syncer::CollaborationId(group_token.group_id.value()), |
| base::BindOnce(&MakingTabGroupShared::ProcessTabGroupSharingResult, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| |
| controller_->data_sharing_service()->ReadGroupDeprecated( |
| group_token.group_id, |
| base::BindOnce(&MakingTabGroupShared::ProcessGroupDataOrFailureOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| RecordShareOrManageEvent( |
| GetLogger(), CollaborationServiceShareOrManageEvent::kTabGroupShared); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kTabGroupShared); |
| controller_->TransitionTo(StateId::kSharingTabGroupUrl); |
| } |
| |
| private: |
| void ProcessTabGroupSharingResult( |
| tab_groups::TabGroupSyncService::TabGroupSharingResult result) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (result != |
| tab_groups::TabGroupSyncService::TabGroupSharingResult::kSuccess) { |
| RecordShareOrManageEvent( |
| GetLogger(), |
| CollaborationServiceShareOrManageEvent::kMigrationFailure); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kMigrationFailure); |
| HandleError(); |
| return; |
| } |
| |
| is_make_group_shared_complete_ = true; |
| MaybeProceedFlow(); |
| } |
| |
| void ProcessGroupDataOrFailureOutcome( |
| const GroupDataOrFailureOutcome& group_outcome) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (!group_outcome.has_value()) { |
| RecordShareOrManageEvent( |
| GetLogger(), |
| CollaborationServiceShareOrManageEvent::kReadGroupFailed); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kReadGroupFailed); |
| HandleError(); |
| return; |
| } |
| |
| is_read_group_complete_ = true; |
| MaybeProceedFlow(); |
| } |
| |
| void MaybeProceedFlow() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (is_make_group_shared_complete_ && is_read_group_complete_) { |
| RecordLatency( |
| GetLogger(), |
| metrics::CollaborationServiceStep::kLinkReadyAfterGroupCreation, |
| base::Time::Now() - start_time_); |
| OnProcessingFinishedWithSuccess(); |
| } |
| } |
| |
| base::Time start_time_; |
| bool is_make_group_shared_complete_{false}; |
| bool is_read_group_complete_{false}; |
| base::WeakPtrFactory<MakingTabGroupShared> local_weak_ptr_factory_{this}; |
| }; |
| |
| class SharingTabGroupUrl : public ControllerState { |
| public: |
| SharingTabGroupUrl(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| CHECK_EQ(controller_->flow().type, FlowType::kShareOrManage); |
| |
| const data_sharing::GroupToken& group_token = |
| controller_->flow().share_token(); |
| data_sharing::GroupData group_data = data_sharing::GroupData(); |
| group_data.group_token = group_token; |
| |
| auto url = |
| controller_->data_sharing_service()->GetDataSharingUrl(group_data); |
| if (!url) { |
| RecordShareOrManageEvent( |
| GetLogger(), |
| CollaborationServiceShareOrManageEvent::kUrlCreationFailed); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kUrlCreationFailed); |
| HandleError(); |
| return; |
| } |
| |
| RecordShareOrManageEvent( |
| GetLogger(), CollaborationServiceShareOrManageEvent::kUrlReadyToShare); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kUrlReadyToShare); |
| controller_->delegate()->OnUrlReadyToShare( |
| group_token.group_id, *url, |
| base::BindOnce(&SharingTabGroupUrl::ProcessOutcome, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| controller_->Exit(); |
| } |
| }; |
| |
| class ManageGroupControllerState : public ControllerState { |
| public: |
| ManageGroupControllerState(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void ProcessOutcome(Outcome outcome) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| switch (outcome) { |
| case Outcome::kGroupLeftOrDeleted: |
| controller_->TransitionTo(StateId::kCleaningUpSharedTabGroup); |
| return; |
| default: |
| ControllerState::ProcessOutcome(outcome); |
| return; |
| } |
| } |
| }; |
| |
| class ShowingManageScreen : public ManageGroupControllerState { |
| public: |
| ShowingManageScreen(StateId id, CollaborationController* controller) |
| : ManageGroupControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| CHECK_EQ(controller_->flow().type, FlowType::kShareOrManage); |
| RecordShareOrManageEvent( |
| GetLogger(), |
| CollaborationServiceShareOrManageEvent::kManageDialogShown); |
| RecordCollaborationFlowEvent( |
| GetLogger(), controller_->flow().type, |
| CollaborationServiceFlowEvent::kManageDialogShown); |
| |
| controller_->delegate()->ShowManageDialog( |
| controller_->flow().either_id(), |
| base::BindOnce(&ShowingManageScreen::ProcessOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->Exit(); |
| } |
| |
| private: |
| base::WeakPtrFactory<ShowingManageScreen> local_weak_ptr_factory_{this}; |
| }; |
| |
| class LeavingGroupState : public ManageGroupControllerState { |
| public: |
| LeavingGroupState(StateId id, CollaborationController* controller) |
| : ManageGroupControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->delegate()->ShowLeaveDialog( |
| controller_->flow().either_id(), |
| base::BindOnce(&LeavingGroupState::ProcessOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| auto group_id_opt = GetGroupIdFromEitherId(controller_->flow().either_id()); |
| if (!group_id_opt.has_value()) { |
| HandleError(); |
| return; |
| } |
| controller_->data_sharing_service()->LeaveGroup( |
| group_id_opt.value(), |
| base::BindOnce(&LeavingGroupState::ProcessPeopleGroupActionOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| private: |
| void ProcessPeopleGroupActionOutcome( |
| data_sharing::DataSharingService::PeopleGroupActionOutcome outcome) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (outcome == |
| data_sharing::DataSharingService::PeopleGroupActionOutcome::kSuccess) { |
| controller_->TransitionTo(StateId::kCleaningUpSharedTabGroup); |
| return; |
| } |
| |
| HandleError(); |
| } |
| |
| base::WeakPtrFactory<LeavingGroupState> local_weak_ptr_factory_{this}; |
| }; |
| |
| class DeletingGroupState : public ManageGroupControllerState { |
| public: |
| DeletingGroupState(StateId id, CollaborationController* controller) |
| : ManageGroupControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->delegate()->ShowDeleteDialog( |
| controller_->flow().either_id(), |
| base::BindOnce(&DeletingGroupState::ProcessOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| auto group_id_opt = GetGroupIdFromEitherId(controller_->flow().either_id()); |
| if (!group_id_opt.has_value()) { |
| HandleError(); |
| return; |
| } |
| controller_->data_sharing_service()->DeleteGroup( |
| group_id_opt.value(), |
| base::BindOnce(&DeletingGroupState::ProcessPeopleGroupActionOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| private: |
| void ProcessPeopleGroupActionOutcome( |
| data_sharing::DataSharingService::PeopleGroupActionOutcome outcome) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (outcome == |
| data_sharing::DataSharingService::PeopleGroupActionOutcome::kSuccess) { |
| controller_->TransitionTo(StateId::kCleaningUpSharedTabGroup); |
| return; |
| } |
| |
| HandleError(); |
| } |
| |
| base::WeakPtrFactory<DeletingGroupState> local_weak_ptr_factory_{this}; |
| }; |
| |
| class CleaningUpSharedTabGroupState : public ControllerState { |
| public: |
| CleaningUpSharedTabGroupState(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| auto group_id_opt = GetGroupIdFromEitherId(controller_->flow().either_id()); |
| if (!group_id_opt.has_value()) { |
| HandleError(); |
| return; |
| } |
| data_sharing::GroupId group_id = group_id_opt.value(); |
| |
| controller_->tab_group_sync_service()->OnCollaborationRemoved( |
| syncer::CollaborationId(group_id.value())); |
| controller_->data_sharing_service()->OnCollaborationGroupRemoved(group_id); |
| OnProcessingFinishedWithSuccess(); |
| } |
| |
| void OnProcessingFinishedWithSuccess() override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->Exit(); |
| } |
| }; |
| |
| class ErrorState : public ControllerState { |
| public: |
| ErrorState(StateId id, CollaborationController* controller) |
| : ControllerState(id, controller) {} |
| |
| void OnEnter(const ErrorInfo& error) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->delegate()->ShowError( |
| error, base::BindOnce(&ErrorState::ProcessOutcome, |
| local_weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ProcessOutcome(Outcome outcome) override { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| controller_->Exit(); |
| } |
| |
| private: |
| base::WeakPtrFactory<ErrorState> local_weak_ptr_factory_{this}; |
| }; |
| |
| } // namespace |
| |
| CollaborationController::Flow::Flow(FlowType type, |
| const data_sharing::GroupToken& token) |
| : type(type), join_token_(token) { |
| DCHECK(type == FlowType::kJoin); |
| } |
| |
| CollaborationController::Flow::Flow(FlowType type, |
| const tab_groups::EitherGroupID& either_id) |
| : type(type), either_id_(either_id) {} |
| |
| CollaborationController::Flow::Flow(const Flow&) = default; |
| |
| CollaborationController::Flow::~Flow() = default; |
| |
| CollaborationController::CollaborationController( |
| const Flow& flow, |
| CollaborationService* collaboration_service, |
| data_sharing::DataSharingService* data_sharing_service, |
| tab_groups::TabGroupSyncService* tab_group_sync_service, |
| syncer::SyncService* sync_service, |
| signin::IdentityManager* identity_manager, |
| std::unique_ptr<CollaborationControllerDelegate> delegate, |
| FinishCallback finish_and_delete) |
| : flow_(flow), |
| collaboration_service_(collaboration_service), |
| data_sharing_service_(data_sharing_service), |
| tab_group_sync_service_(tab_group_sync_service), |
| sync_service_(sync_service), |
| identity_manager_(identity_manager), |
| delegate_(std::move(delegate)), |
| finish_and_delete_(std::move(finish_and_delete)) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| tab_group_sync_service_observer_.Observe(tab_group_sync_service_); |
| collaboration_service_observer_.Observe(collaboration_service_); |
| |
| flow_start_time_ = base::Time::Now(); |
| |
| RecordJoinOrShareOrManageEvent( |
| data_sharing_service_->GetLogger(), flow_.type, |
| CollaborationServiceJoinEvent::kStarted, |
| CollaborationServiceShareOrManageEvent::kStarted); |
| RecordCollaborationFlowEvent(data_sharing_service_->GetLogger(), flow_.type, |
| CollaborationServiceFlowEvent::kStarted); |
| current_state_ = std::make_unique<PendingState>( |
| StateId::kPending, this, |
| base::BindOnce(&CollaborationController::Exit, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // Post task to start the flow. This is to make sure all the conflicting flows |
| // have exited and cleaned up. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&CollaborationController::Start, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| CollaborationController::~CollaborationController() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| } |
| |
| void CollaborationController::TransitionTo(StateId state, |
| const ErrorInfo& error) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| VLOG(2) << "Transition from " << GetStateIdString(current_state_->id()) |
| << " to " << GetStateIdString(state); |
| DATA_SHARING_LOG( |
| logger_common::mojom::LogSource::CollaborationService, |
| data_sharing_service()->GetLogger(), |
| CreateStateTransitionLogString(current_state_->id(), state, error)); |
| DCHECK(IsValidStateTransition(current_state_->id(), state)); |
| current_state_->OnExit(); |
| current_state_ = CreateStateObject(state); |
| current_state_->OnEnter(error); |
| } |
| |
| void CollaborationController::PromoteCurrentSession() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| delegate_->PromoteCurrentScreen(); |
| } |
| |
| void CollaborationController::Exit() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (is_deleting_) { |
| // Exit can be triggered by multiple code paths, the delegate itself, or |
| // from the service. It is safe to ignore multiple requets since we are just |
| // waiting for finish_and_delete_ to run in the next post task. |
| return; |
| } |
| |
| // Transition to the cancel state while waiting for full deletion. |
| if (current_state_->id() != StateId::kCancel) { |
| TransitionTo(StateId::kCancel); |
| } |
| delegate_->OnFlowFinished(); |
| is_deleting_ = true; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(finish_and_delete_), this)); |
| } |
| |
| void CollaborationController::Cancel() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (is_deleting_) { |
| // Cancel can be triggered due to identity manager and tab group changes. |
| return; |
| } |
| |
| delegate()->Cancel(base::IgnoreArgs<Outcome>(base::BindOnce( |
| &CollaborationController::Exit, weak_ptr_factory_.GetWeakPtr()))); |
| } |
| |
| void CollaborationController::SetStateForTesting(StateId state) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| current_state_ = CreateStateObject(state); |
| current_state_->OnEnter(ErrorInfo()); |
| } |
| |
| CollaborationController::StateId CollaborationController::GetStateForTesting() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| return current_state_->id(); |
| } |
| |
| void CollaborationController::OnTabGroupRemoved( |
| const tab_groups::LocalTabGroupID& local_id, |
| tab_groups::TriggerSource source) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| // OnTabGroupRemoved() with sync ID cancels the flow for tab group if it was |
| // started with sync ID. |
| CancelShareOrManageFlow(local_id); |
| } |
| |
| void CollaborationController::OnTabGroupRemoved( |
| const base::Uuid& sync_id, |
| tab_groups::TriggerSource source) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| // OnTabGroupRemoved() with local ID cancels the flow for tab group if it was |
| // started with local ID. |
| CancelShareOrManageFlow(sync_id); |
| } |
| |
| void CollaborationController::OnTabGroupMigrated( |
| const tab_groups::SavedTabGroup& new_group, |
| const base::Uuid& old_sync_id, |
| tab_groups::TriggerSource source) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (new_group.is_shared_tab_group()) { |
| // The group is shared, no action needed. |
| return; |
| } |
| |
| // Cancel only works when the EitherGroupID variant matches the provided ID, |
| // so try cancelling with both sync ID and local ID. |
| CancelShareOrManageFlow(old_sync_id); |
| if (new_group.local_group_id().has_value()) { |
| CancelShareOrManageFlow(new_group.local_group_id().value()); |
| } |
| } |
| |
| void CollaborationController::OnServiceStatusChanged( |
| const ServiceStatusUpdate& update) { |
| // If the Shared Tab Groups feature, sync or signin got disabled by an |
| // enterprise policy while this flow is active, cancel the current flow and |
| // show an error. |
| if (update.old_status.collaboration_status != |
| CollaborationStatus::kDisabledForPolicy && |
| update.new_status.collaboration_status == |
| CollaborationStatus::kDisabledForPolicy) { |
| if (update.new_status.signin_status == SigninStatus::kSigninDisabled) { |
| current_state_->HandleErrorWithType( |
| ErrorInfo::Type::kSigninDisabledByPolicy); |
| } else if (update.new_status.sync_status == |
| SyncStatus::kSyncDisabledByEnterprise) { |
| current_state_->HandleErrorWithType( |
| ErrorInfo::Type::kSyncDisabledByPolicy); |
| } else { |
| current_state_->HandleErrorWithType( |
| ErrorInfo::Type::kSharingDisabledByPolicy); |
| } |
| } |
| } |
| |
| void CollaborationController::CancelShareOrManageFlow( |
| const tab_groups::EitherGroupID& either_id) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (flow_.type == FlowType::kShareOrManage && |
| flow_.either_id() == either_id) { |
| if (current_state_->id() == StateId::kCleaningUpSharedTabGroup) { |
| // Cleanup will cause the TabGroupRemoved event to be observed. It is safe |
| // to ignore. |
| return; |
| } |
| Cancel(); |
| } |
| } |
| |
| bool CollaborationController::IsValidStateTransition(StateId from, StateId to) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| return std::find(kValidTransitions.begin(), kValidTransitions.end(), |
| std::make_pair(from, to)) != std::end(kValidTransitions); |
| } |
| |
| std::unique_ptr<ControllerState> CollaborationController::CreateStateObject( |
| StateId state) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| switch (state) { |
| case StateId::kPending: |
| return std::make_unique<PendingState>(state, this, base::DoNothing()); |
| case StateId::kWaitingForPolicyUpdate: |
| return std::make_unique<WaitingForPolicyUpdateState>(state, this); |
| case StateId::kAuthenticating: |
| return std::make_unique<AuthenticatingState>(state, this); |
| case StateId::kWaitingForServicesToInitialize: |
| return std::make_unique<WaitingForServicesToInitialize>(state, this); |
| case StateId::kCheckingFlowRequirements: |
| return std::make_unique<CheckingFlowRequirementsState>(state, this); |
| case StateId::kAddingUserToGroup: |
| return std::make_unique<AddingUserToGroupState>(state, this); |
| case StateId::kWaitingForSyncAndDataSharingGroup: |
| return std::make_unique<WaitingForSyncAndDataSharingGroup>(state, this); |
| case StateId::kOpeningLocalTabGroup: |
| return std::make_unique<OpeningLocalTabGroupState>(state, this); |
| case StateId::kShowingShareScreen: |
| return std::make_unique<ShowingShareScreen>(state, this); |
| case StateId::kMakingTabGroupShared: |
| return std::make_unique<MakingTabGroupShared>(state, this); |
| case StateId::kSharingTabGroupUrl: |
| return std::make_unique<SharingTabGroupUrl>(state, this); |
| case StateId::kShowingManageScreen: |
| return std::make_unique<ShowingManageScreen>(state, this); |
| case StateId::kLeavingGroup: |
| return std::make_unique<LeavingGroupState>(state, this); |
| case StateId::kDeletingGroup: |
| return std::make_unique<DeletingGroupState>(state, this); |
| case StateId::kCleaningUpSharedTabGroup: |
| return std::make_unique<CleaningUpSharedTabGroupState>(state, this); |
| case StateId::kCancel: |
| return std::make_unique<ControllerState>(state, this); |
| case StateId::kError: |
| return std::make_unique<ErrorState>(state, this); |
| } |
| } |
| |
| void CollaborationController::Start() { |
| current_state_->OnEnter(ErrorInfo()); |
| } |
| |
| } // namespace collaboration |