AccountTypeManager.java revision 3572293500a7406813e61e6fedd63ca606debc19
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.internal.util.Objects;
20import com.google.android.collect.Lists;
21import com.google.android.collect.Maps;
22import com.google.android.collect.Sets;
23import com.google.common.annotations.VisibleForTesting;
24import com.google.i18n.phonenumbers.PhoneNumberUtil;
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 account type strings to {@link AccountType}s which support
92     * the "invite" feature and have one or more account.
93     */
94    public abstract Map<String, 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<String, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
118    private Map<String, AccountType> mInvitableAccountTypes = Collections.unmodifiableMap(
119            new HashMap<String, 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<String, 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                    Log.d(TAG, "Registering extension package account type="
342                            + accountType.accountType + ", dataSet=" + accountType.dataSet
343                            + ", packageName=" + extensionPackage);
344
345                    addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
346                }
347            }
348        } catch (RemoteException e) {
349            Log.w(TAG, "Problem loading accounts: " + e.toString());
350        }
351
352        // Map in accounts to associate the account names with each account type entry.
353        Account[] accounts = mAccountManager.getAccounts();
354        for (Account account : accounts) {
355            boolean syncable = false;
356            try {
357                syncable = cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
358            } catch (RemoteException e) {
359                Log.e(TAG, "Cannot obtain sync flag for account: " + account, e);
360            }
361
362            if (syncable) {
363                List<AccountType> accountTypes = accountTypesByType.get(account.type);
364                if (accountTypes != null) {
365                    // Add an account-with-data-set entry for each account type that is
366                    // authenticated by this account.
367                    for (AccountType accountType : accountTypes) {
368                        AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
369                                account.name, account.type, accountType.dataSet);
370                        allAccounts.add(accountWithDataSet);
371                        if (!accountType.readOnly) {
372                            writableAccounts.add(accountWithDataSet);
373                        }
374                    }
375                }
376            }
377        }
378
379        Collections.sort(allAccounts, ACCOUNT_COMPARATOR);
380        Collections.sort(writableAccounts, ACCOUNT_COMPARATOR);
381
382        // The UI will need a phone number formatter.  We can preload meta data for the
383        // current locale to prevent a delay later on.
384        PhoneNumberUtil.getInstance().getAsYouTypeFormatter(Locale.getDefault().getCountry());
385
386        long endTime = SystemClock.currentThreadTimeMillis();
387
388        synchronized (this) {
389            mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
390            mAccounts = allAccounts;
391            mWritableAccounts = writableAccounts;
392            mInvitableAccountTypes = findInvitableAccountTypes(
393                    mContext, allAccounts, accountTypesByTypeAndDataSet);
394        }
395
396        Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, "
397                + mAccounts.size() + " accounts in " + (endTime - startTime) + "ms");
398
399        if (mInitializationLatch != null) {
400            mInitializationLatch.countDown();
401            mInitializationLatch = null;
402        }
403    }
404
405    // Bookkeeping method for tracking the known account types in the given maps.
406    private void addAccountType(AccountType accountType,
407            Map<String, AccountType> accountTypesByTypeAndDataSet,
408            Map<String, List<AccountType>> accountTypesByType) {
409        accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
410        List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
411        if (accountsForType == null) {
412            accountsForType = Lists.newArrayList();
413        }
414        accountsForType.add(accountType);
415        accountTypesByType.put(accountType.accountType, accountsForType);
416    }
417
418    /**
419     * Find a specific {@link AuthenticatorDescription} in the provided list
420     * that matches the given account type.
421     */
422    protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
423            String accountType) {
424        for (AuthenticatorDescription auth : auths) {
425            if (accountType.equals(auth.type)) {
426                return auth;
427            }
428        }
429        return null;
430    }
431
432    /**
433     * Return list of all known, writable {@link AccountWithDataSet}'s.
434     */
435    @Override
436    public List<AccountWithDataSet> getAccounts(boolean writableOnly) {
437        ensureAccountsLoaded();
438        return writableOnly ? mWritableAccounts : mAccounts;
439    }
440
441    /**
442     * Find the best {@link DataKind} matching the requested
443     * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}.
444     * If no direct match found, we try searching {@link FallbackAccountType}.
445     */
446    @Override
447    public DataKind getKindOrFallback(String accountType, String dataSet, String mimeType) {
448        ensureAccountsLoaded();
449        DataKind kind = null;
450
451        // Try finding account type and kind matching request
452        final AccountType type = mAccountTypesWithDataSets.get(
453                AccountType.getAccountTypeAndDataSet(accountType, dataSet));
454        if (type != null) {
455            kind = type.getKindForMimetype(mimeType);
456        }
457
458        if (kind == null) {
459            // Nothing found, so try fallback as last resort
460            kind = mFallbackAccountType.getKindForMimetype(mimeType);
461        }
462
463        if (kind == null) {
464            Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType);
465        }
466
467        return kind;
468    }
469
470    /**
471     * Return {@link AccountType} for the given account type and data set.
472     */
473    @Override
474    public AccountType getAccountType(String accountType, String dataSet) {
475        ensureAccountsLoaded();
476        synchronized (this) {
477            AccountType type = mAccountTypesWithDataSets.get(
478                    AccountType.getAccountTypeAndDataSet(accountType, dataSet));
479            return type != null ? type : mFallbackAccountType;
480        }
481    }
482
483    @Override
484    public Map<String, AccountType> getInvitableAccountTypes() {
485        return mInvitableAccountTypes;
486    }
487
488    /**
489     * Return all {@link AccountType}s with at least one account which supports "invite", i.e.
490     * its {@link AccountType#getInviteContactActivityClassName()} is not empty.
491     */
492    @VisibleForTesting
493    static Map<String, AccountType> findInvitableAccountTypes(Context context,
494            Collection<AccountWithDataSet> accounts,
495            Map<String, AccountType> accountTypesByTypeAndDataSet) {
496        HashMap<String, AccountType> result = Maps.newHashMap();
497        for (AccountWithDataSet account : accounts) {
498            String accountTypeWithDataSet = account.getAccountTypeWithDataSet();
499            AccountType type = accountTypesByTypeAndDataSet.get(
500                    account.getAccountTypeWithDataSet());
501            if (type == null) continue; // just in case
502            if (result.containsKey(accountTypeWithDataSet)) continue;
503
504            if (Log.isLoggable(TAG, Log.DEBUG)) {
505                Log.d(TAG, "Type " + accountTypeWithDataSet
506                        + " inviteClass=" + type.getInviteContactActivityClassName()
507                        + " inviteAction=" + type.getInviteContactActionLabel(context));
508            }
509            if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
510                result.put(accountTypeWithDataSet, type);
511            }
512        }
513        return Collections.unmodifiableMap(result);
514    }
515}
516