910e62b5创建于 1月15日历史提交
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/app/profile/search_engine_choice_profile_agent.h"

#import <memory>

#import "base/check.h"
#import "components/search_engines/search_engine_choice/search_engine_choice_service.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/profile/profile_init_stage.h"
#import "ios/chrome/app/profile/profile_state.h"
#import "ios/chrome/browser/device_orientation/ui_bundled/scoped_force_portrait_orientation.h"
#import "ios/chrome/browser/scoped_ui_blocker/ui_bundled/scoped_ui_blocker.h"
#import "ios/chrome/browser/search_engine_choice/coordinator/search_engine_choice_coordinator.h"
#import "ios/chrome/browser/search_engine_choice/model/search_engine_choice_util.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state_observer.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider_interface.h"
#import "ios/chrome/browser/signin/model/signin_util.h"

namespace {

// Enum storing the result of deciding whether the Search Engine Choice
// Screen should be skipped or not.
enum class SkipScreenDecision {
  kUnknown,
  kPresent,
  kSkip,
};

}  // namespace

@interface SearchEngineChoiceProfileAgent () <
    SearchEngineChoiceCoordinatorDelegate>
@end

@implementation SearchEngineChoiceProfileAgent {
  // The coordinator of the search engine choice screen.
  SearchEngineChoiceCoordinator* _searchEngineChoiceCoordinator;
  // UI blocker used by the search engine selection screen.
  std::unique_ptr<ScopedUIBlocker> _searchEngineChoiceUIBlocker;
  // Scene state ID where the search engine choice dialog is displayed.
  std::string _searchEngineChoiceSceneStateID;
  // Store whether the Search Engine Choice Screen should be skipped or not.
  SkipScreenDecision _skipScreenDecision;
  // Used to force the device orientation in portrait mode on iPhone.
  std::unique_ptr<ScopedForcePortraitOrientation> _scopedForceOrientation;
}

#pragma mark - SceneObservingProfileAgent

- (void)sceneState:(SceneState*)sceneState
    transitionedToActivationLevel:(SceneActivationLevel)level {
  if (self.profileState.initStage != ProfileInitStage::kChoiceScreen) {
    return;
  }

  switch (level) {
    case SceneActivationLevelBackground:
    case SceneActivationLevelForegroundInactive:
      // Nothing to do as the SceneState is not ready.
      break;

    case SceneActivationLevelForegroundActive:
      [self maybeShowChoiceScreen:sceneState];
      break;

    case SceneActivationLevelDisconnected:
    case SceneActivationLevelUnattached:
      [self sceneStateDisconnected:sceneState];
      break;
  }
}

#pragma mark - ProfileStateObserver

- (void)profileState:(ProfileState*)profileState
    willTransitionToInitStage:(ProfileInitStage)nextInitStage
                fromInitStage:(ProfileInitStage)fromInitStage {
  if (nextInitStage != ProfileInitStage::kChoiceScreen) {
    return;
  }
  if ([self shouldShowChoiceScreen]) {
    AppState* appState = profileState.appState;
    _scopedForceOrientation = ForcePortraitOrientationOnIphone(appState);
  }
}

- (void)profileState:(ProfileState*)profileState
    didTransitionToInitStage:(ProfileInitStage)nextInitStage
               fromInitStage:(ProfileInitStage)fromInitStage {
  if (nextInitStage == ProfileInitStage::kChoiceScreen) {
    // Try to present the Choice Screen on the first active SceneState.
    if (SceneState* sceneState = profileState.foregroundActiveScene) {
      [self maybeShowChoiceScreen:sceneState];
    }
    return;
  }

  if (fromInitStage == ProfileInitStage::kChoiceScreen) {
    _scopedForceOrientation.reset();
    [profileState removeAgent:self];
  }
}

#pragma mark - SearchEngineChoiceCoordinatorDelegate

- (void)choiceScreenWasDismissed:(SearchEngineChoiceCoordinator*)coordinator {
  DCHECK_EQ(_searchEngineChoiceCoordinator, coordinator);
  [self stopPresentingChoiceScreen];

  // Advance to the next stage when the screen is dismissed by the user.
  if (self.profileState.initStage == ProfileInitStage::kChoiceScreen) {
    [self.profileState queueTransitionToNextInitStage];
  }
}

#pragma mark - Private

