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