blob: 6df3d5073fe5714906f82284f188ba6df60cd916 [file] [log] [blame]
// Copyright 2015 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/settings/sync_encryption_passphrase_collection_view_controller.h"
#include <memory>
#include "base/i18n/time_formatting.h"
#include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "components/browser_sync/profile_sync_service.h"
#include "components/google/core/browser/google_util.h"
#include "components/signin/core/browser/profile_oauth2_token_service.h"
#import "components/signin/ios/browser/oauth2_token_service_observer_bridge.h"
#include "components/strings/grit/components_strings.h"
#include "ios/chrome/browser/application_context.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/chrome_url_constants.h"
#import "ios/chrome/browser/signin/authentication_service.h"
#include "ios/chrome/browser/signin/authentication_service_factory.h"
#include "ios/chrome/browser/signin/profile_oauth2_token_service_factory.h"
#include "ios/chrome/browser/sync/profile_sync_service_factory.h"
#include "ios/chrome/browser/sync/sync_setup_service.h"
#include "ios/chrome/browser/sync/sync_setup_service_factory.h"
#import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrome.h"
#import "ios/chrome/browser/ui/collection_view/cells/collection_view_footer_item.h"
#import "ios/chrome/browser/ui/collection_view/cells/collection_view_item.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/settings/cells/byo_textfield_item.h"
#import "ios/chrome/browser/ui/settings/cells/card_multiline_item.h"
#import "ios/chrome/browser/ui/settings/cells/passphrase_error_item.h"
#import "ios/chrome/browser/ui/settings/settings_navigation_controller.h"
#import "ios/chrome/browser/ui/settings/settings_utils.h"
#import "ios/chrome/browser/ui/settings/sync_utils/sync_util.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/signin/chrome_identity.h"
#import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using namespace sync_encryption_passphrase;
namespace {
const CGFloat kSpinnerButtonCustomViewSize = 48;
const CGFloat kSpinnerButtonPadding = 18;
} // namespace
@interface SyncEncryptionPassphraseCollectionViewController ()<
OAuth2TokenServiceObserverBridgeDelegate,
SettingsControllerProtocol> {
ios::ChromeBrowserState* browserState_;
// Whether the decryption progress is currently being shown.
BOOL isDecryptionProgressShown_;
NSString* savedTitle_;
UIBarButtonItem* savedLeftButton_;
std::unique_ptr<SyncObserverBridge> syncObserver_;
std::unique_ptr<OAuth2TokenServiceObserverBridge> tokenServiceObserver_;
UITextField* passphrase_;
}
// Sets up the navigation bar's right button. The button will be enabled iff
// |-areAllFieldsFilled| returns YES.
- (void)setRightNavBarItem;
// Returns a passphrase message item.
- (CollectionViewItem*)passphraseMessageItem;
// Returns a passphrase item.
- (CollectionViewItem*)passphraseItem;
// Returns a passphrase error item having |errorMessage| as title.
- (CollectionViewItem*)passphraseErrorItemWithMessage:(NSString*)errorMessage;
// Shows the UI to indicate the decryption is being attempted.
- (void)showDecryptionProgress;
// Hides the UI to indicate decryption is in process.
- (void)hideDecryptionProgress;
// Returns a transparent content view object to be used as a footer, or nil
// for no footer.
- (CollectionViewItem*)footerItem;
// Creates a new UIBarButtonItem with a spinner.
- (UIBarButtonItem*)spinnerButton;
@end
@implementation SyncEncryptionPassphraseCollectionViewController
@synthesize headerMessage = headerMessage_;
@synthesize footerMessage = footerMessage_;
@synthesize processingMessage = processingMessage_;
@synthesize syncErrorMessage = syncErrorMessage_;
- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState {
DCHECK(browserState);
UICollectionViewLayout* layout = [[MDCCollectionViewFlowLayout alloc] init];
self =
[super initWithLayout:layout style:CollectionViewControllerStyleAppBar];
if (self) {
self.title = l10n_util::GetNSString(IDS_IOS_SYNC_ENTER_PASSPHRASE_TITLE);
self.shouldHideDoneButton = YES;
browserState_ = browserState;
NSString* userEmail =
AuthenticationServiceFactory::GetForBrowserState(browserState_)
->GetAuthenticatedUserEmail();
DCHECK(userEmail);
browser_sync::ProfileSyncService* service =
ProfileSyncServiceFactory::GetForBrowserState(browserState_);
if (service->IsEngineInitialized() &&
service->IsUsingSecondaryPassphrase()) {
base::Time passphrase_time = service->GetExplicitPassphraseTime();
if (!passphrase_time.is_null()) {
base::string16 passphrase_time_str =
base::TimeFormatShortDate(passphrase_time);
self.headerMessage = l10n_util::GetNSStringF(
IDS_IOS_SYNC_ENTER_PASSPHRASE_BODY_WITH_EMAIL_AND_DATE,
base::SysNSStringToUTF16(userEmail), passphrase_time_str);
} else {
self.headerMessage = l10n_util::GetNSStringF(
IDS_IOS_SYNC_ENTER_PASSPHRASE_BODY_WITH_EMAIL,
base::SysNSStringToUTF16(userEmail));
}
} else {
self.headerMessage =
l10n_util::GetNSString(IDS_SYNC_ENTER_GOOGLE_PASSPHRASE_BODY);
}
self.processingMessage = l10n_util::GetNSString(IDS_SYNC_LOGIN_SETTING_UP);
footerMessage_ = l10n_util::GetNSString(IDS_IOS_SYNC_PASSPHRASE_RECOVER);
tokenServiceObserver_.reset(new OAuth2TokenServiceObserverBridge(
ProfileOAuth2TokenServiceFactory::GetForBrowserState(browserState_),
self));
// TODO(crbug.com/764578): -loadModel should not be called from
// initializer. A possible fix is to move this call to -viewDidLoad.
[self loadModel];
}
return self;
}
- (UITextField*)passphrase {
return passphrase_;
}
- (NSString*)syncErrorMessage {
if (syncErrorMessage_)
return syncErrorMessage_;
SyncSetupService* service =
SyncSetupServiceFactory::GetForBrowserState(browserState_);
DCHECK(service);
SyncSetupService::SyncServiceState syncServiceState =
service->GetSyncServiceState();
// Passphrase error directly set |syncErrorMessage_|.
if (syncServiceState == SyncSetupService::kSyncServiceNeedsPassphrase)
return nil;
return GetSyncErrorMessageForBrowserState(browserState_);
}
#pragma mark - View lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
[self setRightNavBarItem];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
if (![self isViewLoaded]) {
passphrase_ = nil;
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.passphrase resignFirstResponder];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
if ([self isMovingFromParentViewController]) {
[self unregisterTextField:self.passphrase];
}
}
#pragma mark - SettingsRootCollectionViewController
- (void)loadModel {
[super loadModel];
CollectionViewModel* model = self.collectionViewModel;
[model addSectionWithIdentifier:SectionIdentifierPassphrase];
if (self.headerMessage) {
[model addItem:[self passphraseMessageItem]
toSectionWithIdentifier:SectionIdentifierPassphrase];
}
[model addItem:[self passphraseItem]
toSectionWithIdentifier:SectionIdentifierPassphrase];
NSString* errorMessage = [self syncErrorMessage];
if (errorMessage) {
[model addItem:[self passphraseErrorItemWithMessage:errorMessage]
toSectionWithIdentifier:SectionIdentifierPassphrase];
}
// TODO(crbug.com/650424): Footer items must currently go into a separate
// section, to work around a drawing bug in MDC.
[model addSectionWithIdentifier:SectionIdentifierFooter];
[model addItem:[self footerItem]
toSectionWithIdentifier:SectionIdentifierFooter];
}
#pragma mark - Items
- (CollectionViewItem*)passphraseMessageItem {
CardMultilineItem* item =
[[CardMultilineItem alloc] initWithType:ItemTypeMessage];
item.text = headerMessage_;
return item;
}
- (CollectionViewItem*)passphraseItem {
if (passphrase_) {
[self unregisterTextField:passphrase_];
}
passphrase_ = [[UITextField alloc] init];
[passphrase_ setFont:[MDCTypography body1Font]];
[passphrase_ setSecureTextEntry:YES];
[passphrase_ setBackgroundColor:[UIColor clearColor]];
[passphrase_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[passphrase_ setAutocorrectionType:UITextAutocorrectionTypeNo];
[passphrase_
setPlaceholder:l10n_util::GetNSString(IDS_SYNC_PASSPHRASE_LABEL)];
[self registerTextField:passphrase_];
BYOTextFieldItem* item =
[[BYOTextFieldItem alloc] initWithType:ItemTypeEnterPassphrase];
item.textField = passphrase_;
return item;
}
- (CollectionViewItem*)passphraseErrorItemWithMessage:(NSString*)errorMessage {
PassphraseErrorItem* item =
[[PassphraseErrorItem alloc] initWithType:ItemTypeError];
item.text = errorMessage;
return item;
}
- (CollectionViewItem*)footerItem {
CollectionViewFooterItem* footerItem =
[[CollectionViewFooterItem alloc] initWithType:ItemTypeFooter];
footerItem.cellStyle = CollectionViewCellStyle::kUIKit;
footerItem.text = self.footerMessage;
footerItem.linkURL = google_util::AppendGoogleLocaleParam(
GURL(kSyncGoogleDashboardURL),
GetApplicationContext()->GetApplicationLocale());
footerItem.linkDelegate = self;
return footerItem;
}
#pragma mark - MDCCollectionViewStylingDelegate
- (MDCCollectionViewCellStyle)collectionView:(UICollectionView*)collectionView
cellStyleForSection:(NSInteger)section {
NSInteger sectionIdentifier =
[self.collectionViewModel sectionIdentifierForSection:section];
switch (sectionIdentifier) {
case SectionIdentifierFooter:
// Display the Learn More footer in the default style with no "card" UI
// and no section padding.
return MDCCollectionViewCellStyleDefault;
default:
return self.styler.cellStyle;
}
}
- (BOOL)collectionView:(UICollectionView*)collectionView
shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath {
NSInteger sectionIdentifier =
[self.collectionViewModel sectionIdentifierForSection:indexPath.section];
switch (sectionIdentifier) {
case SectionIdentifierFooter:
// Display the Learn More footer without any background image or
// shadowing.
return YES;
default:
return NO;
}
}
- (CGFloat)collectionView:(UICollectionView*)collectionView
cellHeightAtIndexPath:(NSIndexPath*)indexPath {
CollectionViewItem* item =
[self.collectionViewModel itemAtIndexPath:indexPath];
if (item.type == ItemTypeMessage || item.type == ItemTypeFooter) {
return [MDCCollectionViewCell
cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds)
forItem:item];
}
return MDCCellDefaultOneLineHeight;
}
#pragma mark - UICollectionViewDataSource
- (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView
cellForItemAtIndexPath:(NSIndexPath*)indexPath {
UICollectionViewCell* cell =
[super collectionView:collectionView cellForItemAtIndexPath:indexPath];
CollectionViewItem* item =
[self.collectionViewModel itemAtIndexPath:indexPath];
if (item.type == ItemTypeMessage) {
CardMultilineCell* messageCell =
base::mac::ObjCCastStrict<CardMultilineCell>(cell);
messageCell.textLabel.font =
[[MDCTypography fontLoader] mediumFontOfSize:14];
}
return cell;
}
#pragma mark - UICollectionViewDelegate
- (void)collectionView:(UICollectionView*)collectionView
didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
[super collectionView:collectionView didSelectItemAtIndexPath:indexPath];
NSInteger itemType =
[self.collectionViewModel itemTypeForIndexPath:indexPath];
if (itemType == ItemTypeEnterPassphrase) {
[passphrase_ becomeFirstResponder];
}
}
#pragma mark - Behavior
- (BOOL)forDecryption {
return YES;
}
- (void)signInPressed {
DCHECK([passphrase_ text].length);
if (!syncObserver_.get()) {
syncObserver_.reset(new SyncObserverBridge(
self, ProfileSyncServiceFactory::GetForBrowserState(browserState_)));
}
// Clear out the error message.
self.syncErrorMessage = nil;
browser_sync::ProfileSyncService* service =
ProfileSyncServiceFactory::GetForBrowserState(browserState_);
DCHECK(service);
// It is possible for a race condition to happen where a user is allowed
// to call the backend with the passphrase before the backend is
// initialized.
// See crbug/276714. As a temporary measure, ignore the tap on sign-in
// button. A better fix may be to disable the rightBarButtonItem (submit)
// until backend is initialized.
if (!service->IsEngineInitialized())
return;
[self showDecryptionProgress];
std::string passphrase = base::SysNSStringToUTF8([passphrase_ text]);
if ([self forDecryption]) {
if (!service->SetDecryptionPassphrase(passphrase)) {
syncObserver_.reset();
[self clearFieldsOnError:l10n_util::GetNSString(
IDS_IOS_SYNC_INCORRECT_PASSPHRASE)];
[self hideDecryptionProgress];
}
} else {
service->EnableEncryptEverything();
service->SetEncryptionPassphrase(
passphrase, browser_sync::ProfileSyncService::EXPLICIT);
}
[self reloadData];
}
- (void)setRightNavBarItem {
UIBarButtonItem* submitButtonItem = self.navigationItem.rightBarButtonItem;
if (!submitButtonItem) {
submitButtonItem = [[UIBarButtonItem alloc]
initWithTitle:l10n_util::GetNSString(IDS_IOS_SYNC_DECRYPT_BUTTON)
style:UIBarButtonItemStylePlain
target:self
action:@selector(signInPressed)];
}
submitButtonItem.enabled = [self areAllFieldsFilled];
// Only setting the enabled state doesn't make the item redraw. As a
// workaround, set it again.
self.navigationItem.rightBarButtonItem = submitButtonItem;
}
- (BOOL)areAllFieldsFilled {
return [self.passphrase text].length > 0;
}
- (void)clearFieldsOnError:(NSString*)errorMessage {
self.syncErrorMessage = errorMessage;
[self.passphrase setText:@""];
}
- (void)showDecryptionProgress {
if (isDecryptionProgressShown_)
return;
isDecryptionProgressShown_ = YES;
// Hide the button.
self.navigationItem.rightBarButtonItem = nil;
// Custom title view with spinner.
DCHECK(!savedTitle_);
DCHECK(!savedLeftButton_);
savedLeftButton_ = self.navigationItem.leftBarButtonItem;
self.navigationItem.leftBarButtonItem = [self spinnerButton];
savedTitle_ = [self.title copy];
self.title = processingMessage_;
}
- (void)hideDecryptionProgress {
if (!isDecryptionProgressShown_)
return;
isDecryptionProgressShown_ = NO;
self.navigationItem.leftBarButtonItem = savedLeftButton_;
savedLeftButton_ = nil;
self.title = savedTitle_;
savedTitle_ = nil;
[self setRightNavBarItem];
}
- (void)registerTextField:(UITextField*)textField {
[textField addTarget:self
action:@selector(textFieldDidBeginEditing:)
forControlEvents:UIControlEventEditingDidBegin];
[textField addTarget:self
action:@selector(textFieldDidChange:)
forControlEvents:UIControlEventEditingChanged];
[textField addTarget:self
action:@selector(textFieldDidEndEditing:)
forControlEvents:UIControlEventEditingDidEndOnExit];
}
- (void)unregisterTextField:(UITextField*)textField {
[textField removeTarget:self
action:@selector(textFieldDidBeginEditing:)
forControlEvents:UIControlEventEditingDidBegin];
[textField removeTarget:self
action:@selector(textFieldDidChange:)
forControlEvents:UIControlEventEditingChanged];
[textField removeTarget:self
action:@selector(textFieldDidEndEditing:)
forControlEvents:UIControlEventEditingDidEndOnExit];
}
- (UIBarButtonItem*)spinnerButton {
CGRect customViewFrame = CGRectMake(0, 0, kSpinnerButtonCustomViewSize,
kSpinnerButtonCustomViewSize);
UIView* customView = [[UIView alloc] initWithFrame:customViewFrame];
UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
CGRect spinnerFrame = [spinner bounds];
spinnerFrame.origin.x = kSpinnerButtonPadding;
spinnerFrame.origin.y = kSpinnerButtonPadding;
[spinner setFrame:spinnerFrame];
[customView addSubview:spinner];
UIBarButtonItem* leftBarButtonItem =
[[UIBarButtonItem alloc] initWithCustomView:customView];
[spinner setHidesWhenStopped:NO];
[spinner startAnimating];
return leftBarButtonItem;
}
- (void)stopObserving {
// Stops observing the sync service. This is required during the shutdown
// phase to avoid observing sync events for a browser state that is being
// killed.
syncObserver_.reset();
tokenServiceObserver_.reset();
}
#pragma mark - UIControl events listener
- (void)textFieldDidBeginEditing:(id)sender {
// Remove the error cell if there is one.
CollectionViewModel* model = self.collectionViewModel;
NSInteger section =
[model sectionForSectionIdentifier:SectionIdentifierPassphrase];
NSIndexPath* errorIndexPath =
[NSIndexPath indexPathForItem:ItemTypeError inSection:section];
if ([model hasItemAtIndexPath:errorIndexPath] &&
[model itemTypeForIndexPath:errorIndexPath] == ItemTypeError) {
DCHECK(self.syncErrorMessage);
[model removeItemWithType:ItemTypeError
fromSectionWithIdentifier:SectionIdentifierPassphrase];
[self.collectionView deleteItemsAtIndexPaths:@[ errorIndexPath ]];
self.syncErrorMessage = nil;
}
}
- (void)textFieldDidChange:(id)sender {
[self setRightNavBarItem];
}
- (void)textFieldDidEndEditing:(id)sender {
if (sender == self.passphrase) {
if ([self areAllFieldsFilled]) {
[self signInPressed];
} else {
[self clearFieldsOnError:l10n_util::GetNSString(
IDS_SYNC_EMPTY_PASSPHRASE_ERROR)];
[self reloadData];
}
}
}
#pragma mark - SyncObserverModelBridge
- (void)onSyncStateChanged {
browser_sync::ProfileSyncService* service =
ProfileSyncServiceFactory::GetForBrowserState(browserState_);
if (!service->IsEngineInitialized()) {
return;
}
// Checking if the operation succeeded.
if (!service->IsPassphraseRequired() &&
(service->IsUsingSecondaryPassphrase() || [self forDecryption])) {
syncObserver_.reset();
[base::mac::ObjCCastStrict<SettingsNavigationController>(
self.navigationController)
popViewControllerOrCloseSettingsAnimated:YES];
return;
}
// Handling passphrase error case.
if (service->IsPassphraseRequired()) {
self.syncErrorMessage =
l10n_util::GetNSString(IDS_IOS_SYNC_INCORRECT_PASSPHRASE);
}
[self hideDecryptionProgress];
[self reloadData];
}
#pragma mark - OAuth2TokenServiceObserverBridgeDelegate
- (void)onEndBatchChanges {
if (AuthenticationServiceFactory::GetForBrowserState(browserState_)
->IsAuthenticated()) {
return;
}
[base::mac::ObjCCastStrict<SettingsNavigationController>(
self.navigationController) popViewControllerOrCloseSettingsAnimated:NO];
}
#pragma mark - SettingsControllerProtocol callbacks
- (void)settingsWillBeDismissed {
[self stopObserving];
}
@end