OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #include "ios/chrome/browser/ui/authentication/signin_confirmation_view_controll
er.h" |
| 6 |
| 7 #import "base/ios/weak_nsobject.h" |
| 8 #import "base/mac/foundation_util.h" |
| 9 #import "base/mac/scoped_nsobject.h" |
| 10 #include "base/metrics/user_metrics.h" |
| 11 #import "base/strings/sys_string_conversions.h" |
| 12 #include "components/google/core/browser/google_util.h" |
| 13 #include "ios/chrome/browser/application_context.h" |
| 14 #include "ios/chrome/browser/signin/chrome_identity_service_observer_bridge.h" |
| 15 #import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrom
e.h" |
| 16 #import "ios/chrome/browser/ui/collection_view/cells/collection_view_footer_item
.h" |
| 17 #import "ios/chrome/browser/ui/collection_view/collection_view_model.h" |
| 18 #import "ios/chrome/browser/ui/settings/cells/account_control_item.h" |
| 19 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 20 #import "ios/chrome/common/string_util.h" |
| 21 #include "ios/chrome/grit/ios_chromium_strings.h" |
| 22 #include "ios/chrome/grit/ios_strings.h" |
| 23 #include "ios/public/provider/chrome/browser/chrome_browser_provider.h" |
| 24 #include "ios/public/provider/chrome/browser/images/branded_image_provider.h" |
| 25 #import "ios/public/provider/chrome/browser/signin/chrome_identity.h" |
| 26 #import "ios/public/provider/chrome/browser/signin/chrome_identity_service.h" |
| 27 #include "ios/public/provider/chrome/browser/signin/signin_resources_provider.h" |
| 28 #import "ios/third_party/material_components_ios/src/components/AppBar/src/Mater
ialAppBar.h" |
| 29 #import "ios/third_party/material_components_ios/src/components/Palettes/src/Mat
erialPalettes.h" |
| 30 #import "ios/third_party/material_components_ios/src/components/Typography/src/M
aterialTypography.h" |
| 31 #import "ui/base/l10n/l10n_util.h" |
| 32 #include "url/gurl.h" |
| 33 |
| 34 namespace { |
| 35 const CGFloat kAccountImageDimension = 64.; |
| 36 const CGFloat kHeaderViewMinHeight = 170.; |
| 37 const CGFloat kHeaderViewHeightMultiplier = 0.33; |
| 38 const CGFloat kContentViewBottomInset = 40.; |
| 39 // Leading separator inset. |
| 40 const CGFloat kLeadingSeparatorInset = 30.; |
| 41 // Trailing separator inset. |
| 42 const CGFloat kTrailingSeparatorInset = 16.; |
| 43 |
| 44 UIImage* GetImageForIdentity(ChromeIdentity* identity) { |
| 45 UIImage* image = ios::GetChromeBrowserProvider() |
| 46 ->GetChromeIdentityService() |
| 47 ->GetCachedAvatarForIdentity(identity); |
| 48 if (!image) { |
| 49 image = ios::GetChromeBrowserProvider() |
| 50 ->GetSigninResourcesProvider() |
| 51 ->GetDefaultAvatar(); |
| 52 // No cached image, trigger a fetch, which will notify all observers |
| 53 // (including the corresponding AccountViewBase). |
| 54 ios::GetChromeBrowserProvider() |
| 55 ->GetChromeIdentityService() |
| 56 ->GetAvatarForIdentity(identity, ^(UIImage*){ |
| 57 }); |
| 58 } |
| 59 return image; |
| 60 } |
| 61 |
| 62 typedef NS_ENUM(NSInteger, SectionIdentifier) { |
| 63 SectionIdentifierInfo = kSectionIdentifierEnumZero, |
| 64 }; |
| 65 |
| 66 typedef NS_ENUM(NSInteger, ItemType) { |
| 67 ItemTypeSync = kItemTypeEnumZero, |
| 68 ItemTypeGoogleServices, |
| 69 ItemTypeFooter, |
| 70 }; |
| 71 } |
| 72 |
| 73 #pragma mark - SigninConfirmationViewController |
| 74 |
| 75 @interface SigninConfirmationViewController ()< |
| 76 ChromeIdentityServiceObserver, |
| 77 CollectionViewFooterLinkDelegate> { |
| 78 base::scoped_nsobject<ChromeIdentity> _identity; |
| 79 std::unique_ptr<ChromeIdentityServiceObserverBridge> _identityServiceObserver; |
| 80 base::WeakNSObject<UIImage> _oldImage; |
| 81 base::scoped_nsobject<UIImageView> _imageView; |
| 82 base::scoped_nsobject<UILabel> _titleLabel; |
| 83 base::scoped_nsobject<UILabel> _emailLabel; |
| 84 } |
| 85 @end |
| 86 |
| 87 @implementation SigninConfirmationViewController |
| 88 |
| 89 @synthesize delegate; |
| 90 |
| 91 - (instancetype)initWithIdentity:(ChromeIdentity*)identity { |
| 92 self = [super initWithStyle:CollectionViewControllerStyleAppBar]; |
| 93 if (self) { |
| 94 _identity.reset([identity retain]); |
| 95 _identityServiceObserver.reset( |
| 96 new ChromeIdentityServiceObserverBridge(self)); |
| 97 } |
| 98 return self; |
| 99 } |
| 100 |
| 101 - (void)scrollToBottom { |
| 102 CGPoint bottomOffset = CGPointMake( |
| 103 0, self.collectionView.contentSize.height - |
| 104 self.collectionView.bounds.size.height + kContentViewBottomInset); |
| 105 [self.collectionView setContentOffset:bottomOffset animated:YES]; |
| 106 } |
| 107 |
| 108 #pragma mark - UIViewController |
| 109 |
| 110 - (void)viewDidLoad { |
| 111 [super viewDidLoad]; |
| 112 |
| 113 // Configure the header. |
| 114 MDCFlexibleHeaderView* headerView = |
| 115 self.appBar.headerViewController.headerView; |
| 116 headerView.canOverExtend = YES; |
| 117 headerView.maximumHeight = 200; |
| 118 headerView.shiftBehavior = MDCFlexibleHeaderShiftBehaviorEnabled; |
| 119 headerView.backgroundColor = [UIColor whiteColor]; |
| 120 [headerView addSubview:[self contentViewWithFrame:headerView.bounds]]; |
| 121 self.appBar.navigationBar.hidesBackButton = YES; |
| 122 self.collectionView.backgroundColor = [UIColor clearColor]; |
| 123 [headerView changeContentInsets:^{ |
| 124 UIEdgeInsets contentInset = self.collectionView.contentInset; |
| 125 contentInset.bottom += kContentViewBottomInset; |
| 126 self.collectionView.contentInset = contentInset; |
| 127 }]; |
| 128 |
| 129 // Load the contents of the collection view. |
| 130 [self loadModel]; |
| 131 } |
| 132 |
| 133 - (void)viewDidAppear:(BOOL)animated { |
| 134 [super viewDidAppear:animated]; |
| 135 |
| 136 BOOL didSend = [self |
| 137 sendDidReachBottomIfNecessary:self.collectionView.collectionViewLayout |
| 138 .collectionViewContentSize.height]; |
| 139 if (!didSend && [self isMovingToParentViewController]) { |
| 140 // The confirmation screen just appeared and there wasn't enough space to |
| 141 // show the full screen (since the scroll hasn't reach the botton). This |
| 142 // means the "More" button is actually necessary. |
| 143 base::RecordAction(base::UserMetricsAction("Signin_MoreButton_Shown")); |
| 144 } |
| 145 } |
| 146 |
| 147 - (UIView*)contentViewWithFrame:(CGRect)frame { |
| 148 base::scoped_nsobject<UIView> contentView( |
| 149 [[UIView alloc] initWithFrame:frame]); |
| 150 contentView.get().autoresizingMask = |
| 151 (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); |
| 152 contentView.get().clipsToBounds = YES; |
| 153 _imageView.reset([[UIImageView alloc] init]); |
| 154 _imageView.get().translatesAutoresizingMaskIntoConstraints = NO; |
| 155 |
| 156 _titleLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]); |
| 157 _titleLabel.get().textColor = [[MDCPalette greyPalette] tint900]; |
| 158 _titleLabel.get().font = [MDCTypography headlineFont]; |
| 159 _titleLabel.get().translatesAutoresizingMaskIntoConstraints = NO; |
| 160 |
| 161 _emailLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]); |
| 162 _emailLabel.get().textColor = [[MDCPalette greyPalette] tint700]; |
| 163 _emailLabel.get().font = [MDCTypography body1Font]; |
| 164 _emailLabel.get().translatesAutoresizingMaskIntoConstraints = NO; |
| 165 |
| 166 [self updateViewWithIdentity:_identity]; |
| 167 |
| 168 base::scoped_nsobject<UIView> divider( |
| 169 [[UIView alloc] initWithFrame:CGRectZero]); |
| 170 divider.get().backgroundColor = [[MDCPalette greyPalette] tint300]; |
| 171 divider.get().translatesAutoresizingMaskIntoConstraints = NO; |
| 172 |
| 173 base::scoped_nsobject<UILayoutGuide> layoutGuide1( |
| 174 [[UILayoutGuide alloc] init]); |
| 175 base::scoped_nsobject<UILayoutGuide> layoutGuide2( |
| 176 [[UILayoutGuide alloc] init]); |
| 177 |
| 178 [contentView addSubview:_imageView]; |
| 179 [contentView addSubview:_titleLabel]; |
| 180 [contentView addSubview:_emailLabel]; |
| 181 [contentView addSubview:divider]; |
| 182 [contentView addLayoutGuide:layoutGuide1]; |
| 183 [contentView addLayoutGuide:layoutGuide2]; |
| 184 |
| 185 NSDictionary* views = @{ |
| 186 @"image" : _imageView, |
| 187 @"title" : _titleLabel, |
| 188 @"email" : _emailLabel, |
| 189 @"divider" : divider, |
| 190 @"v1" : layoutGuide1, |
| 191 @"v2" : layoutGuide2 |
| 192 }; |
| 193 NSArray* constraints = @[ |
| 194 @"V:[image]-(24)-[title]-(8)-[email]-(16)-[divider(==1)]|", |
| 195 @"H:|[v1][image]", |
| 196 @"H:|[v1(16)][title(<=440)][v2(>=v1)]|", |
| 197 @"H:|[v1][email]", |
| 198 @"H:|[divider]|", |
| 199 ]; |
| 200 ApplyVisualConstraints(constraints, views); |
| 201 return contentView.autorelease(); |
| 202 } |
| 203 |
| 204 - (void)viewWillLayoutSubviews { |
| 205 CGSize viewSize = self.view.bounds.size; |
| 206 MDCFlexibleHeaderView* headerView = |
| 207 self.appBar.headerViewController.headerView; |
| 208 headerView.maximumHeight = |
| 209 MAX(kHeaderViewMinHeight, kHeaderViewHeightMultiplier * viewSize.height); |
| 210 } |
| 211 |
| 212 - (void)updateViewWithIdentity:(ChromeIdentity*)identity { |
| 213 UIImage* identityImage = GetImageForIdentity(identity); |
| 214 if (_oldImage.get() != identityImage) { |
| 215 _oldImage.reset(identityImage); |
| 216 identityImage = |
| 217 ResizeImage(identityImage, |
| 218 CGSizeMake(kAccountImageDimension, kAccountImageDimension), |
| 219 ProjectionMode::kAspectFit); |
| 220 identityImage = |
| 221 CircularImageFromImage(identityImage, kAccountImageDimension); |
| 222 _imageView.get().image = identityImage; |
| 223 } |
| 224 |
| 225 _titleLabel.get().text = l10n_util::GetNSStringF( |
| 226 IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_TITLE, |
| 227 base::SysNSStringToUTF16([identity userFullName])); |
| 228 |
| 229 _emailLabel.get().text = [identity userEmail]; |
| 230 } |
| 231 |
| 232 #pragma mark - Model |
| 233 |
| 234 - (void)loadModel { |
| 235 [super loadModel]; |
| 236 CollectionViewModel* model = self.collectionViewModel; |
| 237 |
| 238 [model addSectionWithIdentifier:SectionIdentifierInfo]; |
| 239 [model addItem:[self syncItem] toSectionWithIdentifier:SectionIdentifierInfo]; |
| 240 [model addItem:[self googleServicesItem] |
| 241 toSectionWithIdentifier:SectionIdentifierInfo]; |
| 242 [model addItem:[self openSettingsItem] |
| 243 toSectionWithIdentifier:SectionIdentifierInfo]; |
| 244 } |
| 245 |
| 246 #pragma mark - Model items |
| 247 |
| 248 - (CollectionViewItem*)syncItem { |
| 249 AccountControlItem* item = |
| 250 [[[AccountControlItem alloc] initWithType:ItemTypeSync] autorelease]; |
| 251 item.text = l10n_util::GetNSString( |
| 252 IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_SYNC_TITLE); |
| 253 item.detailText = l10n_util::GetNSString( |
| 254 IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_SYNC_DESCRIPTION); |
| 255 item.image = ios::GetChromeBrowserProvider() |
| 256 ->GetBrandedImageProvider() |
| 257 ->GetSigninConfirmationSyncSettingsImage(); |
| 258 return item; |
| 259 } |
| 260 |
| 261 - (CollectionViewItem*)googleServicesItem { |
| 262 AccountControlItem* item = [[[AccountControlItem alloc] |
| 263 initWithType:ItemTypeGoogleServices] autorelease]; |
| 264 item.text = l10n_util::GetNSString( |
| 265 IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_SERVICES_TITLE); |
| 266 item.detailText = l10n_util::GetNSString( |
| 267 IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_SERVICES_DESCRIPTION); |
| 268 item.image = ios::GetChromeBrowserProvider() |
| 269 ->GetBrandedImageProvider() |
| 270 ->GetSigninConfirmationPersonalizeServicesImage(); |
| 271 return item; |
| 272 } |
| 273 |
| 274 - (CollectionViewItem*)openSettingsItem { |
| 275 CollectionViewFooterItem* item = [[[CollectionViewFooterItem alloc] |
| 276 initWithType:ItemTypeFooter] autorelease]; |
| 277 item.text = l10n_util::GetNSString( |
| 278 IDS_IOS_ACCOUNT_CONSISTENCY_CONFIRMATION_OPEN_SETTINGS); |
| 279 item.linkURL = google_util::AppendGoogleLocaleParam( |
| 280 GURL("internal://settings-sync"), |
| 281 GetApplicationContext()->GetApplicationLocale()); |
| 282 item.linkDelegate = self; |
| 283 return item; |
| 284 } |
| 285 |
| 286 #pragma mark - Helpers |
| 287 |
| 288 // Calls |signinConfirmationControllerDidReachBottom:| on |delegate| if |
| 289 // the collection view has reached its bottom based on a content size height of |
| 290 // |contentSizeHeight|. Returns whether the delegate was notified. |
| 291 - (BOOL)sendDidReachBottomIfNecessary:(CGFloat)contentSizeHeight { |
| 292 if (contentSizeHeight && |
| 293 self.collectionView.contentOffset.y + |
| 294 self.collectionView.frame.size.height >= |
| 295 contentSizeHeight) { |
| 296 [self.delegate signinConfirmationControllerDidReachBottom:self]; |
| 297 return YES; |
| 298 } |
| 299 return NO; |
| 300 } |
| 301 |
| 302 #pragma mark - ChromeIdentityServiceObserver |
| 303 |
| 304 - (void)onProfileUpdate:(ChromeIdentity*)identity { |
| 305 if (identity == _identity) { |
| 306 [self updateViewWithIdentity:identity]; |
| 307 } |
| 308 } |
| 309 |
| 310 - (void)onChromeIdentityServiceWillBeDestroyed { |
| 311 _identityServiceObserver.reset(); |
| 312 } |
| 313 |
| 314 #pragma mark - UIScrollViewDelegate |
| 315 |
| 316 - (void)scrollViewDidScroll:(UIScrollView*)scrollView { |
| 317 [super scrollViewDidScroll:scrollView]; |
| 318 |
| 319 if (scrollView == self.collectionView) { |
| 320 [self sendDidReachBottomIfNecessary:scrollView.contentSize.height]; |
| 321 } |
| 322 } |
| 323 |
| 324 #pragma mark UICollectionViewDataSource |
| 325 |
| 326 - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView |
| 327 cellForItemAtIndexPath:(NSIndexPath*)indexPath { |
| 328 MDCCollectionViewCell* cell = |
| 329 [super collectionView:collectionView cellForItemAtIndexPath:indexPath]; |
| 330 |
| 331 NSInteger itemType = |
| 332 [self.collectionViewModel itemTypeForIndexPath:indexPath]; |
| 333 switch (itemType) { |
| 334 case ItemTypeSync: |
| 335 cell.separatorInset = UIEdgeInsetsMake(0, kLeadingSeparatorInset, 0, |
| 336 kTrailingSeparatorInset); |
| 337 break; |
| 338 case ItemTypeGoogleServices: |
| 339 cell.shouldHideSeparator = YES; |
| 340 break; |
| 341 case ItemTypeFooter: { |
| 342 CollectionViewFooterCell* footerCell = |
| 343 base::mac::ObjCCastStrict<CollectionViewFooterCell>(cell); |
| 344 // TODO(crbug.com/664648): Must use atomic text formatting operation due |
| 345 // to LabelLinkController bug. |
| 346 footerCell.textLabel.attributedText = [[[NSAttributedString alloc] |
| 347 initWithString:footerCell.textLabel.text |
| 348 attributes:@{ |
| 349 NSFontAttributeName : [MDCTypography body1Font], |
| 350 NSForegroundColorAttributeName : |
| 351 [[MDCPalette greyPalette] tint700] |
| 352 }] autorelease]; |
| 353 footerCell.horizontalPadding = 16; |
| 354 break; |
| 355 } |
| 356 |
| 357 default: |
| 358 break; |
| 359 } |
| 360 return cell; |
| 361 } |
| 362 |
| 363 #pragma mark - MDCCollectionViewStylingDelegate |
| 364 |
| 365 - (CGFloat)collectionView:(UICollectionView*)collectionView |
| 366 cellHeightAtIndexPath:(NSIndexPath*)indexPath { |
| 367 CollectionViewItem* item = |
| 368 [self.collectionViewModel itemAtIndexPath:indexPath]; |
| 369 |
| 370 // ItemTypeSync, ItemTypeGoogleServices, and ItemTypeFooter all support |
| 371 // dynamic height calculation. |
| 372 return [MDCCollectionViewCell |
| 373 cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds) |
| 374 forItem:item]; |
| 375 } |
| 376 |
| 377 - (BOOL)collectionView:(UICollectionView*)collectionView |
| 378 hidesInkViewAtIndexPath:(NSIndexPath*)indexPath { |
| 379 return YES; |
| 380 } |
| 381 |
| 382 - (BOOL)collectionView:(nonnull UICollectionView*)collectionView |
| 383 shouldHideItemBackgroundAtIndexPath:(nonnull NSIndexPath*)indexPath { |
| 384 return YES; |
| 385 } |
| 386 |
| 387 #pragma mark - CollectionViewFooterLinkDelegate |
| 388 |
| 389 - (void)cell:(CollectionViewFooterCell*)cell didTapLinkURL:(GURL)URL { |
| 390 [[self delegate] signinConfirmationControllerDidTapSettingsLink:self]; |
| 391 } |
| 392 |
| 393 @end |
OLD | NEW |