chromium/ios/chrome/browser/shared/coordinator/scene/scene_state.mm

// Copyright 2019 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/shared/coordinator/scene/scene_state.h"

#import "base/apple/foundation_util.h"
#import "base/ios/crb_protocol_observers.h"
#import "base/ios/ios_util.h"
#import "base/logging.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/chrome_overlay_window.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_controller.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_util.h"

namespace {

// Preference key used to store which profile is current.
NSString* kIncognitoCurrentKey = @"IncognitoActive";

// Represents the state of the -[SceneState incognitoContentVisible] property
// that is saved in session storage (and thus unknown during app startup and
// will be lazily loaded when needed).
enum class ContentVisibility {
  kUnknown,
  kRegular,
  kIncognito,
};

// Returns the value of ContentVisibility depending on `isIncognito` boolean.
ContentVisibility ContentVisibilityForIncognito(BOOL isIncognito) {
  return isIncognito ? ContentVisibility::kIncognito
                     : ContentVisibility::kRegular;
}

}  // namespace

@interface SceneStateObserverList : CRBProtocolObservers <SceneStateObserver>
@end

@implementation SceneStateObserverList
@end

#pragma mark - SceneState

@interface SceneState ()

// Container for this object's observers.
@property(nonatomic, strong) SceneStateObserverList* observers;

// Agents attached to this scene.
@property(nonatomic, strong) NSMutableArray<id<SceneAgent>>* agents;

@end

@implementation SceneState {
  ContentVisibility _contentVisibility;
  NSString* _sceneSessionID;
}

- (instancetype)initWithAppState:(AppState*)appState {
  self = [super init];
  if (self) {
    _appState = appState;
    _observers = [SceneStateObserverList
        observersWithProtocol:@protocol(SceneStateObserver)];
    _contentVisibility = ContentVisibility::kUnknown;
    _agents = [[NSMutableArray alloc] init];
    _sceneSessionID = @"";

    // AppState might be nil in tests.
    if (appState) {
      [self addObserver:appState];
    }
  }
  return self;
}

#pragma mark - public

- (void)addObserver:(id<SceneStateObserver>)observer {
  [self.observers addObserver:observer];
}

- (void)removeObserver:(id<SceneStateObserver>)observer {
  [self.observers removeObserver:observer];
}

- (void)addAgent:(id<SceneAgent>)agent {
  DCHECK(agent);
  [self.agents addObject:agent];
  [agent setSceneState:self];
}

- (NSArray*)connectedAgents {
  return self.agents;
}

#pragma mark - Setters & Getters.

- (UIWindow*)window {
  UIWindow* mainWindow = nil;
  for (UIWindow* window in self.scene.windows) {
    if ([window isKindOfClass:[ChromeOverlayWindow class]]) {
      mainWindow = window;
    }
  }
  return mainWindow;
}

- (void)setScene:(UIWindowScene*)scene {
  _scene = scene;
  if (_scene) {
    _sceneSessionID = SessionIdentifierForScene(_scene);
  } else {
    _sceneSessionID = @"";
  }
}

- (void)setActivationLevel:(SceneActivationLevel)newLevel {
  if (_activationLevel == newLevel) {
    return;
  }
  _activationLevel = newLevel;

  [self.observers sceneState:self transitionedToActivationLevel:newLevel];
}

- (void)setUIEnabled:(BOOL)UIEnabled {
  if (_UIEnabled == UIEnabled) {
    return;
  }

  _UIEnabled = UIEnabled;
  if (UIEnabled) {
    [self.observers sceneStateDidEnableUI:self];
  } else {
    [self.observers sceneStateDidDisableUI:self];
  }
}

- (id<BrowserProviderInterface>)browserProviderInterface {
  return self.controller.browserProviderInterface;
}

- (void)setPresentingModalOverlay:(BOOL)presentingModalOverlay {
  if (_presentingModalOverlay == presentingModalOverlay) {
    return;
  }
  if (presentingModalOverlay) {
    [self.observers sceneStateWillShowModalOverlay:self];
  } else {
    [self.observers sceneStateWillHideModalOverlay:self];
  }

  _presentingModalOverlay = presentingModalOverlay;

  if (!presentingModalOverlay) {
    [self.observers sceneStateDidHideModalOverlay:self];
  }
}

- (void)setURLContextsToOpen:(NSSet<UIOpenURLContext*>*)URLContextsToOpen {
  if (_URLContextsToOpen == nil || URLContextsToOpen == nil) {
    _URLContextsToOpen = URLContextsToOpen;
  } else {
    _URLContextsToOpen =
        [_URLContextsToOpen setByAddingObjectsFromSet:URLContextsToOpen];
  }
  if (_URLContextsToOpen) {
    [self.observers sceneState:self hasPendingURLs:_URLContextsToOpen];
  }
}

