Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(167)

Side by Side Diff: ios/chrome/browser/ui/settings/sync_encryption_passphrase_collection_view_controller.mm

Issue 2587023002: Upstream Chrome on iOS source code [8/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 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 #import "ios/chrome/browser/ui/settings/sync_encryption_passphrase_collection_vi ew_controller.h"
6
7 #include <memory>
8
9 #include "base/i18n/time_formatting.h"
10 #include "base/mac/foundation_util.h"
11 #include "base/mac/objc_property_releaser.h"
12 #include "base/mac/scoped_nsobject.h"
13 #include "base/strings/sys_string_conversions.h"
14 #include "components/browser_sync/profile_sync_service.h"
15 #include "components/google/core/browser/google_util.h"
16 #include "components/signin/core/browser/profile_oauth2_token_service.h"
17 #include "components/signin/ios/browser/oauth2_token_service_observer_bridge.h"
18 #include "components/strings/grit/components_strings.h"
19 #include "ios/chrome/browser/application_context.h"
20 #include "ios/chrome/browser/browser_state/chrome_browser_state.h"
21 #include "ios/chrome/browser/chrome_url_constants.h"
22 #import "ios/chrome/browser/signin/authentication_service.h"
23 #import "ios/chrome/browser/signin/authentication_service_factory.h"
24 #include "ios/chrome/browser/signin/oauth2_token_service_factory.h"
25 #include "ios/chrome/browser/sync/ios_chrome_profile_sync_service_factory.h"
26 #include "ios/chrome/browser/sync/sync_setup_service.h"
27 #include "ios/chrome/browser/sync/sync_setup_service_factory.h"
28 #import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrom e.h"
29 #import "ios/chrome/browser/ui/collection_view/cells/collection_view_footer_item .h"
30 #import "ios/chrome/browser/ui/collection_view/cells/collection_view_item.h"
31 #import "ios/chrome/browser/ui/collection_view/collection_view_model.h"
32 #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h"
33 #import "ios/chrome/browser/ui/settings/cells/byo_textfield_item.h"
34 #import "ios/chrome/browser/ui/settings/cells/card_multiline_item.h"
35 #import "ios/chrome/browser/ui/settings/cells/passphrase_error_item.h"
36 #import "ios/chrome/browser/ui/settings/settings_navigation_controller.h"
37 #import "ios/chrome/browser/ui/settings/settings_utils.h"
38 #import "ios/chrome/browser/ui/sync/sync_util.h"
39 #import "ios/chrome/browser/ui/uikit_ui_util.h"
40 #include "ios/chrome/grit/ios_strings.h"
41 #import "ios/public/provider/chrome/browser/signin/chrome_identity.h"
42 #import "ios/third_party/material_components_ios/src/components/Typography/src/M aterialTypography.h"
43 #import "ios/third_party/material_roboto_font_loader_ios/src/src/MaterialRobotoF ontLoader.h"
44 #include "ui/base/l10n/l10n_util.h"
45 #include "ui/base/l10n/l10n_util_mac.h"
46 #include "url/gurl.h"
47
48 using namespace ios_internal::sync_encryption_passphrase;
49
50 namespace {
51
52 const CGFloat kSpinnerButtonCustomViewSize = 48;
53 const CGFloat kSpinnerButtonPadding = 18;
54
55 } // namespace
56
57 @interface SyncEncryptionPassphraseCollectionViewController ()<
58 OAuth2TokenServiceObserverBridgeDelegate,
59 SettingsControllerProtocol> {
60 ios::ChromeBrowserState* browserState_;
61 // Whether the decryption progress is currently being shown.
62 BOOL isDecryptionProgressShown_;
63 base::scoped_nsobject<NSString> savedTitle_;
64 base::scoped_nsobject<UIBarButtonItem> savedLeftButton_;
65 std::unique_ptr<SyncObserverBridge> syncObserver_;
66 std::unique_ptr<OAuth2TokenServiceObserverBridge> tokenServiceObserver_;
67 base::scoped_nsobject<UITextField> passphrase_;
68 base::mac::ObjCPropertyReleaser
69 propertyReleaser_SyncEncryptionPassphraseCollectionViewController_;
70 }
71
72 // Sets up the navigation bar's right button. The button will be enabled iff
73 // |-areAllFieldsFilled| returns YES.
74 - (void)setRightNavBarItem;
75
76 // Returns a passphrase message item.
77 - (CollectionViewItem*)passphraseMessageItem;
78
79 // Returns a passphrase item.
80 - (CollectionViewItem*)passphraseItem;
81
82 // Returns a passphrase error item having |errorMessage| as title.
83 - (CollectionViewItem*)passphraseErrorItemWithMessage:(NSString*)errorMessage;
84
85 // Shows the UI to indicate the decryption is being attempted.
86 - (void)showDecryptionProgress;
87
88 // Hides the UI to indicate decryption is in process.
89 - (void)hideDecryptionProgress;
90
91 // Returns a transparent content view object to be used as a footer, or nil
92 // for no footer.
93 - (CollectionViewItem*)footerItem;
94
95 // Creates a new UIBarButtonItem with a spinner.
96 - (UIBarButtonItem*)spinnerButton;
97
98 @end
99
100 @implementation SyncEncryptionPassphraseCollectionViewController
101
102 @synthesize headerMessage = headerMessage_;
103 @synthesize footerMessage = footerMessage_;
104 @synthesize processingMessage = processingMessage_;
105 @synthesize syncErrorMessage = syncErrorMessage_;
106
107 - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState {
108 DCHECK(browserState);
109 self = [super initWithStyle:CollectionViewControllerStyleAppBar];
110 if (self) {
111 self.title = l10n_util::GetNSString(IDS_IOS_SYNC_ENTER_PASSPHRASE_TITLE);
112 self.shouldHideDoneButton = YES;
113 browserState_ = browserState;
114 NSString* userEmail =
115 AuthenticationServiceFactory::GetForBrowserState(browserState_)
116 ->GetAuthenticatedUserEmail();
117 DCHECK(userEmail);
118 browser_sync::ProfileSyncService* service =
119 IOSChromeProfileSyncServiceFactory::GetForBrowserState(browserState_);
120 if (service->IsEngineInitialized() &&
121 service->IsUsingSecondaryPassphrase()) {
122 base::Time passphrase_time = service->GetExplicitPassphraseTime();
123 if (!passphrase_time.is_null()) {
124 base::string16 passphrase_time_str =
125 base::TimeFormatShortDate(passphrase_time);
126 self.headerMessage = l10n_util::GetNSStringF(
127 IDS_IOS_SYNC_ENTER_PASSPHRASE_BODY_WITH_EMAIL_AND_DATE,
128 base::SysNSStringToUTF16(userEmail), passphrase_time_str);
129 } else {
130 self.headerMessage = l10n_util::GetNSStringF(
131 IDS_IOS_SYNC_ENTER_PASSPHRASE_BODY_WITH_EMAIL,
132 base::SysNSStringToUTF16(userEmail));
133 }
134 } else {
135 self.headerMessage =
136 l10n_util::GetNSString(IDS_SYNC_ENTER_GOOGLE_PASSPHRASE_BODY);
137 }
138 self.processingMessage = l10n_util::GetNSString(IDS_SYNC_LOGIN_SETTING_UP);
139 footerMessage_ =
140 [l10n_util::GetNSString(IDS_IOS_SYNC_PASSPHRASE_RECOVER) retain];
141
142 tokenServiceObserver_.reset(new OAuth2TokenServiceObserverBridge(
143 OAuth2TokenServiceFactory::GetForBrowserState(browserState_), self));
144
145 propertyReleaser_SyncEncryptionPassphraseCollectionViewController_.Init(
146 self, [SyncEncryptionPassphraseCollectionViewController class]);
147
148 [self loadModel];
149 }
150 return self;
151 }
152
153 - (UITextField*)passphrase {
154 return passphrase_;
155 }
156
157 - (NSString*)syncErrorMessage {
158 if (syncErrorMessage_)
159 return syncErrorMessage_;
160 SyncSetupService* service =
161 SyncSetupServiceFactory::GetForBrowserState(browserState_);
162 DCHECK(service);
163 SyncSetupService::SyncServiceState syncServiceState =
164 service->GetSyncServiceState();
165
166 // Passphrase error directly set |syncErrorMessage_|.
167 if (syncServiceState == SyncSetupService::kSyncServiceNeedsPassphrase)
168 return nil;
169
170 return ios_internal::sync::GetSyncErrorMessageForBrowserState(browserState_);
171 }
172
173 #pragma mark - View lifecycle
174
175 - (void)viewDidLoad {
176 [super viewDidLoad];
177 [self setRightNavBarItem];
178 }
179
180 - (void)didReceiveMemoryWarning {
181 [super didReceiveMemoryWarning];
182 if (![self isViewLoaded]) {
183 passphrase_.reset();
184 }
185 }
186
187 - (void)viewWillDisappear:(BOOL)animated {
188 [super viewWillDisappear:animated];
189 [self.passphrase resignFirstResponder];
190 }
191
192 - (void)viewDidDisappear:(BOOL)animated {
193 [super viewDidDisappear:animated];
194 if ([self isMovingFromParentViewController]) {
195 [self unregisterTextField:self.passphrase];
196 }
197 }
198
199 #pragma mark - SettingsRootCollectionViewController
200
201 - (void)loadModel {
202 [super loadModel];
203 CollectionViewModel* model = self.collectionViewModel;
204
205 [model addSectionWithIdentifier:SectionIdentifierPassphrase];
206 if (self.headerMessage) {
207 [model addItem:[self passphraseMessageItem]
208 toSectionWithIdentifier:SectionIdentifierPassphrase];
209 }
210 [model addItem:[self passphraseItem]
211 toSectionWithIdentifier:SectionIdentifierPassphrase];
212
213 NSString* errorMessage = [self syncErrorMessage];
214 if (errorMessage) {
215 [model addItem:[self passphraseErrorItemWithMessage:errorMessage]
216 toSectionWithIdentifier:SectionIdentifierPassphrase];
217 }
218 // TODO(crbug.com/650424): Footer items must currently go into a separate
219 // section, to work around a drawing bug in MDC.
220 [model addSectionWithIdentifier:SectionIdentifierFooter];
221 [model addItem:[self footerItem]
222 toSectionWithIdentifier:SectionIdentifierFooter];
223 }
224
225 #pragma mark - Items
226
227 - (CollectionViewItem*)passphraseMessageItem {
228 CardMultilineItem* item =
229 [[[CardMultilineItem alloc] initWithType:ItemTypeMessage] autorelease];
230 item.text = headerMessage_;
231 return item;
232 }
233
234 - (CollectionViewItem*)passphraseItem {
235 if (passphrase_) {
236 [self unregisterTextField:passphrase_];
237 }
238 passphrase_.reset([[UITextField alloc] init]);
239 [passphrase_ setFont:[MDCTypography body1Font]];
240 [passphrase_ setSecureTextEntry:YES];
241 [passphrase_ setBackgroundColor:[UIColor clearColor]];
242 [passphrase_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
243 [passphrase_ setAutocorrectionType:UITextAutocorrectionTypeNo];
244 [passphrase_
245 setPlaceholder:l10n_util::GetNSString(IDS_SYNC_PASSPHRASE_LABEL)];
246 [self registerTextField:passphrase_];
247
248 BYOTextFieldItem* item = [[[BYOTextFieldItem alloc]
249 initWithType:ItemTypeEnterPassphrase] autorelease];
250 item.textField = passphrase_;
251 return item;
252 }
253
254 - (CollectionViewItem*)passphraseErrorItemWithMessage:(NSString*)errorMessage {
255 PassphraseErrorItem* item =
256 [[[PassphraseErrorItem alloc] initWithType:ItemTypeError] autorelease];
257 item.text = errorMessage;
258 return item;
259 }
260
261 - (CollectionViewItem*)footerItem {
262 CollectionViewFooterItem* footerItem = [[[CollectionViewFooterItem alloc]
263 initWithType:ItemTypeFooter] autorelease];
264 footerItem.text = self.footerMessage;
265 footerItem.linkURL = google_util::AppendGoogleLocaleParam(
266 GURL(kSyncGoogleDashboardURL),
267 GetApplicationContext()->GetApplicationLocale());
268 footerItem.linkDelegate = self;
269 return footerItem;
270 }
271
272 #pragma mark - MDCCollectionViewStylingDelegate
273
274 - (MDCCollectionViewCellStyle)collectionView:(UICollectionView*)collectionView
275 cellStyleForSection:(NSInteger)section {
276 NSInteger sectionIdentifier =
277 [self.collectionViewModel sectionIdentifierForSection:section];
278 switch (sectionIdentifier) {
279 case SectionIdentifierFooter:
280 // Display the Learn More footer in the default style with no "card" UI
281 // and no section padding.
282 return MDCCollectionViewCellStyleDefault;
283 default:
284 return self.styler.cellStyle;
285 }
286 }
287
288 - (BOOL)collectionView:(UICollectionView*)collectionView
289 shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath {
290 NSInteger sectionIdentifier =
291 [self.collectionViewModel sectionIdentifierForSection:indexPath.section];
292 switch (sectionIdentifier) {
293 case SectionIdentifierFooter:
294 // Display the Learn More footer without any background image or
295 // shadowing.
296 return YES;
297 default:
298 return NO;
299 }
300 }
301
302 - (CGFloat)collectionView:(UICollectionView*)collectionView
303 cellHeightAtIndexPath:(NSIndexPath*)indexPath {
304 CollectionViewItem* item =
305 [self.collectionViewModel itemAtIndexPath:indexPath];
306 if (item.type == ItemTypeMessage || item.type == ItemTypeFooter) {
307 return [MDCCollectionViewCell
308 cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds)
309 forItem:item];
310 }
311 return MDCCellDefaultOneLineHeight;
312 }
313
314 #pragma mark - UICollectionViewDataSource
315
316 - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView
317 cellForItemAtIndexPath:(NSIndexPath*)indexPath {
318 UICollectionViewCell* cell =
319 [super collectionView:collectionView cellForItemAtIndexPath:indexPath];
320 CollectionViewItem* item =
321 [self.collectionViewModel itemAtIndexPath:indexPath];
322 if (item.type == ItemTypeMessage) {
323 CardMultilineCell* messageCell =
324 base::mac::ObjCCastStrict<CardMultilineCell>(cell);
325 messageCell.textLabel.font =
326 [[MDFRobotoFontLoader sharedInstance] mediumFontOfSize:14];
327 }
328 return cell;
329 }
330
331 #pragma mark - UICollectionViewDelegate
332
333 - (void)collectionView:(UICollectionView*)collectionView
334 didSelectItemAtIndexPath:(NSIndexPath*)indexPath {
335 [super collectionView:collectionView didSelectItemAtIndexPath:indexPath];
336 NSInteger itemType =
337 [self.collectionViewModel itemTypeForIndexPath:indexPath];
338 if (itemType == ItemTypeEnterPassphrase) {
339 [passphrase_ becomeFirstResponder];
340 }
341 }
342
343 #pragma mark - Behavior
344
345 - (BOOL)forDecryption {
346 return YES;
347 }
348
349 - (void)signInPressed {
350 DCHECK([passphrase_ text].length);
351
352 if (!syncObserver_.get()) {
353 syncObserver_.reset(new SyncObserverBridge(
354 self,
355 IOSChromeProfileSyncServiceFactory::GetForBrowserState(browserState_)));
356 }
357
358 // Clear out the error message.
359 self.syncErrorMessage = nil;
360
361 browser_sync::ProfileSyncService* service =
362 IOSChromeProfileSyncServiceFactory::GetForBrowserState(browserState_);
363 DCHECK(service);
364 // It is possible for a race condition to happen where a user is allowed
365 // to call the backend with the passphrase before the backend is
366 // initialized.
367 // See crbug/276714. As a temporary measure, ignore the tap on sign-in
368 // button. A better fix may be to disable the rightBarButtonItem (submit)
369 // until backend is initialized.
370 if (!service->IsEngineInitialized())
371 return;
372
373 [self showDecryptionProgress];
374 std::string passphrase = base::SysNSStringToUTF8([passphrase_ text]);
375 if ([self forDecryption]) {
376 if (!service->SetDecryptionPassphrase(passphrase)) {
377 syncObserver_.reset();
378 [self clearFieldsOnError:l10n_util::GetNSString(
379 IDS_IOS_SYNC_INCORRECT_PASSPHRASE)];
380 [self hideDecryptionProgress];
381 }
382 } else {
383 service->EnableEncryptEverything();
384 service->SetEncryptionPassphrase(
385 passphrase, browser_sync::ProfileSyncService::EXPLICIT);
386 }
387 [self reloadData];
388 }
389
390 - (void)setRightNavBarItem {
391 UIBarButtonItem* submitButtonItem = self.navigationItem.rightBarButtonItem;
392 if (!submitButtonItem) {
393 submitButtonItem = [[[UIBarButtonItem alloc]
394 initWithTitle:l10n_util::GetNSString(IDS_IOS_SYNC_DECRYPT_BUTTON)
395 style:UIBarButtonItemStylePlain
396 target:self
397 action:@selector(signInPressed)] autorelease];
398 }
399 submitButtonItem.enabled = [self areAllFieldsFilled];
400
401 // Only setting the enabled state doesn't make the item redraw. As a
402 // workaround, set it again.
403 self.navigationItem.rightBarButtonItem = submitButtonItem;
404 }
405
406 - (BOOL)areAllFieldsFilled {
407 return [self.passphrase text].length > 0;
408 }
409
410 - (void)clearFieldsOnError:(NSString*)errorMessage {
411 self.syncErrorMessage = errorMessage;
412 [self.passphrase setText:@""];
413 }
414
415 - (void)showDecryptionProgress {
416 if (isDecryptionProgressShown_)
417 return;
418 isDecryptionProgressShown_ = YES;
419
420 // Hide the button.
421 self.navigationItem.rightBarButtonItem = nil;
422
423 // Custom title view with spinner.
424 DCHECK(!savedTitle_);
425 DCHECK(!savedLeftButton_);
426 savedLeftButton_.reset([self.navigationItem.leftBarButtonItem retain]);
427 self.navigationItem.leftBarButtonItem = [self spinnerButton];
428 savedTitle_.reset([self.title copy]);
429 self.title = processingMessage_;
430 }
431
432 - (void)hideDecryptionProgress {
433 if (!isDecryptionProgressShown_)
434 return;
435 isDecryptionProgressShown_ = NO;
436
437 self.navigationItem.leftBarButtonItem = savedLeftButton_.autorelease();
438 self.title = savedTitle_.autorelease();
439 [self setRightNavBarItem];
440 }
441
442 - (void)registerTextField:(UITextField*)textField {
443 [textField addTarget:self
444 action:@selector(textFieldDidBeginEditing:)
445 forControlEvents:UIControlEventEditingDidBegin];
446 [textField addTarget:self
447 action:@selector(textFieldDidChange:)
448 forControlEvents:UIControlEventEditingChanged];
449 [textField addTarget:self
450 action:@selector(textFieldDidEndEditing:)
451 forControlEvents:UIControlEventEditingDidEndOnExit];
452 }
453
454 - (void)unregisterTextField:(UITextField*)textField {
455 [textField removeTarget:self
456 action:@selector(textFieldDidBeginEditing:)
457 forControlEvents:UIControlEventEditingDidBegin];
458 [textField removeTarget:self
459 action:@selector(textFieldDidChange:)
460 forControlEvents:UIControlEventEditingChanged];
461 [textField removeTarget:self
462 action:@selector(textFieldDidEndEditing:)
463 forControlEvents:UIControlEventEditingDidEndOnExit];
464 }
465
466 - (UIBarButtonItem*)spinnerButton {
467 CGRect customViewFrame = CGRectMake(0, 0, kSpinnerButtonCustomViewSize,
468 kSpinnerButtonCustomViewSize);
469 base::scoped_nsobject<UIView> customView(
470 [[UIView alloc] initWithFrame:customViewFrame]);
471
472 base::scoped_nsobject<UIActivityIndicatorView> spinner(
473 [[UIActivityIndicatorView alloc]
474 initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]);
475
476 CGRect spinnerFrame = [spinner bounds];
477 spinnerFrame.origin.x = kSpinnerButtonPadding;
478 spinnerFrame.origin.y = kSpinnerButtonPadding;
479 [spinner setFrame:spinnerFrame];
480 [customView addSubview:spinner];
481
482 base::scoped_nsobject<UIBarButtonItem> leftBarButtonItem(
483 [[UIBarButtonItem alloc] initWithCustomView:customView]);
484
485 [spinner setHidesWhenStopped:NO];
486 [spinner startAnimating];
487
488 return leftBarButtonItem.autorelease();
489 }
490
491 - (void)stopObserving {
492 // Stops observing the sync service. This is required during the shutdown
493 // phase to avoid observing sync events for a browser state that is being
494 // killed.
495 syncObserver_.reset();
496 tokenServiceObserver_.reset();
497 }
498
499 #pragma mark - UIControl events listener
500
501 - (void)textFieldDidBeginEditing:(id)sender {
502 // Remove the error cell if there is one.
503 CollectionViewModel* model = self.collectionViewModel;
504 NSInteger section =
505 [model sectionForSectionIdentifier:SectionIdentifierPassphrase];
506 NSIndexPath* errorIndexPath =
507 [NSIndexPath indexPathForItem:ItemTypeError inSection:section];
508 if ([model hasItemAtIndexPath:errorIndexPath] &&
509 [model itemTypeForIndexPath:errorIndexPath] == ItemTypeError) {
510 DCHECK(self.syncErrorMessage);
511 [model removeItemWithType:ItemTypeError
512 fromSectionWithIdentifier:SectionIdentifierPassphrase];
513 [self.collectionView deleteItemsAtIndexPaths:@[ errorIndexPath ]];
514 self.syncErrorMessage = nil;
515 }
516 }
517
518 - (void)textFieldDidChange:(id)sender {
519 [self setRightNavBarItem];
520 }
521
522 - (void)textFieldDidEndEditing:(id)sender {
523 if (sender == self.passphrase) {
524 if ([self areAllFieldsFilled]) {
525 [self signInPressed];
526 } else {
527 [self clearFieldsOnError:l10n_util::GetNSString(
528 IDS_SYNC_EMPTY_PASSPHRASE_ERROR)];
529 [self reloadData];
530 }
531 }
532 }
533
534 #pragma mark - SyncObserverModelBridge
535
536 - (void)onSyncStateChanged {
537 browser_sync::ProfileSyncService* service =
538 IOSChromeProfileSyncServiceFactory::GetForBrowserState(browserState_);
539
540 if (!service->IsEngineInitialized()) {
541 return;
542 }
543
544 // Checking if the operation succeeded.
545 if (!service->IsPassphraseRequired() &&
546 (service->IsUsingSecondaryPassphrase() || [self forDecryption])) {
547 syncObserver_.reset();
548 [base::mac::ObjCCastStrict<SettingsNavigationController>(
549 self.navigationController)
550 popViewControllerOrCloseSettingsAnimated:YES];
551 return;
552 }
553
554 // Handling passphrase error case.
555 if (service->IsPassphraseRequired()) {
556 self.syncErrorMessage =
557 l10n_util::GetNSString(IDS_IOS_SYNC_INCORRECT_PASSPHRASE);
558 }
559 [self hideDecryptionProgress];
560 [self reloadData];
561 }
562
563 #pragma mark - OAuth2TokenServiceObserverBridgeDelegate
564
565 - (void)onEndBatchChanges {
566 if (AuthenticationServiceFactory::GetForBrowserState(browserState_)
567 ->IsAuthenticated()) {
568 return;
569 }
570 [base::mac::ObjCCastStrict<SettingsNavigationController>(
571 self.navigationController) popViewControllerOrCloseSettingsAnimated:NO];
572 }
573
574 #pragma mark - SettingsControllerProtocol callbacks
575
576 - (void)settingsWillBeDismissed {
577 [self stopObserving];
578 }
579
580 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698