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
17package com.android.contacts.model;
18
19import android.accounts.Account;
20import android.accounts.AccountManager;
21import android.accounts.OnAccountsUpdateListener;
22import android.content.BroadcastReceiver;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.content.SharedPreferences;
28import android.content.SyncStatusObserver;
29import android.content.pm.PackageManager;
30import android.database.ContentObserver;
31import android.net.Uri;
32import android.os.Handler;
33import android.os.Looper;
34import android.provider.ContactsContract;
35import android.support.v4.content.ContextCompat;
36import android.support.v4.content.LocalBroadcastManager;
37import android.text.TextUtils;
38import android.util.Log;
39
40import com.android.contacts.Experiments;
41import com.android.contacts.R;
42import com.android.contacts.list.ContactListFilterController;
43import com.android.contacts.model.account.AccountInfo;
44import com.android.contacts.model.account.AccountType;
45import com.android.contacts.model.account.AccountTypeProvider;
46import com.android.contacts.model.account.AccountTypeWithDataSet;
47import com.android.contacts.model.account.AccountWithDataSet;
48import com.android.contacts.model.account.FallbackAccountType;
49import com.android.contacts.model.account.GoogleAccountType;
50import com.android.contacts.model.dataitem.DataKind;
51import com.android.contacts.util.concurrent.ContactsExecutors;
52import com.android.contactsbind.experiments.Flags;
53import com.google.common.base.Preconditions;
54import com.google.common.base.Function;
55import com.google.common.base.Objects;
56import com.google.common.base.Predicate;
57import com.google.common.collect.Collections2;
58import com.google.common.util.concurrent.FutureCallback;
59import com.google.common.util.concurrent.Futures;
60import com.google.common.util.concurrent.ListenableFuture;
61import com.google.common.util.concurrent.ListeningExecutorService;
62
63import java.util.ArrayList;
64import java.util.Collections;
65import java.util.List;
66import java.util.concurrent.Callable;
67import java.util.concurrent.Executor;
68
69import 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 */
75public 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
334class 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