1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.contacts.model; 18 19 import android.accounts.Account; 20 import android.accounts.AccountManager; 21 import android.accounts.OnAccountsUpdateListener; 22 import android.content.BroadcastReceiver; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.content.SharedPreferences; 28 import android.content.SyncStatusObserver; 29 import android.content.pm.PackageManager; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.provider.ContactsContract; 33 import android.text.TextUtils; 34 import android.util.Log; 35 36 import androidx.core.content.ContextCompat; 37 import androidx.localbroadcastmanager.content.LocalBroadcastManager; 38 39 import com.android.contacts.R; 40 import com.android.contacts.list.ContactListFilterController; 41 import com.android.contacts.model.account.AccountInfo; 42 import com.android.contacts.model.account.AccountType; 43 import com.android.contacts.model.account.AccountTypeProvider; 44 import com.android.contacts.model.account.AccountTypeWithDataSet; 45 import com.android.contacts.model.account.AccountWithDataSet; 46 import com.android.contacts.model.account.FallbackAccountType; 47 import com.android.contacts.model.account.GoogleAccountType; 48 import com.android.contacts.model.account.SimAccountType; 49 import com.android.contacts.model.dataitem.DataKind; 50 import com.android.contacts.util.concurrent.ContactsExecutors; 51 52 import com.google.common.base.Function; 53 import com.google.common.base.Objects; 54 import com.google.common.base.Preconditions; 55 import com.google.common.base.Predicate; 56 import com.google.common.collect.Collections2; 57 import com.google.common.util.concurrent.FutureCallback; 58 import com.google.common.util.concurrent.Futures; 59 import com.google.common.util.concurrent.ListenableFuture; 60 import com.google.common.util.concurrent.ListeningExecutorService; 61 import com.google.common.util.concurrent.MoreExecutors; 62 63 import java.util.ArrayList; 64 import java.util.Collections; 65 import java.util.List; 66 import java.util.concurrent.Callable; 67 import java.util.concurrent.Executor; 68 69 import javax.annotation.Nullable; 70 71 /** 72 * Singleton holder for all parsed {@link AccountType} available on the 73 * system, typically filled through {@link PackageManager} queries. 74 */ 75 public abstract class AccountTypeManager { 76 static final String TAG = "AccountTypeManager"; 77 78 private static final Object mInitializationLock = new Object(); 79 private static AccountTypeManager mAccountTypeManager; 80 81 public static final String BROADCAST_ACCOUNTS_CHANGED = AccountTypeManager.class.getName() + 82 ".AccountsChanged"; 83 84 public enum AccountFilter implements Predicate<AccountInfo> { 85 ALL { 86 @Override apply(@ullable AccountInfo input)87 public boolean apply(@Nullable AccountInfo input) { 88 return input != null; 89 } 90 }, 91 CONTACTS_WRITABLE { 92 @Override apply(@ullable AccountInfo input)93 public boolean apply(@Nullable AccountInfo input) { 94 return input != null && input.getType().areContactsWritable(); 95 } 96 }, 97 DRAWER_DISPLAYABLE { 98 @Override apply(@ullable AccountInfo input)99 public boolean apply(@Nullable AccountInfo input) { 100 return input != null && ((input.getType() instanceof SimAccountType) 101 || input.getType().areContactsWritable()); 102 } 103 }, 104 GROUPS_WRITABLE { 105 @Override apply(@ullable AccountInfo input)106 public boolean apply(@Nullable AccountInfo input) { 107 return input != null && input.getType().isGroupMembershipEditable(); 108 } 109 }; 110 } 111 112 /** 113 * Requests the singleton instance of {@link AccountTypeManager} with data bound from 114 * the available authenticators. This method can safely be called from the UI thread. 115 */ getInstance(Context context)116 public static AccountTypeManager getInstance(Context context) { 117 if (!hasRequiredPermissions(context)) { 118 // Hopefully any component that depends on the values returned by this class 119 // will be restarted if the permissions change. 120 return EMPTY; 121 } 122 synchronized (mInitializationLock) { 123 if (mAccountTypeManager == null) { 124 context = context.getApplicationContext(); 125 mAccountTypeManager = new AccountTypeManagerImpl(context); 126 } 127 } 128 return mAccountTypeManager; 129 } 130 131 /** 132 * Set the instance of account type manager. This is only for and should only be used by unit 133 * tests. While having this method is not ideal, it's simpler than the alternative of 134 * holding this as a service in the ContactsApplication context class. 135 * 136 * @param mockManager The mock AccountTypeManager. 137 */ setInstanceForTest(AccountTypeManager mockManager)138 public static void setInstanceForTest(AccountTypeManager mockManager) { 139 synchronized (mInitializationLock) { 140 mAccountTypeManager = mockManager; 141 } 142 } 143 144 private static final AccountTypeManager EMPTY = new AccountTypeManager() { 145 146 @Override 147 public ListenableFuture<List<AccountInfo>> getAccountsAsync() { 148 return Futures.immediateFuture(Collections.<AccountInfo>emptyList()); 149 } 150 151 @Override 152 public ListenableFuture<List<AccountInfo>> filterAccountsAsync( 153 Predicate<AccountInfo> filter) { 154 return Futures.immediateFuture(Collections.<AccountInfo>emptyList()); 155 } 156 157 @Override 158 public AccountInfo getAccountInfoForAccount(AccountWithDataSet account) { 159 return null; 160 } 161 162 @Override 163 public Account getDefaultGoogleAccount() { 164 return null; 165 } 166 167 @Override 168 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) { 169 return null; 170 } 171 }; 172 173 /** 174 * Returns the list of all accounts (if contactWritableOnly is false) or just the list of 175 * contact writable accounts (if contactWritableOnly is true). 176 * 177 * <p>TODO(mhagerott) delete this method. It's left in place to prevent build breakages when 178 * this change is automerged. Usages of this method in downstream branches should be 179 * replaced with an asynchronous account loading pattern</p> 180 */ getAccounts(boolean contactWritableOnly)181 public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) { 182 return contactWritableOnly 183 ? blockForWritableAccounts() 184 : AccountInfo.extractAccounts(Futures.getUnchecked(getAccountsAsync())); 185 } 186 187 /** 188 * Returns all contact writable accounts 189 * 190 * <p>In general this method should be avoided. It exists to support some legacy usages of 191 * accounts in infrequently used features where refactoring to asynchronous loading is 192 * not justified. The chance that this will actually block is pretty low if the app has been 193 * launched previously</p> 194 */ blockForWritableAccounts()195 public List<AccountWithDataSet> blockForWritableAccounts() { 196 return AccountInfo.extractAccounts( 197 Futures.getUnchecked(filterAccountsAsync(AccountFilter.CONTACTS_WRITABLE))); 198 } 199 200 /** 201 * Loads accounts in background and returns future that will complete with list of all accounts 202 */ getAccountsAsync()203 public abstract ListenableFuture<List<AccountInfo>> getAccountsAsync(); 204 205 /** 206 * Loads accounts and applies the fitler returning only for which the predicate is true 207 */ filterAccountsAsync( Predicate<AccountInfo> filter)208 public abstract ListenableFuture<List<AccountInfo>> filterAccountsAsync( 209 Predicate<AccountInfo> filter); 210 getAccountInfoForAccount(AccountWithDataSet account)211 public abstract AccountInfo getAccountInfoForAccount(AccountWithDataSet account); 212 213 /** 214 * Returns the default google account. 215 */ getDefaultGoogleAccount()216 public abstract Account getDefaultGoogleAccount(); 217 218 /** 219 * Returns the Google Accounts. 220 * 221 * <p>This method exists in addition to filterAccountsByTypeAsync because it should be safe 222 * to call synchronously. 223 * </p> 224 */ getWritableGoogleAccounts()225 public List<AccountInfo> getWritableGoogleAccounts() { 226 // This implementation may block and should be overridden by the Impl class 227 return Futures.getUnchecked(filterAccountsAsync(new Predicate<AccountInfo>() { 228 @Override 229 public boolean apply(@Nullable AccountInfo input) { 230 return input.getType().areContactsWritable() && 231 GoogleAccountType.ACCOUNT_TYPE.equals(input.getType().accountType); 232 } 233 })); 234 } 235 236 /** 237 * Returns true if there are real accounts (not "local" account) in the list of accounts. 238 */ 239 public boolean hasNonLocalAccount() { 240 final List<AccountWithDataSet> allAccounts = 241 AccountInfo.extractAccounts(Futures.getUnchecked(getAccountsAsync())); 242 if (allAccounts == null || allAccounts.size() == 0) { 243 return false; 244 } 245 if (allAccounts.size() > 1) { 246 return true; 247 } 248 return !allAccounts.get(0).isNullAccount(); 249 } 250 251 static Account getDefaultGoogleAccount(AccountManager accountManager, 252 SharedPreferences prefs, String defaultAccountKey) { 253 // Get all the google accounts on the device 254 final Account[] accounts = accountManager.getAccountsByType( 255 GoogleAccountType.ACCOUNT_TYPE); 256 if (accounts == null || accounts.length == 0) { 257 return null; 258 } 259 260 // Get the default account from preferences 261 final String defaultAccount = prefs.getString(defaultAccountKey, null); 262 final AccountWithDataSet accountWithDataSet = defaultAccount == null ? null : 263 AccountWithDataSet.unstringify(defaultAccount); 264 265 // Look for an account matching the one from preferences 266 if (accountWithDataSet != null) { 267 for (int i = 0; i < accounts.length; i++) { 268 if (TextUtils.equals(accountWithDataSet.name, accounts[i].name) 269 && TextUtils.equals(accountWithDataSet.type, accounts[i].type)) { 270 return accounts[i]; 271 } 272 } 273 } 274 275 // Just return the first one 276 return accounts[0]; 277 } 278 279 public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet); 280 281 public final AccountType getAccountType(String accountType, String dataSet) { 282 return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet)); 283 } 284 285 public final AccountType getAccountTypeForAccount(AccountWithDataSet account) { 286 if (account != null) { 287 return getAccountType(account.getAccountTypeWithDataSet()); 288 } 289 return getAccountType(null, null); 290 } 291 292 /** 293 * Find the best {@link DataKind} matching the requested 294 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}. 295 * If no direct match found, we try searching {@link FallbackAccountType}. 296 */ 297 public DataKind getKindOrFallback(AccountType type, String mimeType) { 298 return type == null ? null : type.getKindForMimetype(mimeType); 299 } 300 301 /** 302 * Returns whether the specified account still exists 303 */ 304 public boolean exists(AccountWithDataSet account) { 305 final List<AccountWithDataSet> accounts = 306 AccountInfo.extractAccounts(Futures.getUnchecked(getAccountsAsync())); 307 return accounts.contains(account); 308 } 309 310 /** 311 * Returns whether the specified account is writable 312 * 313 * <p>This checks that the account still exists and that 314 * {@link AccountType#areContactsWritable()} is true</p> 315 */ 316 public boolean isWritable(AccountWithDataSet account) { 317 return exists(account) && getAccountInfoForAccount(account).getType().areContactsWritable(); 318 } 319 320 public boolean hasGoogleAccount() { 321 return getDefaultGoogleAccount() != null; 322 } 323 324 private static boolean hasRequiredPermissions(Context context) { 325 final boolean canGetAccounts = ContextCompat.checkSelfPermission(context, 326 android.Manifest.permission.GET_ACCOUNTS) == PackageManager.PERMISSION_GRANTED; 327 final boolean canReadContacts = ContextCompat.checkSelfPermission(context, 328 android.Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED; 329 return canGetAccounts && canReadContacts; 330 } 331 332 public static Predicate<AccountInfo> writableFilter() { 333 return AccountFilter.CONTACTS_WRITABLE; 334 } 335 336 public static Predicate<AccountInfo> drawerDisplayableFilter() { 337 return AccountFilter.DRAWER_DISPLAYABLE; 338 } 339 340 public static Predicate<AccountInfo> groupWritableFilter() { 341 return AccountFilter.GROUPS_WRITABLE; 342 } 343 } 344 345 class AccountTypeManagerImpl extends AccountTypeManager 346 implements OnAccountsUpdateListener, SyncStatusObserver { 347 348 private final Context mContext; 349 private final AccountManager mAccountManager; 350 private final DeviceLocalAccountLocator mLocalAccountLocator; 351 private final Executor mMainThreadExecutor; 352 private final ListeningExecutorService mExecutor; 353 private AccountTypeProvider mTypeProvider; 354 355 private final AccountType mFallbackAccountType; 356 357 private ListenableFuture<List<AccountWithDataSet>> mLocalAccountsFuture; 358 private ListenableFuture<List<AccountWithDataSet>> mSimAccountsFuture; 359 private ListenableFuture<AccountTypeProvider> mAccountTypesFuture; 360 361 private List<AccountWithDataSet> mLocalAccounts = new ArrayList<>(); 362 private List<AccountWithDataSet> mSimAccounts = new ArrayList<>(); 363 private List<AccountWithDataSet> mAccountManagerAccounts = new ArrayList<>(); 364 365 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 366 367 private final Function<AccountTypeProvider, List<AccountWithDataSet>> mAccountsExtractor = 368 new Function<AccountTypeProvider, List<AccountWithDataSet>>() { 369 @Nullable 370 @Override 371 public List<AccountWithDataSet> apply(@Nullable AccountTypeProvider typeProvider) { 372 return getAccountsWithDataSets(mAccountManager.getAccounts(), typeProvider); 373 } 374 }; 375 376 377 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 378 @Override 379 public void onReceive(Context context, Intent intent) { 380 // Don't use reloadAccountTypesIfNeeded when packages change in case a contacts.xml 381 // was updated. 382 reloadAccountTypes(); 383 } 384 }; 385 386 private final BroadcastReceiver mSimBroadcastReceiver = new BroadcastReceiver() { 387 @Override 388 public void onReceive(Context context, Intent intent) { 389 if (ContactsContract.SimContacts.ACTION_SIM_ACCOUNTS_CHANGED.equals( 390 intent.getAction())) { 391 reloadSimAccounts(); 392 } 393 } 394 }; 395 396 /** 397 * Internal constructor that only performs initial parsing. 398 */ 399 public AccountTypeManagerImpl(Context context) { 400 mContext = context; 401 mLocalAccountLocator = new DeviceLocalAccountLocator(context, AccountManager.get(context)); 402 mTypeProvider = new AccountTypeProvider(context); 403 mFallbackAccountType = new FallbackAccountType(context); 404 405 mAccountManager = AccountManager.get(mContext); 406 407 mExecutor = ContactsExecutors.getDefaultThreadPoolExecutor(); 408 mMainThreadExecutor = ContactsExecutors.newHandlerExecutor(mMainThreadHandler); 409 410 // Request updates when packages or accounts change 411 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 412 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 413 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 414 filter.addDataScheme("package"); 415 mContext.registerReceiver(mBroadcastReceiver, filter); 416 IntentFilter sdFilter = new IntentFilter(); 417 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); 418 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); 419 mContext.registerReceiver(mBroadcastReceiver, sdFilter); 420 421 // Request updates when locale is changed so that the order of each field will 422 // be able to be changed on the locale change. 423 filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); 424 mContext.registerReceiver(mBroadcastReceiver, filter); 425 426 IntentFilter simFilter = new IntentFilter( 427 ContactsContract.SimContacts.ACTION_SIM_ACCOUNTS_CHANGED); 428 mContext.registerReceiver(mSimBroadcastReceiver, simFilter); 429 430 mAccountManager.addOnAccountsUpdatedListener(this, mMainThreadHandler, false); 431 432 ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this); 433 434 loadAccountTypes(); 435 } 436 437 @Override 438 public void onStatusChanged(int which) { 439 reloadAccountTypesIfNeeded(); 440 } 441 442 /* This notification will arrive on the UI thread */ 443 public void onAccountsUpdated(Account[] accounts) { 444 reloadLocalAccounts(); 445 maybeNotifyAccountsUpdated(mAccountManagerAccounts, 446 getAccountsWithDataSets(accounts, mTypeProvider)); 447 } 448 449 private void maybeNotifyAccountsUpdated(List<AccountWithDataSet> current, 450 List<AccountWithDataSet> update) { 451 if (Objects.equal(current, update)) { 452 return; 453 } 454 current.clear(); 455 current.addAll(update); 456 notifyAccountsChanged(); 457 } 458 459 private void notifyAccountsChanged() { 460 ContactListFilterController.getInstance(mContext).checkFilterValidity(true); 461 LocalBroadcastManager.getInstance(mContext).sendBroadcast( 462 new Intent(BROADCAST_ACCOUNTS_CHANGED)); 463 } 464 465 private synchronized void startLoadingIfNeeded() { 466 if (mTypeProvider == null && mAccountTypesFuture == null) { 467 reloadAccountTypesIfNeeded(); 468 } 469 if (mLocalAccountsFuture == null) { 470 reloadLocalAccounts(); 471 } 472 if (mSimAccountsFuture == null) { 473 reloadSimAccounts(); 474 } 475 } 476 477 private synchronized void loadAccountTypes() { 478 mTypeProvider = new AccountTypeProvider(mContext); 479 480 mAccountTypesFuture = mExecutor.submit(new Callable<AccountTypeProvider>() { 481 @Override 482 public AccountTypeProvider call() throws Exception { 483 // This will request the AccountType for each Account forcing them to be loaded 484 getAccountsWithDataSets(mAccountManager.getAccounts(), mTypeProvider); 485 return mTypeProvider; 486 } 487 }); 488 } 489 490 private FutureCallback<List<AccountWithDataSet>> newAccountsUpdatedCallback( 491 final List<AccountWithDataSet> currentAccounts) { 492 return new FutureCallback<List<AccountWithDataSet>>() { 493 @Override 494 public void onSuccess(List<AccountWithDataSet> result) { 495 maybeNotifyAccountsUpdated(currentAccounts, result); 496 } 497 498 @Override 499 public void onFailure(Throwable t) { 500 } 501 }; 502 } 503 504 private synchronized void reloadAccountTypesIfNeeded() { 505 if (mTypeProvider == null || mTypeProvider.shouldUpdate( 506 mAccountManager.getAuthenticatorTypes(), ContentResolver.getSyncAdapterTypes())) { 507 reloadAccountTypes(); 508 } 509 } 510 511 private synchronized void reloadAccountTypes() { 512 loadAccountTypes(); 513 Futures.addCallback( 514 Futures.transform(mAccountTypesFuture, mAccountsExtractor, 515 MoreExecutors.directExecutor()), 516 newAccountsUpdatedCallback(mAccountManagerAccounts), 517 mMainThreadExecutor); 518 } 519 520 private synchronized void loadLocalAccounts() { 521 mLocalAccountsFuture = mExecutor.submit(new Callable<List<AccountWithDataSet>>() { 522 @Override 523 public List<AccountWithDataSet> call() throws Exception { 524 return mLocalAccountLocator.getDeviceLocalAccounts(); 525 } 526 }); 527 } 528 529 private synchronized void reloadLocalAccounts() { 530 loadLocalAccounts(); 531 Futures.addCallback(mLocalAccountsFuture, newAccountsUpdatedCallback(mLocalAccounts), 532 mMainThreadExecutor); 533 } 534 535 private synchronized void loadSimAccounts() { 536 mSimAccountsFuture = mExecutor.submit(new Callable<List<AccountWithDataSet>>() { 537 @Override 538 public List<AccountWithDataSet> call() throws Exception { 539 List<AccountWithDataSet> simAccountWithDataSets = new ArrayList<>(); 540 List<ContactsContract.SimAccount> simAccounts = 541 ContactsContract.SimContacts.getSimAccounts(mContext.getContentResolver()); 542 for (ContactsContract.SimAccount simAccount : simAccounts) { 543 simAccountWithDataSets.add(new AccountWithDataSet(simAccount.getAccountName(), 544 simAccount.getAccountType(), null)); 545 } 546 return simAccountWithDataSets; 547 } 548 }); 549 } 550 551 private synchronized void reloadSimAccounts() { 552 loadSimAccounts(); 553 Futures.addCallback(mSimAccountsFuture, newAccountsUpdatedCallback(mSimAccounts), 554 mMainThreadExecutor); 555 } 556 557 @Override 558 public ListenableFuture<List<AccountInfo>> getAccountsAsync() { 559 return getAllAccountsAsyncInternal(); 560 } 561 562 private synchronized ListenableFuture<List<AccountInfo>> getAllAccountsAsyncInternal() { 563 startLoadingIfNeeded(); 564 final AccountTypeProvider typeProvider = mTypeProvider; 565 final ListenableFuture<List<List<AccountWithDataSet>>> all = 566 Futures.nonCancellationPropagating( 567 Futures.successfulAsList( 568 Futures.transform(mAccountTypesFuture, mAccountsExtractor, 569 MoreExecutors.directExecutor()), 570 mLocalAccountsFuture, 571 mSimAccountsFuture)); 572 573 return Futures.transform(all, new Function<List<List<AccountWithDataSet>>, 574 List<AccountInfo>>() { 575 @Nullable 576 @Override 577 public List<AccountInfo> apply(@Nullable List<List<AccountWithDataSet>> input) { 578 // input.get(0) contains accounts from AccountManager 579 // input.get(1) contains device local accounts 580 // input.get(2) contains SIM accounts 581 Preconditions.checkArgument(input.size() == 3, 582 "List should have exactly 3 elements"); 583 584 final List<AccountInfo> result = new ArrayList<>(); 585 for (AccountWithDataSet account : input.get(0)) { 586 result.add( 587 typeProvider.getTypeForAccount(account).wrapAccount(mContext, account)); 588 } 589 590 for (AccountWithDataSet account : input.get(1)) { 591 result.add( 592 typeProvider.getTypeForAccount(account).wrapAccount(mContext, account)); 593 } 594 595 for (AccountWithDataSet account : input.get(2)) { 596 result.add( 597 typeProvider.getTypeForAccount(account).wrapAccount(mContext, account)); 598 } 599 AccountInfo.sortAccounts(null, result); 600 return result; 601 } 602 }, MoreExecutors.directExecutor()); 603 } 604 605 @Override 606 public ListenableFuture<List<AccountInfo>> filterAccountsAsync( 607 final Predicate<AccountInfo> filter) { 608 return Futures.transform(getAllAccountsAsyncInternal(), new Function<List<AccountInfo>, 609 List<AccountInfo>>() { 610 @Override 611 public List<AccountInfo> apply(List<AccountInfo> input) { 612 return new ArrayList<>(Collections2.filter(input, filter)); 613 } 614 }, mExecutor); 615 } 616 617 @Override 618 public AccountInfo getAccountInfoForAccount(AccountWithDataSet account) { 619 if (account == null) { 620 return null; 621 } 622 AccountType type = mTypeProvider.getTypeForAccount(account); 623 if (type == null) { 624 type = mFallbackAccountType; 625 } 626 return type.wrapAccount(mContext, account); 627 } 628 629 private List<AccountWithDataSet> getAccountsWithDataSets(Account[] accounts, 630 AccountTypeProvider typeProvider) { 631 List<AccountWithDataSet> result = new ArrayList<>(); 632 for (Account account : accounts) { 633 final List<AccountType> types = typeProvider.getAccountTypes(account.type); 634 for (AccountType type : types) { 635 result.add(new AccountWithDataSet( 636 account.name, account.type, type.dataSet)); 637 } 638 } 639 return result; 640 } 641 642 /** 643 * Returns the default google account specified in preferences, the first google account 644 * if it is not specified in preferences or is no longer on the device, and null otherwise. 645 */ 646 @Override 647 public Account getDefaultGoogleAccount() { 648 final SharedPreferences sharedPreferences = 649 mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE); 650 final String defaultAccountKey = 651 mContext.getResources().getString(R.string.contact_editor_default_account_key); 652 return getDefaultGoogleAccount(mAccountManager, sharedPreferences, defaultAccountKey); 653 } 654 655 @Override 656 public List<AccountInfo> getWritableGoogleAccounts() { 657 final Account[] googleAccounts = 658 mAccountManager.getAccountsByType(GoogleAccountType.ACCOUNT_TYPE); 659 final List<AccountInfo> result = new ArrayList<>(); 660 for (Account account : googleAccounts) { 661 final AccountWithDataSet accountWithDataSet = new AccountWithDataSet( 662 account.name, account.type, null); 663 final AccountType type = mTypeProvider.getTypeForAccount(accountWithDataSet); 664 if (type != null) { 665 // Accounts with a dataSet (e.g. Google plus accounts) are not writable. 666 result.add(type.wrapAccount(mContext, accountWithDataSet)); 667 } 668 } 669 return result; 670 } 671 672 /** 673 * Returns true if there are real accounts (not "local" account) in the list of accounts. 674 * 675 * <p>This is overriden for performance since the default implementation blocks until all 676 * accounts are loaded 677 * </p> 678 */ 679 @Override 680 public boolean hasNonLocalAccount() { 681 final Account[] accounts = mAccountManager.getAccounts(); 682 if (accounts == null) { 683 return false; 684 } 685 for (Account account : accounts) { 686 if (mTypeProvider.supportsContactsSyncing(account.type)) { 687 return true; 688 } 689 } 690 return false; 691 } 692 693 /** 694 * Find the best {@link DataKind} matching the requested 695 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}. 696 * If no direct match found, we try searching {@link FallbackAccountType}. 697 */ 698 @Override 699 public DataKind getKindOrFallback(AccountType type, String mimeType) { 700 DataKind kind = null; 701 702 // Try finding account type and kind matching request 703 if (type != null) { 704 kind = type.getKindForMimetype(mimeType); 705 } 706 707 if (kind == null) { 708 // Nothing found, so try fallback as last resort 709 kind = mFallbackAccountType.getKindForMimetype(mimeType); 710 } 711 712 if (kind == null) { 713 if (Log.isLoggable(TAG, Log.DEBUG)) { 714 Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType); 715 } 716 } 717 718 return kind; 719 } 720 721 /** 722 * Returns whether the account still exists on the device 723 * 724 * <p>This is overridden for performance. The default implementation loads all accounts then 725 * searches through them for specified. This implementation will only load the types for the 726 * specified AccountType (it may still require blocking on IO in some cases but it shouldn't 727 * be as bad as blocking for all accounts). 728 * </p> 729 */ 730 @Override 731 public boolean exists(AccountWithDataSet account) { 732 final Account[] accounts = mAccountManager.getAccountsByType(account.type); 733 for (Account existingAccount : accounts) { 734 if (existingAccount.name.equals(account.name)) { 735 return mTypeProvider.getTypeForAccount(account) != null; 736 } 737 } 738 return false; 739 } 740 741 /** 742 * Return {@link AccountType} for the given account type and data set. 743 */ 744 @Override 745 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) { 746 final AccountType type = mTypeProvider.getType( 747 accountTypeWithDataSet.accountType, accountTypeWithDataSet.dataSet); 748 return type != null ? type : mFallbackAccountType; 749 } 750 } 751