-
Notifications
You must be signed in to change notification settings - Fork 6.7k
/
SigninManagerImpl.java
1028 lines (916 loc) · 43.3 KB
/
SigninManagerImpl.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.signin;
import androidx.annotation.IntDef;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.jni_zero.CalledByNative;
import org.jni_zero.NativeMethods;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.Promise;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.bookmarks.BookmarkModel;
import org.chromium.chrome.browser.browsing_data.BrowsingDataBridge;
import org.chromium.chrome.browser.browsing_data.BrowsingDataType;
import org.chromium.chrome.browser.browsing_data.TimePeriod;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.password_manager.PasswordManagerUtilBridge;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.services.SigninManager;
import org.chromium.chrome.browser.signin.services.SigninPreferencesManager;
import org.chromium.components.externalauth.ExternalAuthUtils;
import org.chromium.components.signin.AccountManagerFacade;
import org.chromium.components.signin.AccountManagerFacadeProvider;
import org.chromium.components.signin.AccountUtils;
import org.chromium.components.signin.AccountsChangeObserver;
import org.chromium.components.signin.SigninFeatureMap;
import org.chromium.components.signin.SigninFeatures;
import org.chromium.components.signin.base.CoreAccountId;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.AccountInfoServiceProvider;
import org.chromium.components.signin.identitymanager.AccountTrackerService;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.signin.identitymanager.IdentityMutator;
import org.chromium.components.signin.identitymanager.PrimaryAccountError;
import org.chromium.components.signin.metrics.SigninAccessPoint;
import org.chromium.components.signin.metrics.SignoutReason;
import org.chromium.components.sync.SyncService;
import org.chromium.components.user_prefs.UserPrefs;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Android wrapper of the SigninManager which provides access from the Java layer.
*
* <p>This class handles common paths during the sign-in and sign-out flows.
*
* <p>Only usable from the UI thread as the native SigninManager requires its access to be in the UI
* thread.
*
* <p>See chrome/browser/android/signin/signin_manager_android.h for more details.
*/
class SigninManagerImpl implements IdentityManager.Observer, SigninManager, AccountsChangeObserver {
private static final String TAG = "SigninManager";
/**
* Address of the native Signin Manager android.
* This is not final, as destroy() updates this.
*/
private long mNativeSigninManagerAndroid;
private final Profile mProfile;
private final AccountTrackerService mAccountTrackerService;
private final AccountManagerFacade mAccountManagerFacade;
private final IdentityManager mIdentityManager;
private final IdentityMutator mIdentityMutator;
private final SyncService mSyncService;
private final ObserverList<SignInStateObserver> mSignInStateObservers = new ObserverList<>();
private final List<Runnable> mCallbacksWaitingForPendingOperation = new ArrayList<>();
private boolean mSigninAllowedByPolicy;
/**
* Will be set during the sign in process, and nulled out when there is not a pending sign in.
* Needs to be null checked after ever async entry point because it can be nulled out at any
* time by system accounts changing.
*/
private @Nullable SignInState mSignInState;
/**
* Set during sign-out process and nulled out once complete. Helps to atomically gather/clear
* various sign-out state.
*/
private @Nullable SignOutState mSignOutState;
/** Tracks whether deletion of browsing data is in progress. */
private boolean mWipeUserDataInProgress;
/**
* Called by native to create an instance of SigninManager.
*
* @param nativeSigninManagerAndroid A pointer to native's SigninManagerAndroid.
*/
@CalledByNative
@VisibleForTesting
static SigninManager create(
long nativeSigninManagerAndroid,
Profile profile,
AccountTrackerService accountTrackerService,
IdentityManager identityManager,
IdentityMutator identityMutator,
SyncService syncService) {
assert nativeSigninManagerAndroid != 0;
assert profile != null;
assert accountTrackerService != null;
assert identityManager != null;
assert identityMutator != null;
final SigninManagerImpl signinManager =
new SigninManagerImpl(
nativeSigninManagerAndroid,
profile,
accountTrackerService,
identityManager,
identityMutator,
syncService);
identityManager.addObserver(signinManager);
AccountInfoServiceProvider.init(identityManager, accountTrackerService);
if (!SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
CoreAccountInfo primaryAccountInfo =
identityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN);
signinManager.reloadAllAccountsFromSystem(
CoreAccountInfo.getIdFrom(primaryAccountInfo));
}
return signinManager;
}
private SigninManagerImpl(
long nativeSigninManagerAndroid,
Profile profile,
AccountTrackerService accountTrackerService,
IdentityManager identityManager,
IdentityMutator identityMutator,
SyncService syncService) {
ThreadUtils.assertOnUiThread();
mNativeSigninManagerAndroid = nativeSigninManagerAndroid;
mProfile = profile;
mAccountTrackerService = accountTrackerService;
mIdentityManager = identityManager;
mIdentityMutator = identityMutator;
mSyncService = syncService;
mSigninAllowedByPolicy =
SigninManagerImplJni.get().isSigninAllowedByPolicy(mNativeSigninManagerAndroid);
mAccountManagerFacade = AccountManagerFacadeProvider.getInstance();
if (SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
mAccountManagerFacade.addObserver(this);
Promise<List<CoreAccountInfo>> coreAccountInfosPromise =
mAccountManagerFacade.getCoreAccountInfos();
if (coreAccountInfosPromise.isFulfilled()
&& (mAccountManagerFacade.didAccountFetchSucceed()
|| !coreAccountInfosPromise.getResult().isEmpty())) {
seedThenReloadAllAccountsFromSystem(
CoreAccountInfo.getIdFrom(
identityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN)));
}
}
}
/**
* Triggered during SigninManagerAndroidWrapper's KeyedService::Shutdown. Drop references with
* external services and native.
*/
@VisibleForTesting
@CalledByNative
void destroy() {
AccountInfoServiceProvider.get().destroy();
mIdentityManager.removeObserver(this);
if (SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
mAccountManagerFacade.removeObserver(this);
}
mNativeSigninManagerAndroid = 0;
}
/** Implements {@link AccountsChangeObserver}. */
@Override
public void onCoreAccountInfosChanged() {
if (!SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
throw new IllegalStateException(
"This method should never be called when SeedAccountsRevamp is disabled");
}
Promise<List<CoreAccountInfo>> coreAccountInfosPromise =
mAccountManagerFacade.getCoreAccountInfos();
assert coreAccountInfosPromise.isFulfilled();
List<CoreAccountInfo> coreAccountInfos = coreAccountInfosPromise.getResult();
if (!mAccountManagerFacade.didAccountFetchSucceed() && coreAccountInfos.isEmpty()) {
// If the account fetch did not succeed, the AccountManagerFacade falls back to an empty
// list. Do nothing when this is the case.
return;
}
@Nullable
CoreAccountInfo primaryAccountInfo =
mIdentityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN);
if (primaryAccountInfo == null) {
seedThenReloadAllAccountsFromSystem(null);
return;
}
if (AccountUtils.findCoreAccountInfoByGaiaId(
coreAccountInfos, primaryAccountInfo.getGaiaId())
!= null) {
// The primary account is still on the device, reseed accounts.
seedThenReloadAllAccountsFromSystem(CoreAccountInfo.getIdFrom(primaryAccountInfo));
// Should be called after re-seeding accounts to make sure that we get the new email.
maybeUpdateLegacySyncAccountEmail();
return;
}
if (isOperationInProgress()) {
// Re-check whether there's still a primary account after the current operation.
runAfterOperationInProgress(this::onCoreAccountInfosChanged);
} else {
// Sign out if the current primary account is no longer on the device.
signOut(SignoutReason.ACCOUNT_REMOVED_FROM_DEVICE);
}
}
/**
* Updates the email of the primary account stored in shared preferences in case the primary
* email address of the primary account has changed.
*/
private void maybeUpdateLegacySyncAccountEmail() {
// TODO(crbug.com/40066882): Use ConsentLevel.SIGNIN instead.
CoreAccountInfo accountInfo = mIdentityManager.getPrimaryAccountInfo(ConsentLevel.SYNC);
if (accountInfo == null) {
return;
}
if (accountInfo
.getEmail()
.equals(SigninPreferencesManager.getInstance().getLegacySyncAccountEmail())) {
return;
}
SigninPreferencesManager.getInstance().setLegacySyncAccountEmail(accountInfo.getEmail());
}
/** Extracts the domain name of a given account's email. */
@Override
public String extractDomainName(String accountEmail) {
return SigninManagerImplJni.get().extractDomainName(accountEmail);
}
/** Returns the IdentityManager used by SigninManager. */
@Override
public IdentityManager getIdentityManager() {
return mIdentityManager;
}
/** Returns true if sign in can be started now. */
@Override
public boolean isSigninAllowed() {
return mSignInState == null
&& mSigninAllowedByPolicy
&& mIdentityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN) == null
&& isSigninSupported(/* requireUpdatedPlayServices= */ false);
}
/** Returns true if sync opt in can be started now. */
@Override
public boolean isSyncOptInAllowed() {
return mSignInState == null
&& mSigninAllowedByPolicy
&& mIdentityManager.getPrimaryAccountInfo(ConsentLevel.SYNC) == null
&& isSigninSupported(/* requireUpdatedPlayServices= */ false);
}
/** Returns true if sign out can be started now. */
@Override
public boolean isSignOutAllowed() {
return mSignOutState == null
&& mSignInState == null
&& mIdentityManager.getPrimaryAccountInfo(ConsentLevel.SIGNIN) != null
&& mIdentityManager.isClearPrimaryAccountAllowed();
}
/** Returns true if signin is disabled by policy. */
@Override
public boolean isSigninDisabledByPolicy() {
return !mSigninAllowedByPolicy;
}
/**
* Returns whether the user can sign-in (maybe after an update to Google Play services).
* @param requireUpdatedPlayServices Indicates whether an updated version of play services is
* required or not.
*/
@Override
public boolean isSigninSupported(boolean requireUpdatedPlayServices) {
if (ApiCompatibilityUtils.isDemoUser()) {
return false;
}
if (requireUpdatedPlayServices) {
return ExternalAuthUtils.getInstance().canUseGooglePlayServices();
}
return !ExternalAuthUtils.getInstance()
.isGooglePlayServicesMissing(ContextUtils.getApplicationContext());
}
/**
* @return Whether force sign-in is enabled by policy.
*/
@Override
public boolean isForceSigninEnabled() {
return SigninManagerImplJni.get().isForceSigninEnabled(mNativeSigninManagerAndroid);
}
/** Registers a SignInStateObserver to be notified when the user signs in or out of Chrome. */
@Override
public void addSignInStateObserver(SignInStateObserver observer) {
mSignInStateObservers.addObserver(observer);
}
/** Unregisters a SignInStateObserver to be notified when the user signs in or out of Chrome. */
@Override
public void removeSignInStateObserver(SignInStateObserver observer) {
mSignInStateObservers.removeObserver(observer);
}
private void notifySignInAllowedChanged() {
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
for (SignInStateObserver observer : mSignInStateObservers) {
observer.onSignInAllowedChanged();
}
});
}
private void notifySignOutAllowedChanged() {
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
for (SignInStateObserver observer : mSignInStateObservers) {
observer.onSignOutAllowedChanged();
}
});
}
@Override
public void signin(
CoreAccountInfo coreAccountInfo,
@SigninAccessPoint int accessPoint,
@Nullable SignInCallback callback) {
signinInternal(SignInState.createForSignin(accessPoint, coreAccountInfo, callback));
}
@Override
public void signinAndEnableSync(
CoreAccountInfo coreAccountInfo,
@SigninAccessPoint int accessPoint,
@Nullable SignInCallback callback) {
signinInternal(
SignInState.createForSigninAndEnableSync(accessPoint, coreAccountInfo, callback));
}
private void signinInternal(SignInState signInState) {
assert isSyncOptInAllowed()
: String.format(
"Sign-in isn't allowed!\n"
+ " mSignInState: %s\n"
+ " mSigninAllowedByPolicy: %s\n"
+ " Primary sync account: %s",
mSignInState,
mSigninAllowedByPolicy,
mIdentityManager.getPrimaryAccountInfo(ConsentLevel.SYNC));
assert signInState != null : "SigninState shouldn't be null!";
// The mSignInState must be updated prior to the async processing below, as this indicates
// that a signin operation is in progress and prevents other sign in operations from being
// started until this one completes (see {@link isOperationInProgress()}).
mSignInState = signInState;
if (SigninFeatureMap.isEnabled(SigninFeatures.ENTERPRISE_POLICY_ON_SIGNIN)
&& !getUserAcceptedAccountManagement()) {
String email = mSignInState.mCoreAccountInfo.getEmail();
isAccountManaged(
email,
(Boolean isAccountManaged) -> {
if (isAccountManaged) {
throw new IllegalStateException(
"User must accept Account Management before "
+ "signing into a Managed account.");
} else {
signinInternalAfterCheckingManagedState();
}
});
} else {
signinInternalAfterCheckingManagedState();
}
}
private void signinInternalAfterCheckingManagedState() {
if (SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
// Retrieve the primary account and use it to seed and reload all accounts.
if (!mAccountManagerFacade.getCoreAccountInfos().isFulfilled()) {
throw new IllegalStateException(
"Account information should be available on signin");
}
if (mSignInState.mCoreAccountInfo == null) {
throw new IllegalStateException(
"The account should be on the device before it can be set as primary.");
}
seedThenReloadAllAccountsFromSystem(mSignInState.mCoreAccountInfo.getId());
notifySignInAllowedChanged();
if (SigninFeatureMap.isEnabled(SigninFeatures.ENTERPRISE_POLICY_ON_SIGNIN)
|| mSignInState.shouldTurnSyncOn()) {
Log.d(TAG, "Checking if account has policy management enabled");
fetchAndApplyCloudPolicy(
mSignInState.mCoreAccountInfo, this::finishSignInAfterPolicyEnforced);
} else {
// Sign-in without sync doesn't enforce enterprise policy, so skip that
// step.
finishSignInAfterPolicyEnforced();
}
} else {
Log.i(TAG, "Signin starts (enabling sync: %b).", mSignInState.shouldTurnSyncOn());
AccountInfoServiceProvider.get()
.getAccountInfoByEmail(mSignInState.mCoreAccountInfo.getEmail())
.then(
accountInfo -> {
mSignInState.mCoreAccountInfo = accountInfo;
notifySignInAllowedChanged();
if (SigninFeatureMap.isEnabled(
SigninFeatures.ENTERPRISE_POLICY_ON_SIGNIN)
|| mSignInState.shouldTurnSyncOn()) {
Log.d(TAG, "Checking if account has policy management enabled");
fetchAndApplyCloudPolicy(
mSignInState.mCoreAccountInfo,
this::finishSignInAfterPolicyEnforced);
} else {
// Sign-in without sync doesn't enforce enterprise policy, so
// skip that step.
finishSignInAfterPolicyEnforced();
}
});
}
}
/**
* Finishes the sign-in flow. If the user is managed, the policy should be fetched and enforced
* before calling this method.
*/
private void finishSignInAfterPolicyEnforced() {
assert mSignInState != null : "SigninState shouldn't be null!";
assert !mIdentityManager.hasPrimaryAccount(ConsentLevel.SYNC)
: "The user should not be already signed in";
// Setting the primary account triggers observers which query accounts from IdentityManager.
// Reloading before setting the primary ensures they don't get an empty list of accounts.
if (!SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
reloadAllAccountsFromSystem(mSignInState.mCoreAccountInfo.getId());
}
@ConsentLevel
int consentLevel =
mSignInState.shouldTurnSyncOn() ? ConsentLevel.SYNC : ConsentLevel.SIGNIN;
// Retain the sign-in callback since pref commit callback will be called after sign-in is
// considered completed and sign-in state is reset.
final SignInCallback signInCallback = mSignInState.mCallback;
@PrimaryAccountError
int primaryAccountError =
mIdentityMutator.setPrimaryAccount(
mSignInState.mCoreAccountInfo.getId(),
consentLevel,
mSignInState.getAccessPoint(),
() -> {
Log.d(TAG, "Sign-in native prefs written.");
if (signInCallback != null) {
signInCallback.onPrefsCommitted();
}
});
if (primaryAccountError != PrimaryAccountError.NO_ERROR) {
Log.w(
TAG,
"SetPrimaryAccountError in IdentityManager: %d, aborting signin",
primaryAccountError);
abortSignIn();
return;
}
if (mSignInState.shouldTurnSyncOn()) {
// TODO(https://crbug.com/1091858): Remove this after migrating the legacy code that
// uses the sync account before the native is loaded.
SigninPreferencesManager.getInstance()
.setLegacySyncAccountEmail(mSignInState.mCoreAccountInfo.getEmail());
mSyncService.setSyncRequested();
RecordUserAction.record("Signin_Signin_Succeed");
RecordHistogram.recordEnumeratedHistogram(
"Signin.SigninCompletedAccessPoint",
mSignInState.getAccessPoint(),
SigninAccessPoint.MAX);
}
if (mSignInState.mCallback != null) {
mSignInState.mCallback.onSignInComplete();
}
Log.i(TAG, "Signin completed.");
mSignInState = null;
notifyCallbacksWaitingForOperation();
notifySignInAllowedChanged();
notifySignOutAllowedChanged();
for (SignInStateObserver observer : mSignInStateObservers) {
observer.onSignedIn();
}
}
@Override
@MainThread
public void runAfterOperationInProgress(Runnable runnable) {
ThreadUtils.assertOnUiThread();
if (isOperationInProgress()) {
mCallbacksWaitingForPendingOperation.add(runnable);
return;
}
PostTask.postTask(TaskTraits.UI_DEFAULT, runnable);
}
/**
* Whether an operation is in progress for which we should wait before
* scheduling users of {@link runAfterOperationInProgress}.
*/
private boolean isOperationInProgress() {
ThreadUtils.assertOnUiThread();
return mSignInState != null || mSignOutState != null || mWipeUserDataInProgress;
}
/**
* Maybe notifies any callbacks registered via runAfterOperationInProgress().
*
* The callbacks are notified in FIFO order, and each callback is only notified if there is no
* outstanding work (either work which was outstanding at the time the callback was added, or
* which was scheduled by subsequently dequeued callbacks).
*/
private void notifyCallbacksWaitingForOperation() {
ThreadUtils.assertOnUiThread();
while (!mCallbacksWaitingForPendingOperation.isEmpty()) {
if (isOperationInProgress()) return;
Runnable callback = mCallbacksWaitingForPendingOperation.remove(0);
PostTask.postTask(TaskTraits.UI_DEFAULT, callback);
}
}
/**
* Initialize SignOutState, and call identity mutator to revoke the sync consent. Processing
* will complete asynchronously in the {@link #onPrimaryAccountChanged()} callback.
*/
@Override
public void revokeSyncConsent(
@SignoutReason int signoutSource,
SignOutCallback signOutCallback,
boolean forceWipeUserData) {
// Only one signOut at a time!
assert mSignOutState == null;
// User must be syncing.
assert mIdentityManager.hasPrimaryAccount(ConsentLevel.SYNC);
// Grab the management domain before nativeSignOut() potentially clears it.
String managementDomain = getManagementDomain();
// We wipe sync data only, as wiping the profile data would also trigger sign-out.
mSignOutState =
new SignOutState(
signOutCallback,
(forceWipeUserData || managementDomain != null)
? SignOutState.DataWipeAction.WIPE_SYNC_DATA_ONLY
: SignOutState.DataWipeAction.WIPE_SIGNIN_DATA_ONLY);
Log.i(
TAG,
"Revoking sync consent, dataWipeAction: %d",
(forceWipeUserData || managementDomain != null)
? SignOutState.DataWipeAction.WIPE_SYNC_DATA_ONLY
: SignOutState.DataWipeAction.WIPE_SIGNIN_DATA_ONLY);
mIdentityMutator.revokeSyncConsent(signoutSource);
notifySignOutAllowedChanged();
disableSyncAndWipeData(this::finishSignOut);
}
/**
* Signs out of Chrome. This method clears the signed-in username, stops sync and sends out a
* sign-out notification on the native side.
*
* @param signoutSource describes the event driving the signout (e.g.
* {@link SignoutReason.USER_CLICKED_SIGNOUT_SETTINGS}).
* @param signOutCallback Callback to notify about the sign-out progress.
* @param forceWipeUserData Whether user selected to wipe all device data.
*/
@Override
public void signOut(
@SignoutReason int signoutSource,
SignOutCallback signOutCallback,
boolean forceWipeUserData) {
// Only one signOut at a time!
assert mSignOutState == null;
// Grab the management domain before nativeSignOut() potentially clears it.
String managementDomain = getManagementDomain();
mSignOutState =
new SignOutState(
signOutCallback,
(forceWipeUserData || managementDomain != null)
? SignOutState.DataWipeAction.WIPE_ALL_PROFILE_DATA
: SignOutState.DataWipeAction.WIPE_SIGNIN_DATA_ONLY);
Log.i(
TAG,
"Signing out, dataWipeAction: %d",
(forceWipeUserData || managementDomain != null)
? SignOutState.DataWipeAction.WIPE_ALL_PROFILE_DATA
: SignOutState.DataWipeAction.WIPE_SIGNIN_DATA_ONLY);
mIdentityMutator.clearPrimaryAccount(signoutSource);
notifySignOutAllowedChanged();
disableSyncAndWipeData(this::finishSignOut);
}
/**
* Returns the management domain if the signed in account is managed, otherwise returns null.
*/
@Override
public String getManagementDomain() {
return SigninManagerImplJni.get().getManagementDomain(mNativeSigninManagerAndroid);
}
/**
* Aborts the current sign in.
*
* Package protected to allow dialog fragments to abort the signin flow.
*/
private void abortSignIn() {
// Ensure this function can only run once per signin flow.
SignInState signInState = mSignInState;
assert signInState != null;
mSignInState = null;
notifyCallbacksWaitingForOperation();
RecordHistogram.recordEnumeratedHistogram(
"Signin.SigninAbortedAccessPoint",
signInState.getAccessPoint(),
SigninAccessPoint.MAX);
if (signInState.mCallback != null) {
signInState.mCallback.onSignInAborted();
}
stopApplyingCloudPolicy();
Log.d(TAG, "Signin flow aborted.");
notifySignInAllowedChanged();
if (SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
seedThenReloadAllAccountsFromSystem(null);
}
}
@VisibleForTesting
void finishSignOut() {
// Should be set at start of sign-out flow.
assert mSignOutState != null;
if (ChromeFeatureList.isEnabled(
ChromeFeatureList.SYNC_ANDROID_LIMIT_NTP_PROMO_IMPRESSIONS)) {
// After sign-out, reset the Sync promo show count, so the user will see Sync promos
// again.
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.SYNC_PROMO_SHOW_COUNT.createKey(
SigninPreferencesManager.SyncPromoAccessPointId.NTP),
0);
}
SignOutCallback signOutCallback = mSignOutState.mSignOutCallback;
if (SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
seedThenReloadAllAccountsFromSystem(null);
}
mSignOutState = null;
if (signOutCallback != null) signOutCallback.signOutComplete();
notifyCallbacksWaitingForOperation();
for (SignInStateObserver observer : mSignInStateObservers) {
observer.onSignedOut();
}
}
@CalledByNative
private void onSigninAllowedByPolicyChanged(boolean newSigninAllowedByPolicy) {
mSigninAllowedByPolicy = newSigninAllowedByPolicy;
notifySignInAllowedChanged();
}
/**
* Verifies if the account is managed. Callback may be called either
* synchronously or asynchronously depending on the availability of the
* result.
* @param email An email of the account.
* @param callback The callback that will receive true if the account is managed, false
* otherwise.
*/
// TODO(crbug.com/1002408) Update API to use CoreAccountInfo instead of email
@Override
public void isAccountManaged(String email, final Callback<Boolean> callback) {
assert email != null;
CoreAccountInfo account = mIdentityManager.findExtendedAccountInfoByEmailAddress(email);
isAccountManaged(account, callback);
}
@Override
public void isAccountManaged(
@NonNull CoreAccountInfo account, final Callback<Boolean> callback) {
assert account != null;
SigninManagerImplJni.get().isAccountManaged(mNativeSigninManagerAndroid, account, callback);
}
@Override
public void reloadAllAccountsFromSystem(@Nullable CoreAccountId primaryAccountId) {
if (SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
throw new IllegalStateException(
"This method should never be called when SeedAccountsRevamp is enabled");
}
mIdentityMutator.reloadAllAccountsFromSystemWithPrimaryAccount(primaryAccountId);
}
private void seedThenReloadAllAccountsFromSystem(@Nullable CoreAccountId primaryAccountId) {
if (!SigninFeatureMap.isEnabled(SigninFeatures.SEED_ACCOUNTS_REVAMP)) {
throw new IllegalStateException(
"This method should never be called when SeedAccountsRevamp is disabled");
}
if (!mAccountManagerFacade.getCoreAccountInfos().isFulfilled()) {
throw new IllegalStateException("Account information should be available when seeding");
}
mIdentityMutator.seedAccountsThenReloadAllAccountsWithPrimaryAccount(
mAccountManagerFacade.getCoreAccountInfos().getResult(), primaryAccountId);
mIdentityManager.refreshAccountInfoIfStale(
mAccountManagerFacade.getCoreAccountInfos().getResult());
}
/**
* Wipes the user's bookmarks and sync data.
*
* @param wipeDataCallback A callback which will be called once the data is wiped.
* @param dataWipeOption What kind of data to delete.
*/
@Override
public void wipeSyncUserData(Runnable wipeDataCallback, @DataWipeOption int dataWipeOption) {
assert !mWipeUserDataInProgress;
mWipeUserDataInProgress = true;
switch (dataWipeOption) {
case DataWipeOption.WIPE_SYNC_DATA:
wipeSyncUserDataOnly(wipeDataCallback);
break;
case DataWipeOption.WIPE_ALL_PROFILE_DATA:
SigninManagerImplJni.get()
.wipeProfileData(
mNativeSigninManagerAndroid,
() -> {
mWipeUserDataInProgress = false;
wipeDataCallback.run();
notifyCallbacksWaitingForOperation();
});
break;
}
}
// TODO(crbug.com/1272911): this function and disableSyncAndWipeData() have very similar
// functionality, but with different implementations. Consider merging them.
// TODO(crbug.com/1272911): add test coverage for this function (including its effect on
// notifyCallbacksWaitingForOperation()), after resolving the TODO above.
private void wipeSyncUserDataOnly(Runnable wipeDataCallback) {
final BookmarkModel model = BookmarkModel.getForProfile(mProfile);
model.finishLoadingBookmarkModel(
new Runnable() {
@Override
public void run() {
List<Integer> clearedTypes =
new ArrayList<>(
Arrays.asList(
BrowsingDataType.HISTORY,
BrowsingDataType.CACHE,
BrowsingDataType.SITE_DATA,
BrowsingDataType.FORM_DATA));
// If usesSplitStoresAndUPMForLocal() is true, browser sign-in won't upload
// existing passwords, so there's no reason to wipe them immediately before.
// Similarly, on browser sign-out, account passwords should survive (outside
// of the browser) to be used by other apps, until system-level sign-out.
// In other words, the browser has no business deleting any passwords here.
if (!PasswordManagerUtilBridge.usesSplitStoresAndUPMForLocal(
UserPrefs.get(mProfile))) {
clearedTypes.add(BrowsingDataType.PASSWORDS);
}
model.removeAllUserBookmarks();
BrowsingDataBridge.getForProfile(mProfile)
.clearBrowsingData(
new BrowsingDataBridge.OnClearBrowsingDataListener() {
@Override
public void onBrowsingDataCleared() {
assert mWipeUserDataInProgress;
mWipeUserDataInProgress = false;
wipeDataCallback.run();
notifyCallbacksWaitingForOperation();
}
},
clearedTypes.stream().mapToInt(Integer::intValue).toArray(),
TimePeriod.ALL_TIME);
}
});
}
@Override
public void setUserAcceptedAccountManagement(boolean acceptedAccountManagement) {
SigninManagerImplJni.get()
.setUserAcceptedAccountManagement(
mNativeSigninManagerAndroid, acceptedAccountManagement);
}
@Override
public boolean getUserAcceptedAccountManagement() {
return SigninManagerImplJni.get()
.getUserAcceptedAccountManagement(mNativeSigninManagerAndroid);
}
private boolean isGooglePlayServicesPresent() {
return !ExternalAuthUtils.getInstance()
.isGooglePlayServicesMissing(ContextUtils.getApplicationContext());
}
private void fetchAndApplyCloudPolicy(CoreAccountInfo account, final Runnable callback) {
SigninManagerImplJni.get()
.fetchAndApplyCloudPolicy(mNativeSigninManagerAndroid, account, callback);
}
private void stopApplyingCloudPolicy() {
SigninManagerImplJni.get().stopApplyingCloudPolicy(mNativeSigninManagerAndroid);
}
private void disableSyncAndWipeData(final Runnable wipeDataCallback) {
Log.i(
TAG,
"Native signout complete, wiping data (user callback: %s)",
mSignOutState.mDataWipeAction);
// TODO(https://crbug.com/1091858): Remove this after migrating the legacy code that
// uses the sync account before the native is
// loaded.
SigninPreferencesManager.getInstance().setLegacySyncAccountEmail(null);
if (mSignOutState.mSignOutCallback != null) {
mSignOutState.mSignOutCallback.preWipeData();
}
switch (mSignOutState.mDataWipeAction) {
case SignOutState.DataWipeAction.WIPE_SIGNIN_DATA_ONLY:
SigninManagerImplJni.get()
.wipeGoogleServiceWorkerCaches(
mNativeSigninManagerAndroid, wipeDataCallback);
break;
case SignOutState.DataWipeAction.WIPE_SYNC_DATA_ONLY:
wipeSyncUserData(wipeDataCallback, DataWipeOption.WIPE_SYNC_DATA);
break;
case SignOutState.DataWipeAction.WIPE_ALL_PROFILE_DATA:
wipeSyncUserData(wipeDataCallback, DataWipeOption.WIPE_ALL_PROFILE_DATA);
break;
}
}
/**
* Contains all the state needed for signin. This forces signin flow state to be
* cleared atomically, and all final fields to be set upon initialization.
*/
private static class SignInState {
private final @SigninAccessPoint Integer mAccessPoint;
private final boolean mShouldTurnSyncOn;
final SignInCallback mCallback;
/**
* Contains the full Core account info, which will be updated once account seeding is
* complete.
*
* <p>TODO(crbug.com/1491005): Update comment and make this field private if possible.
*/
CoreAccountInfo mCoreAccountInfo;
/**
* State for the sign-in flow that doesn't enable sync.
*
* @param accessPoint {@link SigninAccessPoint} that has initiated the sign-in.
* @param coreAccountInfo The {@link CoreAccountInfo} to sign in with.
* @param callback Called when the sign-in process finishes or is cancelled. Can be null.
*/
static SignInState createForSignin(
@SigninAccessPoint int accessPoint,
CoreAccountInfo coreAccountInfo,
@Nullable SignInCallback callback) {
return new SignInState(accessPoint, coreAccountInfo, callback, false);
}
/**
* State for the sync consent flow.
*
* @param accessPoint {@link SigninAccessPoint} that has initiated the sign-in.
* @param coreAccountInfo The {@link CoreAccountInfo} to sign in with.
* @param callback Called when the sign-in process finishes or is cancelled. Can be null.
*/
static SignInState createForSigninAndEnableSync(
@SigninAccessPoint int accessPoint,
CoreAccountInfo coreAccountInfo,
@Nullable SignInCallback callback) {
return new SignInState(accessPoint, coreAccountInfo, callback, true);
}
private SignInState(
@SigninAccessPoint Integer accessPoint,
CoreAccountInfo coreAccountInfo,
@Nullable SignInCallback callback,
boolean shouldTurnSyncOn) {
assert coreAccountInfo != null : "CoreAccountInfo must be set and valid to progress.";
mAccessPoint = accessPoint;
mCoreAccountInfo = coreAccountInfo;
mCallback = callback;
mShouldTurnSyncOn = shouldTurnSyncOn;
}
/**
* Getter for the access point that initiated sync consent flow. Shouldn't be called if
* {@link #shouldTurnSyncOn()} is false.
*/
@SigninAccessPoint
int getAccessPoint() {
assert mAccessPoint != null : "Not going to enable sync - no access point!";
return mAccessPoint;
}
/** Whether this sign-in flow should also turn on sync. */
boolean shouldTurnSyncOn() {
return mShouldTurnSyncOn;
}
}
/**
* Contains all the state needed for sign out. Like SignInState, this forces flow state to be
* cleared atomically, and all final fields to be set upon initialization.
*/
private static class SignOutState {
@IntDef({
DataWipeAction.WIPE_SIGNIN_DATA_ONLY,
DataWipeAction.WIPE_SYNC_DATA_ONLY,
DataWipeAction.WIPE_ALL_PROFILE_DATA
})
@Retention(RetentionPolicy.SOURCE)
public @interface DataWipeAction {
int WIPE_SIGNIN_DATA_ONLY = 0;
int WIPE_SYNC_DATA_ONLY = 1;
int WIPE_ALL_PROFILE_DATA = 2;
}
final @Nullable SignOutCallback mSignOutCallback;
final @DataWipeAction int mDataWipeAction;
/**
* @param signOutCallback Hooks to call before/after data wiping phase of sign-out.
* @param shouldWipeUserData Flag to wipe user data as requested by the user and enforced
* for managed users.
*/
SignOutState(
@Nullable SignOutCallback signOutCallback, @DataWipeAction int dataWipeAction) {
this.mSignOutCallback = signOutCallback;
this.mDataWipeAction = dataWipeAction;
}
}
@NativeMethods
interface Natives {