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