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 if (Flags.getInstance().getBoolean(Experiments.CP2_DEVICE_ACCOUNT_DETECTION_ENABLED)) { 409 // Observe changes to RAW_CONTACTS so that we will update the list of "Device" accounts 410 // if a new device contact is added. 411 mContext.getContentResolver().registerContentObserver( 412 ContactsContract.RawContacts.CONTENT_URI, /* notifyDescendents */ true, 413 new ContentObserver(mMainThreadHandler) { 414 @Override 415 public boolean deliverSelfNotifications() { 416 return true; 417 } 418 419 @Override 420 public void onChange(boolean selfChange) { 421 reloadLocalAccounts(); 422 } 423 424 @Override 425 public void onChange(boolean selfChange, Uri uri) { 426 reloadLocalAccounts(); 427 } 428 }); 429 } 430 loadAccountTypes(); 431 } 432 433 @Override 434 public void onStatusChanged(int which) { 435 reloadAccountTypesIfNeeded(); 436 } 437 438 /* This notification will arrive on the UI thread */ 439 public void onAccountsUpdated(Account[] accounts) { 440 reloadLocalAccounts(); 441 maybeNotifyAccountsUpdated(mAccountManagerAccounts, 442 getAccountsWithDataSets(accounts, mTypeProvider)); 443 } 444 445 private void maybeNotifyAccountsUpdated(List<AccountWithDataSet> current, 446 List<AccountWithDataSet> update) { 447 if (Objects.equal(current, update)) { 448 return; 449 } 450 current.clear(); 451 current.addAll(update); 452 notifyAccountsChanged(); 453 } 454 455 private void notifyAccountsChanged() { 456 ContactListFilterController.getInstance(mContext).checkFilterValidity(true); 457 LocalBroadcastManager.getInstance(mContext).sendBroadcast( 458 new Intent(BROADCAST_ACCOUNTS_CHANGED)); 459 } 460 461 private synchronized void startLoadingIfNeeded() { 462 if (mTypeProvider == null && mAccountTypesFuture == null) { 463 reloadAccountTypesIfNeeded(); 464 } 465 if (mLocalAccountsFuture == null) { 466 reloadLocalAccounts(); 467 } 468 } 469 470 private synchronized void loadAccountTypes() { 471 mTypeProvider = new AccountTypeProvider(mContext); 472 473 mAccountTypesFuture = mExecutor.submit(new Callable<AccountTypeProvider>() { 474 @Override 475 public AccountTypeProvider call() throws Exception { 476 // This will request the AccountType for each Account forcing them to be loaded 477 getAccountsWithDataSets(mAccountManager.getAccounts(), mTypeProvider); 478 return mTypeProvider; 479 } 480 }); 481 } 482 483 private FutureCallback<List<AccountWithDataSet>> newAccountsUpdatedCallback( 484 final List<AccountWithDataSet> currentAccounts) { 485 return new FutureCallback<List<AccountWithDataSet>>() { 486 @Override 487 public void onSuccess(List<AccountWithDataSet> result) { 488 maybeNotifyAccountsUpdated(currentAccounts, result); 489 } 490 491 @Override 492 public void onFailure(Throwable t) { 493 } 494 }; 495 } 496 497 private synchronized void reloadAccountTypesIfNeeded() { 498 if (mTypeProvider == null || mTypeProvider.shouldUpdate( 499 mAccountManager.getAuthenticatorTypes(), ContentResolver.getSyncAdapterTypes())) { 500 reloadAccountTypes(); 501 } 502 } 503 504 private synchronized void reloadAccountTypes() { 505 loadAccountTypes(); 506 Futures.addCallback( 507 Futures.transform(mAccountTypesFuture, mAccountsExtractor, 508 MoreExecutors.directExecutor()), 509 newAccountsUpdatedCallback(mAccountManagerAccounts), 510 mMainThreadExecutor); 511 } 512 513 private synchronized void loadLocalAccounts() { 514 mLocalAccountsFuture = mExecutor.submit(new Callable<List<AccountWithDataSet>>() { 515 @Override 516 public List<AccountWithDataSet> call() throws Exception { 517 return mLocalAccountLocator.getDeviceLocalAccounts(); 518 } 519 }); 520 } 521 522 private synchronized void reloadLocalAccounts() { 523 loadLocalAccounts(); 524 Futures.addCallback(mLocalAccountsFuture, newAccountsUpdatedCallback(mLocalAccounts), 525 mMainThreadExecutor); 526 } 527 528 @Override 529 public ListenableFuture<List<AccountInfo>> getAccountsAsync() { 530 return getAllAccountsAsyncInternal(); 531 } 532 533 private synchronized ListenableFuture<List<AccountInfo>> getAllAccountsAsyncInternal() { 534 startLoadingIfNeeded(); 535 final AccountTypeProvider typeProvider = mTypeProvider; 536 final ListenableFuture<List<List<AccountWithDataSet>>> all = 537 Futures.nonCancellationPropagating( 538 Futures.successfulAsList( 539 Futures.transform(mAccountTypesFuture, mAccountsExtractor, 540 MoreExecutors.directExecutor()), 541 mLocalAccountsFuture)); 542 543 return Futures.transform(all, new Function<List<List<AccountWithDataSet>>, 544 List<AccountInfo>>() { 545 @Nullable 546 @Override 547 public List<AccountInfo> apply(@Nullable List<List<AccountWithDataSet>> input) { 548 // input.get(0) contains accounts from AccountManager 549 // input.get(1) contains device local accounts 550 Preconditions.checkArgument(input.size() == 2, 551 "List should have exactly 2 elements"); 552 553 final List<AccountInfo> result = new ArrayList<>(); 554 for (AccountWithDataSet account : input.get(0)) { 555 result.add( 556 typeProvider.getTypeForAccount(account).wrapAccount(mContext, account)); 557 } 558 559 for (AccountWithDataSet account : input.get(1)) { 560 result.add( 561 typeProvider.getTypeForAccount(account).wrapAccount(mContext, account)); 562 } 563 AccountInfo.sortAccounts(null, result); 564 return result; 565 } 566 }, MoreExecutors.directExecutor()); 567 } 568 569 @Override 570 public ListenableFuture<List<AccountInfo>> filterAccountsAsync( 571 final Predicate<AccountInfo> filter) { 572 return Futures.transform(getAllAccountsAsyncInternal(), new Function<List<AccountInfo>, 573 List<AccountInfo>>() { 574 @Override 575 public List<AccountInfo> apply(List<AccountInfo> input) { 576 return new ArrayList<>(Collections2.filter(input, filter)); 577 } 578 }, mExecutor); 579 } 580 581 @Override 582 public AccountInfo getAccountInfoForAccount(AccountWithDataSet account) { 583 if (account == null) { 584 return null; 585 } 586 AccountType type = mTypeProvider.getTypeForAccount(account); 587 if (type == null) { 588 type = mFallbackAccountType; 589 } 590 return type.wrapAccount(mContext, account); 591 } 592 593 private List<AccountWithDataSet> getAccountsWithDataSets(Account[] accounts, 594 AccountTypeProvider typeProvider) { 595 List<AccountWithDataSet> result = new ArrayList<>(); 596 for (Account account : accounts) { 597 final List<AccountType> types = typeProvider.getAccountTypes(account.type); 598 for (AccountType type : types) { 599 result.add(new AccountWithDataSet( 600 account.name, account.type, type.dataSet)); 601 } 602 } 603 return result; 604 } 605 606 /** 607 * Returns the default google account specified in preferences, the first google account 608 * if it is not specified in preferences or is no longer on the device, and null otherwise. 609 */ 610 @Override 611 public Account getDefaultGoogleAccount() { 612 final SharedPreferences sharedPreferences = 613 mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE); 614 final String defaultAccountKey = 615 mContext.getResources().getString(R.string.contact_editor_default_account_key); 616 return getDefaultGoogleAccount(mAccountManager, sharedPreferences, defaultAccountKey); 617 } 618 619 @Override 620 public List<AccountInfo> getWritableGoogleAccounts() { 621 final Account[] googleAccounts = 622 mAccountManager.getAccountsByType(GoogleAccountType.ACCOUNT_TYPE); 623 final List<AccountInfo> result = new ArrayList<>(); 624 for (Account account : googleAccounts) { 625 final AccountWithDataSet accountWithDataSet = new AccountWithDataSet( 626 account.name, account.type, null); 627 final AccountType type = mTypeProvider.getTypeForAccount(accountWithDataSet); 628 if (type != null) { 629 // Accounts with a dataSet (e.g. Google plus accounts) are not writable. 630 result.add(type.wrapAccount(mContext, accountWithDataSet)); 631 } 632 } 633 return result; 634 } 635 636 /** 637 * Returns true if there are real accounts (not "local" account) in the list of accounts. 638 * 639 * <p>This is overriden for performance since the default implementation blocks until all 640 * accounts are loaded 641 * </p> 642 */ 643 @Override 644 public boolean hasNonLocalAccount() { 645 final Account[] accounts = mAccountManager.getAccounts(); 646 if (accounts == null) { 647 return false; 648 } 649 for (Account account : accounts) { 650 if (mTypeProvider.supportsContactsSyncing(account.type)) { 651 return true; 652 } 653 } 654 return false; 655 } 656 657 /** 658 * Find the best {@link DataKind} matching the requested 659 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}. 660 * If no direct match found, we try searching {@link FallbackAccountType}. 661 */ 662 @Override 663 public DataKind getKindOrFallback(AccountType type, String mimeType) { 664 DataKind kind = null; 665 666 // Try finding account type and kind matching request 667 if (type != null) { 668 kind = type.getKindForMimetype(mimeType); 669 } 670 671 if (kind == null) { 672 // Nothing found, so try fallback as last resort 673 kind = mFallbackAccountType.getKindForMimetype(mimeType); 674 } 675 676 if (kind == null) { 677 if (Log.isLoggable(TAG, Log.DEBUG)) { 678 Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType); 679 } 680 } 681 682 return kind; 683 } 684 685 /** 686 * Returns whether the account still exists on the device 687 * 688 * <p>This is overridden for performance. The default implementation loads all accounts then 689 * searches through them for specified. This implementation will only load the types for the 690 * specified AccountType (it may still require blocking on IO in some cases but it shouldn't 691 * be as bad as blocking for all accounts). 692 * </p> 693 */ 694 @Override 695 public boolean exists(AccountWithDataSet account) { 696 final Account[] accounts = mAccountManager.getAccountsByType(account.type); 697 for (Account existingAccount : accounts) { 698 if (existingAccount.name.equals(account.name)) { 699 return mTypeProvider.getTypeForAccount(account) != null; 700 } 701 } 702 return false; 703 } 704 705 /** 706 * Return {@link AccountType} for the given account type and data set. 707 */ 708 @Override 709 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) { 710 final AccountType type = mTypeProvider.getType( 711 accountTypeWithDataSet.accountType, accountTypeWithDataSet.dataSet); 712 return type != null ? type : mFallbackAccountType; 713 } 714 } 715