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