| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/ui/authentication/signed_in_accounts_view_controller.h" |
| |
| #import "base/mac/foundation_util.h" |
| #include "components/signin/core/browser/account_tracker_service.h" |
| #include "components/signin/core/browser/profile_oauth2_token_service.h" |
| #include "components/signin/ios/browser/oauth2_token_service_observer_bridge.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #include "ios/chrome/browser/signin/account_tracker_service_factory.h" |
| #include "ios/chrome/browser/signin/authentication_service.h" |
| #include "ios/chrome/browser/signin/authentication_service_factory.h" |
| #include "ios/chrome/browser/signin/chrome_identity_service_observer_bridge.h" |
| #include "ios/chrome/browser/signin/oauth2_token_service_factory.h" |
| #import "ios/chrome/browser/ui/authentication/resized_avatar_cache.h" |
| #import "ios/chrome/browser/ui/collection_view/cells/collection_view_account_item.h" |
| #import "ios/chrome/browser/ui/collection_view/collection_view_controller.h" |
| #import "ios/chrome/browser/ui/collection_view/collection_view_model.h" |
| #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" |
| #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" |
| #import "ios/chrome/browser/ui/commands/generic_chrome_command.h" |
| #include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
| #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| #include "ios/chrome/grit/ios_chromium_strings.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #include "ios/public/provider/chrome/browser/chrome_browser_provider.h" |
| #import "ios/public/provider/chrome/browser/signin/chrome_identity.h" |
| #import "ios/public/provider/chrome/browser/signin/chrome_identity_service.h" |
| #include "ios/public/provider/chrome/browser/signin/signin_resources_provider.h" |
| #import "ios/third_party/material_components_ios/src/components/Buttons/src/MaterialButtons.h" |
| #import "ios/third_party/material_components_ios/src/components/Dialogs/src/MaterialDialogs.h" |
| #import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h" |
| #import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h" |
| #include "ui/base/l10n/l10n_util_mac.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| |
| const int kMaxShownAccounts = 3; |
| const CGFloat kAccountsExtraBottomInset = 16; |
| const CGFloat kVerticalPadding = 24; |
| const CGFloat kButtonVerticalPadding = 16; |
| const CGFloat kHorizontalPadding = 24; |
| const CGFloat kAccountsHorizontalPadding = 8; |
| const CGFloat kButtonHorizontalPadding = 16; |
| const CGFloat kBetweenButtonsPadding = 8; |
| const CGFloat kMDCMinHorizontalPadding = 20; |
| const CGFloat kDialogMaxWidth = 328; |
| |
| typedef NS_ENUM(NSInteger, SectionIdentifier) { |
| SectionIdentifierAccounts = kSectionIdentifierEnumZero, |
| }; |
| |
| typedef NS_ENUM(NSInteger, ItemType) { |
| ItemTypeAccount = kItemTypeEnumZero, |
| }; |
| |
| // Whether the Signed In Accounts view is currently being shown. |
| BOOL gSignedInAccountsViewControllerIsShown = NO; |
| |
| } // namespace |
| |
| @interface SignedInAccountsCollectionViewController |
| : CollectionViewController<ChromeIdentityServiceObserver> { |
| ios::ChromeBrowserState* _browserState; // Weak. |
| std::unique_ptr<ChromeIdentityServiceObserverBridge> _identityServiceObserver; |
| ResizedAvatarCache* _avatarCache; |
| |
| // Enable lookup of item corresponding to a given identity GAIA ID string. |
| NSDictionary<NSString*, CollectionViewItem*>* _identityMap; |
| } |
| @end |
| |
| @implementation SignedInAccountsCollectionViewController |
| |
| - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState { |
| self = [super initWithStyle:CollectionViewControllerStyleDefault]; |
| if (self) { |
| _browserState = browserState; |
| _avatarCache = [[ResizedAvatarCache alloc] init]; |
| _identityServiceObserver.reset( |
| new ChromeIdentityServiceObserverBridge(self)); |
| [self loadModel]; |
| } |
| return self; |
| } |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| |
| self.styler.shouldHideSeparators = YES; |
| self.collectionView.backgroundColor = [UIColor clearColor]; |
| |
| // Add an inset at the bottom so the user can see whether it is possible to |
| // scroll to see additional accounts. |
| UIEdgeInsets contentInset = self.collectionView.contentInset; |
| contentInset.bottom += kAccountsExtraBottomInset; |
| self.collectionView.contentInset = contentInset; |
| } |
| |
| #pragma mark CollectionViewController |
| |
| - (void)loadModel { |
| [super loadModel]; |
| CollectionViewModel* model = self.collectionViewModel; |
| NSMutableDictionary<NSString*, CollectionViewItem*>* mutableIdentityMap = |
| [[NSMutableDictionary alloc] init]; |
| |
| [model addSectionWithIdentifier:SectionIdentifierAccounts]; |
| ProfileOAuth2TokenService* oauth2_service = |
| OAuth2TokenServiceFactory::GetForBrowserState(_browserState); |
| AccountTrackerService* accountTracker = |
| ios::AccountTrackerServiceFactory::GetForBrowserState(_browserState); |
| for (const std::string& account_id : oauth2_service->GetAccounts()) { |
| AccountInfo account = accountTracker->GetAccountInfo(account_id); |
| ChromeIdentity* identity = ios::GetChromeBrowserProvider() |
| ->GetChromeIdentityService() |
| ->GetIdentityWithGaiaID(account.gaia); |
| CollectionViewItem* item = [self accountItem:identity]; |
| [model addItem:item toSectionWithIdentifier:SectionIdentifierAccounts]; |
| [mutableIdentityMap setObject:item forKey:identity.gaiaID]; |
| } |
| _identityMap = mutableIdentityMap; |
| } |
| |
| #pragma mark Model objects |
| |
| - (CollectionViewItem*)accountItem:(ChromeIdentity*)identity { |
| CollectionViewAccountItem* item = |
| [[CollectionViewAccountItem alloc] initWithType:ItemTypeAccount]; |
| [self updateAccountItem:item withIdentity:identity]; |
| return item; |
| } |
| |
| - (void)updateAccountItem:(CollectionViewAccountItem*)item |
| withIdentity:(ChromeIdentity*)identity { |
| item.image = [_avatarCache resizedAvatarForIdentity:identity]; |
| item.text = [identity userFullName]; |
| item.detailText = [identity userEmail]; |
| item.chromeIdentity = identity; |
| } |
| |
| #pragma mark MDCCollectionViewStylingDelegate |
| |
| - (BOOL)collectionView:(UICollectionView*)collectionView |
| shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath { |
| return YES; |
| } |
| |
| - (CGFloat)collectionView:(UICollectionView*)collectionView |
| cellHeightAtIndexPath:(NSIndexPath*)indexPath { |
| return MDCCellDefaultOneLineWithAvatarHeight; |
| } |
| |
| - (BOOL)collectionView:(UICollectionView*)collectionView |
| hidesInkViewAtIndexPath:(NSIndexPath*)indexPath { |
| return YES; |
| } |
| |
| #pragma mark ChromeIdentityServiceObserver |
| |
| - (void)onProfileUpdate:(ChromeIdentity*)identity { |
| CollectionViewAccountItem* item = |
| base::mac::ObjCCastStrict<CollectionViewAccountItem>( |
| [_identityMap objectForKey:identity.gaiaID]); |
| [self updateAccountItem:item withIdentity:identity]; |
| NSIndexPath* indexPath = [self.collectionViewModel indexPathForItem:item]; |
| [self.collectionView reloadItemsAtIndexPaths:@[ indexPath ]]; |
| } |
| |
| - (void)onChromeIdentityServiceWillBeDestroyed { |
| _identityServiceObserver.reset(); |
| } |
| |
| @end |
| |
| @interface SignedInAccountsViewController ()< |
| OAuth2TokenServiceObserverBridgeDelegate> { |
| ios::ChromeBrowserState* _browserState; // Weak. |
| std::unique_ptr<OAuth2TokenServiceObserverBridge> _tokenServiceObserver; |
| MDCDialogTransitionController* _transitionController; |
| |
| UILabel* _titleLabel; |
| SignedInAccountsCollectionViewController* _accountsCollection; |
| UILabel* _infoLabel; |
| MDCButton* _primaryButton; |
| MDCButton* _secondaryButton; |
| } |
| @end |
| |
| @implementation SignedInAccountsViewController |
| |
| + (BOOL)shouldBePresentedForBrowserState: |
| (ios::ChromeBrowserState*)browserState { |
| if (!browserState || browserState->IsOffTheRecord()) { |
| return NO; |
| } |
| AuthenticationService* authService = |
| AuthenticationServiceFactory::GetForBrowserState(browserState); |
| return !gSignedInAccountsViewControllerIsShown && |
| authService->IsAuthenticated() && authService->HaveAccountsChanged(); |
| } |
| |
| #pragma mark Initialization |
| |
| - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState { |
| self = [super initWithNibName:nil bundle:nil]; |
| if (self) { |
| _browserState = browserState; |
| _tokenServiceObserver.reset(new OAuth2TokenServiceObserverBridge( |
| OAuth2TokenServiceFactory::GetForBrowserState(_browserState), self)); |
| _transitionController = [[MDCDialogTransitionController alloc] init]; |
| self.modalPresentationStyle = UIModalPresentationCustom; |
| self.transitioningDelegate = _transitionController; |
| } |
| return self; |
| } |
| |
| - (void)dismiss { |
| [self.presentingViewController dismissViewControllerAnimated:YES |
| completion:nil]; |
| } |
| |
| - (void)dealloc { |
| [_primaryButton removeTarget:self |
| action:@selector(onPrimaryButtonPressed:) |
| forControlEvents:UIControlEventTouchDown]; |
| [_secondaryButton removeTarget:self |
| action:@selector(onSecondaryButtonPressed:) |
| forControlEvents:UIControlEventTouchDown]; |
| } |
| |
| #pragma mark UIViewController |
| |
| - (CGSize)preferredContentSize { |
| CGFloat width = MIN(kDialogMaxWidth, |
| self.presentingViewController.view.bounds.size.width - |
| 2 * kMDCMinHorizontalPadding); |
| OAuth2TokenService* token_service = |
| OAuth2TokenServiceFactory::GetForBrowserState(_browserState); |
| int shownAccounts = |
| MIN(kMaxShownAccounts, token_service->GetAccounts().size()); |
| CGSize maxSize = CGSizeMake(width - 2 * kHorizontalPadding, CGFLOAT_MAX); |
| CGSize buttonSize = [_primaryButton sizeThatFits:maxSize]; |
| CGSize infoSize = [_infoLabel sizeThatFits:maxSize]; |
| CGSize titleSize = [_titleLabel sizeThatFits:maxSize]; |
| CGFloat height = kVerticalPadding + titleSize.height + kVerticalPadding + |
| shownAccounts * MDCCellDefaultOneLineWithAvatarHeight + |
| kVerticalPadding + infoSize.height + kVerticalPadding + |
| buttonSize.height + kButtonVerticalPadding; |
| return CGSizeMake(width, height); |
| } |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| |
| self.view.backgroundColor = [UIColor whiteColor]; |
| |
| _titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; |
| _titleLabel.text = |
| l10n_util::GetNSString(IDS_IOS_SIGNED_IN_ACCOUNTS_VIEW_TITLE); |
| _titleLabel.textColor = [[MDCPalette greyPalette] tint900]; |
| _titleLabel.font = [MDCTypography headlineFont]; |
| _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; |
| [self.view addSubview:_titleLabel]; |
| |
| _accountsCollection = [[SignedInAccountsCollectionViewController alloc] |
| initWithBrowserState:_browserState]; |
| _accountsCollection.view.translatesAutoresizingMaskIntoConstraints = NO; |
| [self addChildViewController:_accountsCollection]; |
| [self.view addSubview:_accountsCollection.view]; |
| [_accountsCollection didMoveToParentViewController:self]; |
| |
| _infoLabel = [[UILabel alloc] initWithFrame:CGRectZero]; |
| _infoLabel.text = |
| l10n_util::GetNSString(IDS_IOS_SIGNED_IN_ACCOUNTS_VIEW_INFO); |
| _infoLabel.numberOfLines = 0; |
| _infoLabel.textColor = [[MDCPalette greyPalette] tint700]; |
| _infoLabel.font = [MDCTypography body1Font]; |
| _infoLabel.translatesAutoresizingMaskIntoConstraints = NO; |
| [self.view addSubview:_infoLabel]; |
| |
| _primaryButton = [[MDCFlatButton alloc] init]; |
| [_primaryButton addTarget:self |
| action:@selector(onPrimaryButtonPressed:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| [_primaryButton |
| setTitle:l10n_util::GetNSString(IDS_IOS_SIGNED_IN_ACCOUNTS_VIEW_OK_BUTTON) |
| forState:UIControlStateNormal]; |
| [_primaryButton setBackgroundColor:[[MDCPalette cr_bluePalette] tint500] |
| forState:UIControlStateNormal]; |
| _primaryButton.customTitleColor = [UIColor whiteColor]; |
| _primaryButton.underlyingColorHint = [UIColor blackColor]; |
| _primaryButton.inkColor = [UIColor colorWithWhite:1 alpha:0.2f]; |
| _primaryButton.translatesAutoresizingMaskIntoConstraints = NO; |
| [self.view addSubview:_primaryButton]; |
| |
| _secondaryButton = [[MDCFlatButton alloc] init]; |
| [_secondaryButton addTarget:self |
| action:@selector(onSecondaryButtonPressed:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| [_secondaryButton |
| setTitle:l10n_util::GetNSString( |
| IDS_IOS_SIGNED_IN_ACCOUNTS_VIEW_SETTINGS_BUTTON) |
| forState:UIControlStateNormal]; |
| [_secondaryButton setBackgroundColor:[UIColor whiteColor] |
| forState:UIControlStateNormal]; |
| _secondaryButton.customTitleColor = [[MDCPalette cr_bluePalette] tint500]; |
| _secondaryButton.underlyingColorHint = [UIColor whiteColor]; |
| _secondaryButton.inkColor = [UIColor colorWithWhite:0 alpha:0.06f]; |
| _secondaryButton.translatesAutoresizingMaskIntoConstraints = NO; |
| [self.view addSubview:_secondaryButton]; |
| |
| NSDictionary* views = @{ |
| @"title" : _titleLabel, |
| @"accounts" : _accountsCollection.view, |
| @"info" : _infoLabel, |
| @"primaryButton" : _primaryButton, |
| @"secondaryButton" : _secondaryButton, |
| }; |
| NSDictionary* metrics = @{ |
| @"verticalPadding" : @(kVerticalPadding), |
| @"accountsVerticalPadding" : |
| @(kVerticalPadding - kAccountsExtraBottomInset), |
| @"buttonVerticalPadding" : @(kButtonVerticalPadding), |
| @"horizontalPadding" : @(kHorizontalPadding), |
| @"accountsHorizontalPadding" : @(kAccountsHorizontalPadding), |
| @"buttonHorizontalPadding" : @(kButtonHorizontalPadding), |
| @"betweenButtonsPadding" : @(kBetweenButtonsPadding), |
| }; |
| NSArray* constraints = @[ |
| @"V:|-(verticalPadding)-[title]-(verticalPadding)-[accounts]", |
| @"V:[accounts]-(accountsVerticalPadding)-[info]", |
| @"V:[info]-(verticalPadding)-[primaryButton]-(buttonVerticalPadding)-|", |
| @"V:[info]-(verticalPadding)-[secondaryButton]-(buttonVerticalPadding)-|", |
| @"H:|-(horizontalPadding)-[title]-(horizontalPadding)-|", |
| @"H:|-(accountsHorizontalPadding)-[accounts]-(accountsHorizontalPadding)-|", |
| @"H:|-(horizontalPadding)-[info]-(horizontalPadding)-|", |
| @"H:[secondaryButton]-(betweenButtonsPadding)-[primaryButton]", |
| @"H:[primaryButton]-(buttonHorizontalPadding)-|", |
| ]; |
| |
| ApplyVisualConstraintsWithMetrics(constraints, views, metrics); |
| } |
| |
| - (void)viewWillAppear:(BOOL)animated { |
| [super viewWillAppear:animated]; |
| if ([self isBeingPresented] || [self isMovingToParentViewController]) { |
| gSignedInAccountsViewControllerIsShown = YES; |
| } |
| } |
| |
| - (void)viewWillDisappear:(BOOL)animated { |
| [super viewWillDisappear:animated]; |
| if ([self isBeingDismissed] || [self isMovingFromParentViewController]) { |
| gSignedInAccountsViewControllerIsShown = NO; |
| } |
| } |
| |
| #pragma mark Events |
| |
| - (void)onPrimaryButtonPressed:(id)sender { |
| [self dismiss]; |
| } |
| |
| - (void)onSecondaryButtonPressed:(id)sender { |
| [self dismiss]; |
| GenericChromeCommand* showAccountsSettingsCommand = |
| [[GenericChromeCommand alloc] initWithTag:IDC_SHOW_ACCOUNTS_SETTINGS]; |
| [self chromeExecuteCommand:showAccountsSettingsCommand]; |
| } |
| |
| #pragma mark OAuth2TokenServiceObserverBridgeDelegate |
| |
| - (void)onEndBatchChanges { |
| ProfileOAuth2TokenService* tokenService = |
| OAuth2TokenServiceFactory::GetForBrowserState(_browserState); |
| if (tokenService->GetAccounts().empty()) { |
| [self dismiss]; |
| return; |
| } |
| [_accountsCollection loadModel]; |
| [_accountsCollection.collectionView reloadData]; |
| } |
| |
| @end |