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