AccountTypeManager.java revision 49627cc3f606085d001397ebba93dcb52bf67a5c
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 com.android.contacts.util.Constants;
20import com.android.i18n.phonenumbers.PhoneNumberUtil;
21import com.android.internal.util.Objects;
22import com.google.android.collect.Lists;
23import com.google.android.collect.Maps;
24import com.google.android.collect.Sets;
25import com.google.common.annotations.VisibleForTesting;
26
27import android.accounts.Account;
28import android.accounts.AccountManager;
29import android.accounts.AuthenticatorDescription;
30import android.accounts.OnAccountsUpdateListener;
31import android.content.BroadcastReceiver;
32import android.content.ContentResolver;
33import android.content.Context;
34import android.content.IContentService;
35import android.content.Intent;
36import android.content.IntentFilter;
37import android.content.SyncAdapterType;
38import android.content.SyncStatusObserver;
39import android.content.pm.PackageManager;
40import android.os.Handler;
41import android.os.HandlerThread;
42import android.os.Message;
43import android.os.RemoteException;
44import android.os.SystemClock;
45import android.provider.ContactsContract;
46import android.text.TextUtils;
47import android.util.Log;
48
49import java.util.Collection;
50import java.util.Collections;
51import java.util.Comparator;
52import java.util.HashMap;
53import java.util.List;
54import java.util.Locale;
55import java.util.Map;
56import java.util.Set;
57import java.util.concurrent.CountDownLatch;
58
59/**
60 * Singleton holder for all parsed {@link AccountType} available on the
61 * system, typically filled through {@link PackageManager} queries.
62 */
63public abstract class AccountTypeManager {
64    static final String TAG = "AccountTypeManager";
65
66    public static final String ACCOUNT_TYPE_SERVICE = "contactAccountTypes";
67
68    /**
69     * Requests the singleton instance of {@link AccountTypeManager} with data bound from
70     * the available authenticators. This method can safely be called from the UI thread.
71     */
72    public static AccountTypeManager getInstance(Context context) {
73        context = context.getApplicationContext();
74        AccountTypeManager service =
75                (AccountTypeManager) context.getSystemService(ACCOUNT_TYPE_SERVICE);
76        if (service == null) {
77            service = createAccountTypeManager(context);
78            Log.e(TAG, "No account type service in context: " + context);
79        }
80        return service;
81    }
82
83    public static synchronized AccountTypeManager createAccountTypeManager(Context context) {
84        return new AccountTypeManagerImpl(context);
85    }
86
87    public abstract List<AccountWithDataSet> getAccounts(boolean writableOnly);
88
89    public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
90
91    public final AccountType getAccountType(String accountType, String dataSet) {
92        return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
93    }
94
95    public final AccountType getAccountTypeForAccount(AccountWithDataSet account) {
96        return getAccountType(account.getAccountTypeWithDataSet());
97    }
98
99    /**
100     * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
101     * which support the "invite" feature and have one or more account.
102     */
103    public abstract Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes();
104
105    /**
106     * Find the best {@link DataKind} matching the requested
107     * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}.
108     * If no direct match found, we try searching {@link FallbackAccountType}.
109     */
110    public DataKind getKindOrFallback(String accountType, String dataSet, String mimeType) {
111        final AccountType type = getAccountType(accountType, dataSet);
112        return type == null ? null : type.getKindForMimetype(mimeType);
113    }
114}
115
116class AccountTypeManagerImpl extends AccountTypeManager
117        implements OnAccountsUpdateListener, SyncStatusObserver {
118
119    private Context mContext;
120    private AccountManager mAccountManager;
121
122    private AccountType mFallbackAccountType;
123
124    private List<AccountWithDataSet> mAccounts = Lists.newArrayList();
125    private List<AccountWithDataSet> mWritableAccounts = Lists.newArrayList();
126    private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
127    private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
128            Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
129
130    private static final int MESSAGE_LOAD_DATA = 0;
131    private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
132
133    private HandlerThread mListenerThread;
134    private Handler mListenerHandler;
135
136    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
137
138        @Override
139        public void onReceive(Context context, Intent intent) {
140            Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
141            mListenerHandler.sendMessage(msg);
142        }
143
144    };
145
146    /* A latch that ensures that asynchronous initialization completes before data is used */
147    private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);
148
149    private static final Comparator<Account> ACCOUNT_COMPARATOR = new Comparator<Account>() {
150        @Override
151        public int compare(Account a, Account b) {
152            String aDataSet = null;
153            String bDataSet = null;
154            if (a instanceof AccountWithDataSet) {
155                aDataSet = ((AccountWithDataSet) a).dataSet;
156            }
157            if (b instanceof AccountWithDataSet) {
158                bDataSet = ((AccountWithDataSet) b).dataSet;
159            }
160
161            if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type)
162                    && Objects.equal(aDataSet, bDataSet)) {
163                return 0;
164            } else if (b.name == null || b.type == null) {
165                return -1;
166            } else if (a.name == null || a.type == null) {
167                return 1;
168            } else {
169                int diff = a.name.compareTo(b.name);
170                if (diff != 0) {
171                    return diff;
172                }
173                diff = a.type.compareTo(b.type);
174                if (diff != 0) {
175                    return diff;
176                }
177
178                // Accounts without data sets get sorted before those that have them.
179                if (aDataSet != null) {
180                    return bDataSet == null ? 1 : aDataSet.compareTo(bDataSet);
181                } else {
182                    return -1;
183                }
184            }
185        }
186    };
187
188    /**
189     * Internal constructor that only performs initial parsing.
190     */
191    public AccountTypeManagerImpl(Context context) {
192        mContext = context;
193        mFallbackAccountType = new FallbackAccountType(context);
194
195        mAccountManager = AccountManager.get(mContext);
196
197        mListenerThread = new HandlerThread("AccountChangeListener");
198        mListenerThread.start();
199        mListenerHandler = new Handler(mListenerThread.getLooper()) {
200            @Override
201            public void handleMessage(Message msg) {
202                switch (msg.what) {
203                    case MESSAGE_LOAD_DATA:
204                        loadAccountsInBackground();
205                        break;
206                    case MESSAGE_PROCESS_BROADCAST_INTENT:
207                        processBroadcastIntent((Intent) msg.obj);
208                        break;
209                }
210            }
211        };
212
213        // Request updates when packages or accounts change
214        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
215        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
216        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
217        filter.addDataScheme("package");
218        mContext.registerReceiver(mBroadcastReceiver, filter);
219        IntentFilter sdFilter = new IntentFilter();
220        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
221        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
222        mContext.registerReceiver(mBroadcastReceiver, sdFilter);
223
224        // Request updates when locale is changed so that the order of each field will
225        // be able to be changed on the locale change.
226        filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
227        mContext.registerReceiver(mBroadcastReceiver, filter);
228
229        mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
230
231        ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
232
233        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
234    }
235
236    @Override
237    public void onStatusChanged(int which) {
238        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
239    }
240
241    public void processBroadcastIntent(Intent intent) {
242        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
243    }
244
245    /* This notification will arrive on the background thread */
246    public void onAccountsUpdated(Account[] accounts) {
247        // Refresh to catch any changed accounts
248        loadAccountsInBackground();
249    }
250
251    /**
252     * Returns instantly if accounts and account types have already been loaded.
253     * Otherwise waits for the background thread to complete the loading.
254     */
255    void ensureAccountsLoaded() {
256        CountDownLatch latch = mInitializationLatch;
257        if (latch == null) {
258            return;
259        }
260        while (true) {
261            try {
262                latch.await();
263                return;
264            } catch (InterruptedException e) {
265                Thread.currentThread().interrupt();
266            }
267        }
268    }
269
270    /**
271     * Loads account list and corresponding account types (potentially with data sets). Always
272     * called on a background thread.
273     */
274    protected void loadAccountsInBackground() {
275        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
276            Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
277        }
278        long startTime = SystemClock.currentThreadTimeMillis();
279
280        // Account types, keyed off the account type and data set concatenation.
281        Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = Maps.newHashMap();
282
283        // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}.  Since there can
284        // be multiple account types (with different data sets) for the same type of account, each
285        // type string may have multiple AccountType entries.
286        Map<String, List<AccountType>> accountTypesByType = Maps.newHashMap();
287
288        List<AccountWithDataSet> allAccounts = Lists.newArrayList();
289        List<AccountWithDataSet> writableAccounts = Lists.newArrayList();
290        Set<String> extensionPackages = Sets.newHashSet();
291
292        final AccountManager am = mAccountManager;
293        final IContentService cs = ContentResolver.getContentService();
294
295        try {
296            final SyncAdapterType[] syncs = cs.getSyncAdapterTypes();
297            final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
298
299            // First process sync adapters to find any that provide contact data.
300            for (SyncAdapterType sync : syncs) {
301                if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
302                    // Skip sync adapters that don't provide contact data.
303                    continue;
304                }
305
306                // Look for the formatting details provided by each sync
307                // adapter, using the authenticator to find general resources.
308                final String type = sync.accountType;
309                final AuthenticatorDescription auth = findAuthenticator(auths, type);
310                if (auth == null) {
311                    Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
312                    continue;
313                }
314
315                AccountType accountType;
316                if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
317                    accountType = new GoogleAccountType(mContext, auth.packageName);
318                } else if (ExchangeAccountType.ACCOUNT_TYPE.equals(type)) {
319                    accountType = new ExchangeAccountType(mContext, auth.packageName);
320                } else {
321                    // TODO: use syncadapter package instead, since it provides resources
322                    Log.d(TAG, "Registering external account type=" + type
323                            + ", packageName=" + auth.packageName);
324                    accountType = new ExternalAccountType(mContext, auth.packageName, false);
325                    if (!((ExternalAccountType) accountType).isInitialized()) {
326                        // Skip external account types that couldn't be initialized.
327                        continue;
328                    }
329                    accountType.readOnly = !sync.supportsUploading();
330                }
331
332                accountType.accountType = auth.type;
333                accountType.titleRes = auth.labelId;
334                accountType.iconRes = auth.iconId;
335
336                addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
337
338                // Check to see if the account type knows of any other non-sync-adapter packages
339                // that may provide other data sets of contact data.
340                extensionPackages.addAll(accountType.getExtensionPackageNames());
341            }
342
343            // If any extension packages were specified, process them as well.
344            if (!extensionPackages.isEmpty()) {
345                Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages");
346                for (String extensionPackage : extensionPackages) {
347                    ExternalAccountType accountType =
348                            new ExternalAccountType(mContext, extensionPackage, true);
349                    if (!accountType.isInitialized()) {
350                        // Skip external account types that couldn't be initialized.
351                        continue;
352                    }
353                    if (!accountType.hasContactsMetadata()) {
354                        Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
355                                + " it doesn't have the CONTACTS_STRUCTURE metadata");
356                        continue;
357                    }
358                    if (TextUtils.isEmpty(accountType.accountType)) {
359                        Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
360                                + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
361                                + " attribute");
362                        continue;
363                    }
364                    Log.d(TAG, "Registering extension package account type="
365                            + accountType.accountType + ", dataSet=" + accountType.dataSet
366                            + ", packageName=" + extensionPackage);
367
368                    addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
369                }
370            }
371        } catch (RemoteException e) {
372            Log.w(TAG, "Problem loading accounts: " + e.toString());
373        }
374
375        // Map in accounts to associate the account names with each account type entry.
376        Account[] accounts = mAccountManager.getAccounts();
377        for (Account account : accounts) {
378            boolean syncable = false;
379            try {
380                syncable = cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
381            } catch (RemoteException e) {
382                Log.e(TAG, "Cannot obtain sync flag for account: " + account, e);
383            }
384
385            if (syncable) {
386                List<AccountType> accountTypes = accountTypesByType.get(account.type);
387                if (accountTypes != null) {
388                    // Add an account-with-data-set entry for each account type that is
389                    // authenticated by this account.
390                    for (AccountType accountType : accountTypes) {
391                        AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
392                                account.name, account.type, accountType.dataSet);
393                        allAccounts.add(accountWithDataSet);
394                        if (!accountType.readOnly) {
395                            writableAccounts.add(accountWithDataSet);
396                        }
397                    }
398                }
399            }
400        }
401
402        Collections.sort(allAccounts, ACCOUNT_COMPARATOR);
403        Collections.sort(writableAccounts, ACCOUNT_COMPARATOR);
404
405        // The UI will need a phone number formatter.  We can preload meta data for the
406        // current locale to prevent a delay later on.
407        PhoneNumberUtil.getInstance().getAsYouTypeFormatter(Locale.getDefault().getCountry());
408
409        long endTime = SystemClock.currentThreadTimeMillis();
410
411        synchronized (this) {
412            mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
413            mAccounts = allAccounts;
414            mWritableAccounts = writableAccounts;
415            mInvitableAccountTypes = findInvitableAccountTypes(
416                    mContext, allAccounts, accountTypesByTypeAndDataSet);
417        }
418
419        Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, "
420                + mAccounts.size() + " accounts in " + (endTime - startTime) + "ms");
421
422        if (mInitializationLatch != null) {
423            mInitializationLatch.countDown();
424            mInitializationLatch = null;
425        }
426        if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
427            Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
428        }
429    }
430
431    // Bookkeeping method for tracking the known account types in the given maps.
432    private void addAccountType(AccountType accountType,
433            Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
434            Map<String, List<AccountType>> accountTypesByType) {
435        accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
436        List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
437        if (accountsForType == null) {
438            accountsForType = Lists.newArrayList();
439        }
440        accountsForType.add(accountType);
441        accountTypesByType.put(accountType.accountType, accountsForType);
442    }
443
444    /**
445     * Find a specific {@link AuthenticatorDescription} in the provided list
446     * that matches the given account type.
447     */
448    protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
449            String accountType) {
450        for (AuthenticatorDescription auth : auths) {
451            if (accountType.equals(auth.type)) {
452                return auth;
453            }
454        }
455        return null;
456    }
457
458    /**
459     * Return list of all known, writable {@link AccountWithDataSet}'s.
460     */
461    @Override
462    public List<AccountWithDataSet> getAccounts(boolean writableOnly) {
463        ensureAccountsLoaded();
464        return writableOnly ? mWritableAccounts : mAccounts;
465    }
466
467    /**
468     * Find the best {@link DataKind} matching the requested
469     * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}.
470     * If no direct match found, we try searching {@link FallbackAccountType}.
471     */
472    @Override
473    public DataKind getKindOrFallback(String accountType, String dataSet, String mimeType) {
474        ensureAccountsLoaded();
475        DataKind kind = null;
476
477        // Try finding account type and kind matching request
478        final AccountType type = mAccountTypesWithDataSets.get(
479                AccountTypeWithDataSet.get(accountType, dataSet));
480        if (type != null) {
481            kind = type.getKindForMimetype(mimeType);
482        }
483
484        if (kind == null) {
485            // Nothing found, so try fallback as last resort
486            kind = mFallbackAccountType.getKindForMimetype(mimeType);
487        }
488
489        if (kind == null) {
490            Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType);
491        }
492
493        return kind;
494    }
495
496    /**
497     * Return {@link AccountType} for the given account type and data set.
498     */
499    @Override
500    public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
501        ensureAccountsLoaded();
502        synchronized (this) {
503            AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
504            return type != null ? type : mFallbackAccountType;
505        }
506    }
507
508    @Override
509    public Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes() {
510        return mInvitableAccountTypes;
511    }
512
513    /**
514     * Return all {@link AccountType}s with at least one account which supports "invite", i.e.
515     * its {@link AccountType#getInviteContactActivityClassName()} is not empty.
516     */
517    @VisibleForTesting
518    static Map<AccountTypeWithDataSet, AccountType> findInvitableAccountTypes(Context context,
519            Collection<AccountWithDataSet> accounts,
520            Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
521        HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
522        for (AccountWithDataSet account : accounts) {
523            AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet();
524            AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
525            if (type == null) continue; // just in case
526            if (result.containsKey(accountTypeWithDataSet)) continue;
527
528            if (Log.isLoggable(TAG, Log.DEBUG)) {
529                Log.d(TAG, "Type " + accountTypeWithDataSet
530                        + " inviteClass=" + type.getInviteContactActivityClassName()
531                        + " inviteAction=" + type.getInviteContactActionLabel(context));
532            }
533            if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
534                result.put(accountTypeWithDataSet, type);
535            }
536        }
537        return Collections.unmodifiableMap(result);
538    }
539}
540