// Returns whether the app was started via an external intent (i.e. any
// connected scene was given an external intent).
- (BOOL)startupHadExternalIntent {
  for (SceneState* sceneState in self.profileState.connectedScenes) {
    if (sceneState.startupHadExternalIntent) {
      return YES;
    }
  }

  return NO;
}

// Returns whether the choice screen should be presented or not. The return
// value is cached to ensure stability.
- (BOOL)shouldShowChoiceScreen {
  DCHECK(self.profileState.profile);
  ProfileIOS* profile = self.profileState.profile;

  if (_skipScreenDecision == SkipScreenDecision::kUnknown) {
    if (ShouldDisplaySearchEngineChoiceScreen(
            *profile, /*is_first_run_entrypoint=*/false,
            [self startupHadExternalIntent])) {
      _skipScreenDecision = SkipScreenDecision::kPresent;
    } else {
      _skipScreenDecision = SkipScreenDecision::kSkip;
    }
  }

  return _skipScreenDecision == SkipScreenDecision::kPresent;
}

// Tries to present the choice screen on `sceneState`. If the screen is not
// presented for any reason, then advance the application init state.
- (void)maybeShowChoiceScreen:(SceneState*)sceneState {
  DCHECK_EQ(self.profileState.initStage, ProfileInitStage::kChoiceScreen);
  DCHECK_EQ(sceneState.activationLevel, SceneActivationLevelForegroundActive);

  // If the Choice Screen is already presented on another SceneState, then
  // there is nothing to do.
  if (_searchEngineChoiceCoordinator) {
    DCHECK(_searchEngineChoiceUIBlocker);
    DCHECK(!_searchEngineChoiceSceneStateID.empty());
    return;
  }

  DCHECK(!_searchEngineChoiceUIBlocker);
  DCHECK(_searchEngineChoiceSceneStateID.empty());

  if (![self shouldShowChoiceScreen]) {
    // If there is no need to present the screen, then transition to the next
    // application stage (otherwise the transition will happen once the user
    // has selected a default search engine and completed the workflow). In
    // that case, the method won't be called again.
    [self.profileState queueTransitionToNextInitStage];
    return;
  }

  // Present the screen.
  _searchEngineChoiceSceneStateID = sceneState.sceneSessionID;
  _searchEngineChoiceUIBlocker = std::make_unique<ScopedUIBlocker>(sceneState);

  id<BrowserProvider> browserProvider =
      sceneState.browserProviderInterface.currentBrowserProvider;

  _searchEngineChoiceCoordinator = [[SearchEngineChoiceCoordinator alloc]
      initWithBaseViewController:browserProvider.viewController
                         browser:browserProvider.browser];
  _searchEngineChoiceCoordinator.delegate = self;
  [_searchEngineChoiceCoordinator start];
}

// Tries to dismiss the choice screen if presented by `sceneState` as the
// SceneState will be disconnected or detached soon. If that `sceneState`
// was presenting the Search Engine Choice Screen, move the presentation
// to the next active SceneState, if any.
- (void)sceneStateDisconnected:(SceneState*)sceneState {
  DCHECK_EQ(self.profileState.initStage, ProfileInitStage::kChoiceScreen);
  if (!_searchEngineChoiceCoordinator) {
    // Nothing to do if the Search Engine Choice Screen is not presented.
    return;
  }

  DCHECK(_searchEngineChoiceUIBlocker);
  DCHECK(!_searchEngineChoiceSceneStateID.empty());

  if (_searchEngineChoiceSceneStateID != sceneState.sceneSessionID) {
    // Nothing to do if the Search Engine Choice Screen is not presented
    // by `sceneState`.
    return;
  }

  [self stopPresentingChoiceScreen];
  if (SceneState* nextSceneState = self.profileState.foregroundActiveScene) {
    [self maybeShowChoiceScreen:nextSceneState];
  }
}

// Stops presenting the choice screen. Called after it has been dismissed
// by the user or when programmatically dismissing when a SceneState is
// detached or disconnected while the screen is presented.
- (void)stopPresentingChoiceScreen {
  DCHECK(!_searchEngineChoiceSceneStateID.empty());
  _searchEngineChoiceSceneStateID.clear();
  _searchEngineChoiceUIBlocker.reset();

  _searchEngineChoiceCoordinator.delegate = nil;
  [_searchEngineChoiceCoordinator stop];
  _searchEngineChoiceCoordinator = nil;
}

@end