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