• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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