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