AccountTypeManager.java revision 179c9960e50019608d91661cfbcbb3cc8bc48093
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.model.AccountType.DataKind;
20import com.google.android.collect.Lists;
21import com.google.android.collect.Maps;
22import com.google.i18n.phonenumbers.PhoneNumberUtil;
23
24import android.accounts.Account;
25import android.accounts.AccountManager;
26import android.accounts.AuthenticatorDescription;
27import android.accounts.OnAccountsUpdateListener;
28import android.content.BroadcastReceiver;
29import android.content.ContentResolver;
30import android.content.Context;
31import android.content.IContentService;
32import android.content.Intent;
33import android.content.IntentFilter;
34import android.content.SyncAdapterType;
35import android.content.SyncStatusObserver;
36import android.content.pm.PackageManager;
37import android.os.Handler;
38import android.os.HandlerThread;
39import android.os.Message;
40import android.os.RemoteException;
41import android.os.SystemClock;
42import android.provider.ContactsContract;
43import android.util.Log;
44
45import java.util.ArrayList;
46import java.util.Collections;
47import java.util.Comparator;
48import java.util.HashMap;
49import java.util.Locale;
50import java.util.concurrent.CountDownLatch;
51
52/**
53 * Singleton holder for all parsed {@link AccountType} available on the
54 * system, typically filled through {@link PackageManager} queries.
55 */
56public abstract class AccountTypeManager {
57    static final String TAG = "AccountTypeManager";
58
59    public static final String ACCOUNT_TYPE_SERVICE = "contactAccountTypes";
60
61    /**
62     * Requests the singleton instance of {@link AccountTypeManager} with data bound from
63     * the available authenticators. This method can safely be called from the UI thread.
64     */
65    public static AccountTypeManager getInstance(Context context) {
66        AccountTypeManager service =
67                (AccountTypeManager) context.getSystemService(ACCOUNT_TYPE_SERVICE);
68        if (service == null) {
69            service = createAccountTypeManager(context);
70            Log.e(TAG, "No account type service in context: " + context);
71        }
72        return service;
73    }
74
75    public static synchronized AccountTypeManager createAccountTypeManager(Context context) {
76        return new AccountTypeManagerImpl(context);
77    }
78
79    public abstract ArrayList<Account> getAccounts(boolean writableOnly);
80
81    public abstract AccountType getAccountType(String accountType);
82
83    /**
84     * Find the best {@link DataKind} matching the requested
85     * {@link AccountType#accountType} and {@link DataKind#mimeType}. If no
86     * direct match found, we try searching {@link FallbackAccountType}.
87     */
88    public DataKind getKindOrFallback(String accountType, String mimeType) {
89        final AccountType type = getAccountType(accountType);
90        return type == null ? null : type.getKindForMimetype(mimeType);
91    }
92}
93
94class AccountTypeManagerImpl extends AccountTypeManager
95        implements OnAccountsUpdateListener, SyncStatusObserver {
96
97    private Context mContext;
98    private AccountManager mAccountManager;
99
100    private AccountType mFallbackAccountType = new FallbackAccountType();
101
102    private ArrayList<Account> mAccounts = Lists.newArrayList();
103    private ArrayList<Account> mWritableAccounts = Lists.newArrayList();
104    private HashMap<String, AccountType> mAccountTypes = Maps.newHashMap();
105
106    private static final int MESSAGE_LOAD_DATA = 0;
107    private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
108
109    private HandlerThread mListenerThread;
110    private Handler mListenerHandler;
111
112    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
113
114        @Override
115        public void onReceive(Context context, Intent intent) {
116            Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
117            mListenerHandler.sendMessage(msg);
118        }
119
120    };
121
122    /* A latch that ensures that asynchronous initialization completes before data is used */
123    private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);
124
125    private static final Comparator<Account> ACCOUNT_COMPARATOR = new Comparator<Account>() {
126
127        @Override
128        public int compare(Account account1, Account account2) {
129            int diff = account1.name.compareTo(account2.name);
130            if (diff != 0) {
131                return diff;
132            }
133            return account1.type.compareTo(account2.type);
134        }
135    };
136
137    /**
138     * Internal constructor that only performs initial parsing.
139     */
140    public AccountTypeManagerImpl(Context context) {
141        mContext = context;
142        mAccountManager = AccountManager.get(mContext);
143
144        mListenerThread = new HandlerThread("AccountChangeListener");
145        mListenerThread.start();
146        mListenerHandler = new Handler(mListenerThread.getLooper()) {
147            @Override
148            public void handleMessage(Message msg) {
149                switch (msg.what) {
150                    case MESSAGE_LOAD_DATA:
151                        loadAccountsInBackground();
152                        break;
153                    case MESSAGE_PROCESS_BROADCAST_INTENT:
154                        processBroadcastIntent((Intent) msg.obj);
155                        break;
156                }
157            }
158        };
159
160        // Request updates when packages or accounts change
161        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
162        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
163        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
164        filter.addDataScheme("package");
165        mContext.registerReceiver(mBroadcastReceiver, filter);
166        IntentFilter sdFilter = new IntentFilter();
167        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
168        sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
169        mContext.registerReceiver(mBroadcastReceiver, sdFilter);
170
171        // Request updates when locale is changed so that the order of each field will
172        // be able to be changed on the locale change.
173        filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
174        mContext.registerReceiver(mBroadcastReceiver, filter);
175
176        mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
177
178        ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
179
180        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
181    }
182
183    @Override
184    public void onStatusChanged(int which) {
185        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
186    }
187
188    public void processBroadcastIntent(Intent intent) {
189        mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
190    }
191
192    /* This notification will arrive on the background thread */
193    public void onAccountsUpdated(Account[] accounts) {
194        // Refresh to catch any changed accounts
195        loadAccountsInBackground();
196    }
197
198    /**
199     * Returns instantly if accounts and account types have already been loaded.
200     * Otherwise waits for the background thread to complete the loading.
201     */
202    void ensureAccountsLoaded() {
203        CountDownLatch latch = mInitializationLatch;
204        if (latch == null) {
205            return;
206        }
207        while (true) {
208            try {
209                latch.await();
210                return;
211            } catch (InterruptedException e) {
212                Thread.currentThread().interrupt();
213            }
214        }
215    }
216
217    /**
218     * Loads account list and corresponding account types. Always called on a
219     * background thread.
220     */
221    protected void loadAccountsInBackground() {
222        long startTime = SystemClock.currentThreadTimeMillis();
223
224        HashMap<String, AccountType> accountTypes = Maps.newHashMap();
225        ArrayList<Account> allAccounts = Lists.newArrayList();
226        ArrayList<Account> writableAccounts = Lists.newArrayList();
227
228        final AccountManager am = mAccountManager;
229        final IContentService cs = ContentResolver.getContentService();
230
231        try {
232            final SyncAdapterType[] syncs = cs.getSyncAdapterTypes();
233            final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
234
235            for (SyncAdapterType sync : syncs) {
236                if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
237                    // Skip sync adapters that don't provide contact data.
238                    continue;
239                }
240
241                // Look for the formatting details provided by each sync
242                // adapter, using the authenticator to find general resources.
243                final String type = sync.accountType;
244                final AuthenticatorDescription auth = findAuthenticator(auths, type);
245                if (auth == null) {
246                    Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
247                    continue;
248                }
249
250                AccountType accountType;
251                if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
252                    accountType = new GoogleAccountType(mContext, auth.packageName);
253                } else if (ExchangeAccountType.ACCOUNT_TYPE.equals(type)) {
254                    accountType = new ExchangeAccountType(mContext, auth.packageName);
255                } else {
256                    // TODO: use syncadapter package instead, since it provides resources
257                    Log.d(TAG, "Registering external account type=" + type
258                            + ", packageName=" + auth.packageName);
259                    accountType = new ExternalAccountType(mContext, auth.packageName);
260                    accountType.readOnly = !sync.supportsUploading();
261                }
262
263                accountType.accountType = auth.type;
264                accountType.titleRes = auth.labelId;
265                accountType.iconRes = auth.iconId;
266
267                accountTypes.put(accountType.accountType, accountType);
268            }
269        } catch (RemoteException e) {
270            Log.w(TAG, "Problem loading accounts: " + e.toString());
271        }
272
273        Account[] accounts = mAccountManager.getAccounts();
274        for (Account account : accounts) {
275            boolean syncable = false;
276            try {
277                int isSyncable = cs.getIsSyncable(account, ContactsContract.AUTHORITY);
278                if (isSyncable > 0) {
279                    syncable = true;
280                }
281            } catch (RemoteException e) {
282                Log.e(TAG, "Cannot obtain sync flag for account: " + account, e);
283            }
284
285            if (syncable) {
286                // Ensure we have details loaded for each account
287                final AccountType accountType = accountTypes.get(account.type);
288                if (accountType != null) {
289                    allAccounts.add(account);
290                    if (!accountType.readOnly) {
291                        writableAccounts.add(account);
292                    }
293                }
294            }
295        }
296
297        Collections.sort(allAccounts, ACCOUNT_COMPARATOR);
298        Collections.sort(writableAccounts, ACCOUNT_COMPARATOR);
299
300        // The UI will need a phone number formatter.  We can preload meta data for the
301        // current locale to prevent a delay later on.
302        PhoneNumberUtil.getInstance().getAsYouTypeFormatter(Locale.getDefault().getCountry());
303
304        long endTime = SystemClock.currentThreadTimeMillis();
305
306        synchronized (this) {
307            mAccountTypes = accountTypes;
308            mAccounts = allAccounts;
309            mWritableAccounts = writableAccounts;
310        }
311
312        Log.i(TAG, "Loaded meta-data for " + mAccountTypes.size() + " account types, "
313                + mAccounts.size() + " accounts in " + (endTime - startTime) + "ms");
314
315        if (mInitializationLatch != null) {
316            mInitializationLatch.countDown();
317            mInitializationLatch = null;
318        }
319    }
320
321    /**
322     * Find a specific {@link AuthenticatorDescription} in the provided list
323     * that matches the given account type.
324     */
325    protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
326            String accountType) {
327        for (AuthenticatorDescription auth : auths) {
328            if (accountType.equals(auth.type)) {
329                return auth;
330            }
331        }
332        return null;
333    }
334
335    /**
336     * Return list of all known, writable {@link Account}'s.
337     */
338    @Override
339    public ArrayList<Account> getAccounts(boolean writableOnly) {
340        ensureAccountsLoaded();
341        return writableOnly ? mWritableAccounts : mAccounts;
342    }
343
344    /**
345     * Find the best {@link DataKind} matching the requested
346     * {@link AccountType#accountType} and {@link DataKind#mimeType}. If no
347     * direct match found, we try searching {@link FallbackAccountType}.
348     */
349    @Override
350    public DataKind getKindOrFallback(String accountType, String mimeType) {
351        ensureAccountsLoaded();
352        DataKind kind = null;
353
354        // Try finding account type and kind matching request
355        final AccountType type = mAccountTypes.get(accountType);
356        if (type != null) {
357            kind = type.getKindForMimetype(mimeType);
358        }
359
360        if (kind == null) {
361            // Nothing found, so try fallback as last resort
362            kind = mFallbackAccountType.getKindForMimetype(mimeType);
363        }
364
365        if (kind == null) {
366            Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType);
367        }
368
369        return kind;
370    }
371
372    /**
373     * Return {@link AccountType} for the given account type.
374     */
375    @Override
376    public AccountType getAccountType(String accountType) {
377        ensureAccountsLoaded();
378        synchronized (this) {
379            AccountType type = mAccountTypes.get(accountType);
380            return type != null ? type : mFallbackAccountType;
381        }
382    }
383}
384