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