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