// Copyright 2021 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/browser/policy/ui_bundled/signin_policy_scene_agent.h"
#import "components/signin/public/base/signin_metrics.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/app_state_observer.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/policy/model/policy_watcher_browser_agent.h"
#import "ios/chrome/browser/policy/model/policy_watcher_browser_agent_observer_bridge.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_controller.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_ui_provider.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/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/policy_change_commands.h"
#import "ios/chrome/browser/shared/public/commands/show_signin_command.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/authentication_service_observer_bridge.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_utils.h"
#import "ios/chrome/browser/ui/scoped_ui_blocker/scoped_ui_blocker.h"
@interface SigninPolicySceneAgent () <AppStateObserver,
AuthenticationServiceObserving,
IdentityManagerObserverBridgeDelegate> {
// Observes changes in identity to make sure that the sign-in state matches
// the BrowserSignin policy.
std::unique_ptr<signin::IdentityManagerObserverBridge>
_identityObserverBridge;
std::unique_ptr<AuthenticationServiceObserverBridge>
_authenticationServiceObserverBridge;
}
// Handler of application commands.
@property(nonatomic, weak, readonly) id<ApplicationCommands>
applicationCommandsHandler;
// Browser of the main interface of the scene.
@property(nonatomic, assign) Browser* mainBrowser;
// SceneUIProvider that provides the scene UI objects.
@property(nonatomic, weak, readonly) id<SceneUIProvider> sceneUIProvider;
// Handler of policy change commands.
@property(nonatomic, weak, readonly) id<PolicyChangeCommands>
policyChangeCommandsHandler;
@end
@implementation SigninPolicySceneAgent
- (instancetype)initWithSceneUIProvider:(id<SceneUIProvider>)sceneUIProvider
applicationCommandsHandler:
(id<ApplicationCommands>)applicationCommandsHandler
policyChangeCommandsHandler:
(id<PolicyChangeCommands>)policyChangeCommandsHandler {
self = [super init];
if (self) {
_sceneUIProvider = sceneUIProvider;
_applicationCommandsHandler = applicationCommandsHandler;
_policyChangeCommandsHandler = policyChangeCommandsHandler;
}
return self;
}
#pragma mark - ObservingSceneAgent
- (void)setSceneState:(SceneState*)sceneState {
[super setSceneState:sceneState];
[self.sceneState.appState addObserver:self];
}
#pragma mark - SceneStateObserver
- (void)sceneStateDidDisableUI:(SceneState*)sceneState {
// Tear down objects tied to the scene state before it is deleted.
[self tearDownObservers];
[self.sceneState.appState removeObserver:self];
[self.sceneState removeObserver:self];
self.mainBrowser = nullptr;
}
- (void)sceneStateDidEnableUI:(SceneState*)sceneState {
// Setup objects that need the browser UI objects before being set.
self.mainBrowser =
self.sceneState.browserProviderInterface.mainBrowserProvider.browser;
[self setupObservers];
}
- (void)sceneState:(SceneState*)sceneState
transitionedToActivationLevel:(SceneActivationLevel)level {
// Monitor the scene activation level to consider showing the sign-in prompt
// when the scene becomes active and in the foreground. In which case the
// scene is visible and interactable.
[self handleSigninPromptsIfUIAvailable];
}
- (void)sceneStateDidHideModalOverlay:(SceneState*)sceneState {
// Reconsider showing the forced sign-in prompt if the UI blocker is
// dismissed which might be because the scene that was displaying the
// sign-in prompt previously was closed. Choosing a new scene to prompt
// is needed in that case.
[self handleSigninPromptsIfUIAvailable];
}
- (void)signinDidEnd:(SceneState*)sceneState {
// Consider showing the forced sign-in prompt when the sign-in prompt is
// dismissed/done because the browser may be signed out if sign-in is
// cancelled.
[self handleSigninPromptsIfUIAvailable];
}
#pragma mark - AppStateObserver
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(InitStage)previousInitStage {
// Monitor the app intialization stages to consider showing the sign-in
// prompts at a point in the initialization of the app that allows it.
[self handleSigninPromptsIfUIAvailable];
}
#pragma mark - AuthenticationServiceObserving
- (void)onServiceStatusChanged {
[self handleSigninPromptsIfUIAvailable];
}
#pragma mark - IdentityManagerObserverBridgeDelegate
- (void)onPrimaryAccountChanged:
(const signin::PrimaryAccountChangeEvent&)event {
// Consider showing the sign-in prompts when there is change in the
// primary account.
[self handleSigninPromptsIfUIAvailable];
}
#pragma mark - Internal
- (void)setupObservers {
DCHECK(self.mainBrowser);
ChromeBrowserState* browserState = self.mainBrowser->GetBrowserState();
// Set observer for service status changes.
AuthenticationService* authService =
AuthenticationServiceFactory::GetForBrowserState(browserState);
_authenticationServiceObserverBridge =
std::make_unique<AuthenticationServiceObserverBridge>(authService, self);
// Set observer for primary account changes.
signin::IdentityManager* identityManager =
IdentityManagerFactory::GetForBrowserState(browserState);
_identityObserverBridge =
std::make_unique<signin::IdentityManagerObserverBridge>(identityManager,
self);
}
- (void)tearDownObservers {
_authenticationServiceObserverBridge.reset();
_identityObserverBridge.reset();
}
- (BOOL)isForcedSignInRequiredByPolicy {
DCHECK(self.mainBrowser);
AuthenticationService* authService =
AuthenticationServiceFactory::GetForBrowserState(
self.mainBrowser->GetBrowserState());
switch (authService->GetServiceStatus()) {
case AuthenticationService::ServiceStatus::SigninAllowed:
case AuthenticationService::ServiceStatus::SigninDisabledByInternal:
case AuthenticationService::ServiceStatus::SigninDisabledByUser:
case AuthenticationService::ServiceStatus::SigninDisabledByPolicy:
return NO;
case AuthenticationService::ServiceStatus::SigninForcedByPolicy:
break;
}
// Skip prompting to sign-in when there is already a primary account
// signed in.
return !authService->HasPrimaryIdentity(signin::ConsentLevel::kSignin);
}
// Handle the policy sign-in prompts if the scene UI is available to show
// prompts.
- (void)handleSigninPromptsIfUIAvailable {
if (![self isUIAvailableToPrompt]) {
return;
}
if (self.sceneState.appState.shouldShowForceSignOutPrompt) {
// Show the sign-out prompt if the user was signed out due to policy.
[self.policyChangeCommandsHandler showForceSignedOutPrompt];
self.sceneState.appState.shouldShowForceSignOutPrompt = NO;
}
if ([self isForcedSignInRequiredByPolicy]) {
// Sanity check that when the policy is handled while there is a UIBlocker
// that the scene that will show the sign-in prompt corresponds to the
// target of the UI blocker.
if (self.sceneState.appState.currentUIBlocker) {
DCHECK(self.sceneState.appState.currentUIBlocker == self.sceneState);
}
// Put a UI blocker to stop the other scenes from handling the policy.
// This UI blocker will be superimposed on the one of the sign-in prompt
// command and maybe the existing sign-in prompt (to be dismissed) to not
// leave any gap that would allow the other scenes to handle the sign-in
// policy (by keeping `sceneState.presentingModalOverlay` == YES). There
// won't be issues with the superimpositions of the UI blockers because this
// is done on the same SceneState target, which will only increase the
// target counter. If the scene is dismissed, the count will be decremented
// to zero leaving the way for another scene to take over the forced
// sign-in prompt.
__block std::unique_ptr<ScopedUIBlocker> uiBlocker =
std::make_unique<ScopedUIBlocker>(self.sceneState);
__weak __typeof(self) weakSelf = self;
[self.applicationCommandsHandler dismissModalDialogsWithCompletion:^{
[weakSelf showForcedSigninPrompt];
// Remove the blocker after the showSignin: command to make sure that the
// blocker doesn't go down to 0 in which case the other scenes will try to
// take over handling the force sign-in prompt.
uiBlocker.reset();
}];
}
}
// Shows the forced sign-in prompt using the application command.
- (void)showForcedSigninPrompt {
ShowSigninCommand* command = [[ShowSigninCommand alloc]
initWithOperation:AuthenticationOperation::kForcedSigninAndSync
identity:nil
accessPoint:signin_metrics::AccessPoint::ACCESS_POINT_FORCED_SIGNIN
promoAction:signin_metrics::PromoAction::
PROMO_ACTION_NO_SIGNIN_PROMO
callback:nil];
[self.applicationCommandsHandler
showSignin:command
baseViewController:[self.sceneUIProvider activeViewController]];
}
// YES if the scene and the app are in a state where the UI of the scene is
// available to show sign-in related prompts.
- (BOOL)isUIAvailableToPrompt {
if (self.sceneState.appState.initStage < InitStageFinal) {
return NO;
}
if (self.sceneState.activationLevel < SceneActivationLevelForegroundActive) {
return NO;
}
if (self.sceneState.appState.currentUIBlocker) {
// Return NO when the scene cannot present views because it is blocked.
return NO;
}
if (self.sceneState.signinInProgress) {
// Prompting to sign-in is already in progress in that scene, no need to
// present the forced sign-in prompt on top of that. The other scenes will
// have `self.sceneState.presentingModalOverlay` == YES which will stop
// them from handling the policy as well. For example, this stops the scene
// from rehandling the forced sign-in policy when foregrounded.
return NO;
}
return YES;
}
@end