- (BOOL)incognitoContentVisible {
  switch (_contentVisibility) {
    case ContentVisibility::kRegular:
      return NO;

    case ContentVisibility::kIncognito:
      return YES;

    case ContentVisibility::kUnknown: {
      const BOOL incognitoContentVisible = [base::apple::ObjCCast<NSNumber>(
          [self sessionObjectForKey:kIncognitoCurrentKey]) boolValue];

      _contentVisibility =
          ContentVisibilityForIncognito(incognitoContentVisible);
      DCHECK_NE(_contentVisibility, ContentVisibility::kUnknown);

      return incognitoContentVisible;
    }
  }
}

- (void)setIncognitoContentVisible:(BOOL)incognitoContentVisible {
  const ContentVisibility contentVisibility =
      ContentVisibilityForIncognito(incognitoContentVisible);
  if (contentVisibility == _contentVisibility) {
    return;
  }

  _contentVisibility = contentVisibility;

  [self setSessionObject:@(incognitoContentVisible)
                  forKey:kIncognitoCurrentKey];

  [self.observers sceneState:self
      isDisplayingIncognitoContent:incognitoContentVisible];
}

- (void)setPendingUserActivity:(NSUserActivity*)pendingUserActivity {
  _pendingUserActivity = pendingUserActivity;
  [self.observers sceneState:self receivedUserActivity:pendingUserActivity];
}

- (void)setSigninInProgress:(BOOL)signinInProgress {
  DCHECK(_signinInProgress != signinInProgress);

  _signinInProgress = signinInProgress;
  if (signinInProgress) {
    [self.observers signinDidStart:self];
  } else {
    [self.observers signinDidEnd:self];
  }
}

#pragma mark - UIBlockerTarget

- (id<UIBlockerManager>)uiBlockerManager {
  return _appState;
}

- (void)bringBlockerToFront:(UIScene*)requestingScene {
  if (!base::ios::IsMultipleScenesSupported()) {
    return;
  }
  UISceneActivationRequestOptions* options =
      [[UISceneActivationRequestOptions alloc] init];
  options.requestingScene = requestingScene;

  [[UIApplication sharedApplication]
      requestSceneSessionActivation:self.scene.session
                       userActivity:nil
                            options:options
                       errorHandler:^(NSError* error) {
                         LOG(ERROR) << base::SysNSStringToUTF8(
                             error.localizedDescription);
                         NOTREACHED_IN_MIGRATION();
                       }];
}

#pragma mark - debug

- (NSString*)description {
  NSString* activityString = nil;
  switch (self.activationLevel) {
    case SceneActivationLevelUnattached: {
      activityString = @"Unattached";
      break;
    }

    case SceneActivationLevelDisconnected: {
      activityString = @"Disconnected";
      break;
    }

    case SceneActivationLevelBackground: {
      activityString = @"Background";
      break;
    }
    case SceneActivationLevelForegroundInactive: {
      activityString = @"Foreground, Inactive";
      break;
    }
    case SceneActivationLevelForegroundActive: {
      activityString = @"Active";
      break;
    }
  }

  return
      [NSString stringWithFormat:@"SceneState %p (%@)", self, activityString];
}

#pragma mark - Session scoped defaults.

// Helper methods to get/set values that are "per-scene" (such as whether the
// incognito or regular UI is presented, ...). Those methods store/fetch the
// values from -userInfo property of UISceneSession for devices that support
// multi-window or in NSUserDefaults for other device.
//
// The reason the values are not always stored in UISceneSession -userInfo is
// that iOS consider that the "swipe gesture" can mean "close the window" even
// on device that do not support multi-window (such as iPhone) if multi-window
// support is enabled. As enabling the support is done in the Info.plist and
// Chrome does not want to distribute a different app to phones and tablets,
// this means that on iPhone the scene may be closed by the OS and the session
// destroyed. On device that support multi-window, the user has the option to
// re-open the window via a shortcut presented by the OS, but there is no such
// options for device that do not support multi-window.
//
// Finally, the methods also support moving the value from NSUserDefaults to
// UISceneSession -userInfo as required when Chrome is updated from an old
// version to one where multi-window is enabled (or when the users upgrade
// their devices).
//
// The heuristic is:
// -  if the device does not support multi-window, NSUserDefaults is used,
// -  otherwise, the value is first looked up in UISceneSession -userInfo,
//    if present, it is used (and any copy in NSUserDefaults is deleted),
//    if not present, the value is looked in NSUserDefaults.

- (NSObject*)sessionObjectForKey:(NSString*)key {
  if (base::ios::IsMultipleScenesSupported()) {
    NSObject* value = [_scene.session.userInfo objectForKey:key];
    if (value) {
      NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
      if ([userDefaults objectForKey:key]) {
        [userDefaults removeObjectForKey:key];
        [userDefaults synchronize];
      }
      return value;
    }
  }

  NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
  return [userDefaults objectForKey:key];
}

- (void)setSessionObject:(NSObject*)object forKey:(NSString*)key {
  if (base::ios::IsMultipleScenesSupported()) {
    NSMutableDictionary<NSString*, id>* userInfo =
        [NSMutableDictionary dictionaryWithDictionary:_scene.session.userInfo];
    [userInfo setObject:object forKey:key];
    _scene.session.userInfo = userInfo;
    return;
  }

  NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults];
  [userDefaults setObject:object forKey:key];
  [userDefaults synchronize];
}

@end