chromium/ios/chrome/app/search_engine_choice_app_agent.mm

// 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/search_engine_choice_app_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/browser/search_engine_choice/model/search_engine_choice_util.h"
#import "ios/chrome/browser/search_engines/model/search_engine_choice_service_factory.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"
#import "ios/chrome/browser/ui/scoped_ui_blocker/scoped_ui_blocker.h"
#import "ios/chrome/browser/ui/search_engine_choice/search_engine_choice_coordinator.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 SearchEngineChoiceAppAgent () <SearchEngineChoiceCoordinatorDelegate>
@end

@implementation SearchEngineChoiceAppAgent {
  // 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.
  NSString* _searchEngineChoiceSceneStateID;
  // Store whether the Search Engine Choice Screen should be skipped or not.
  SkipScreenDecision _skipScreenDecision;
}

#pragma mark - SceneObservingAppAgent

- (void)sceneState:(SceneState*)sceneState
    transitionedToActivationLevel:(SceneActivationLevel)level {
  [super sceneState:sceneState transitionedToActivationLevel:level];
  if (self.appState.initStage != InitStageChoiceScreen) {
    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 - AppStateObserver

- (void)appState:(AppState*)appState
    didTransitionFromInitStage:(InitStage)previousInitStage {
  [super appState:appState didTransitionFromInitStage:previousInitStage];
  if (appState.initStage != InitStageChoiceScreen) {
    return;
  }

  // Try to present the Choice Screen on the first active SceneState.
  if (SceneState* sceneState = appState.foregroundActiveScene) {
    [self maybeShowChoiceScreen:sceneState];
  }
}

#pragma mark - SearchEngineChoiceCoordinatorDelegate

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

  // Advance to the next stage when the screen is dismissed by the user.
  if (self.appState.initStage == InitStageChoiceScreen) {
    [self.appState 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.appState.connectedScenes) {
    if (sceneState.startupHadExternalIntent) {
      return YES;
    }
  }

  return NO;
}

// 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.appState.initStage, InitStageChoiceScreen);
  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);
    return;
  }

  DCHECK(!_searchEngineChoiceUIBlocker);
  DCHECK(!_searchEngineChoiceSceneStateID);

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

  // If the decision to skip the Search Engine Choice Screen has not been
  // made yet, evaluate the conditions, and then store the decision result.
  if (_skipScreenDecision == SkipScreenDecision::kUnknown) {
    DCHECK(browserProvider.browser);
    DCHECK(browserProvider.browser->GetBrowserState());

    // 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.
    if (!ShouldDisplaySearchEngineChoiceScreen(
            *browserProvider.browser->GetBrowserState(),
            search_engines::ChoicePromo::kDialog,
            [self startupHadExternalIntent])) {
      _skipScreenDecision = SkipScreenDecision::kSkip;
      [self.appState queueTransitionToNextInitStage];
      return;
    }

    _skipScreenDecision = SkipScreenDecision::kPresent;
  }

  // This point can be reached multiple time if the decision has been taken
  // to present the screen, then the SceneState used for presentation is
  // closed. In that case, the previous decision must have been to present
  // (otherwise the app would have moved to the next InitStage). Assert it
  // is the case.
  DCHECK_EQ(_skipScreenDecision, SkipScreenDecision::kPresent);

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

  _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.appState.initStage, InitStageChoiceScreen);
  if (!_searchEngineChoiceCoordinator) {
    // Nothing to do if the Search Engine Choice Screen is not presented.
    return;
  }

  DCHECK(_searchEngineChoiceUIBlocker);
  DCHECK(_searchEngineChoiceSceneStateID);

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

  [self stopPresentingChoiceScreen];
  if (SceneState* nextSceneState = self.appState.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);
  _searchEngineChoiceSceneStateID = nil;
  _searchEngineChoiceUIBlocker.reset();

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

@end