blob: a08321999a986b5e9e1d2a4221daf935a761dd37 [file] [log] [blame]
Chiao Cheng6c712f42012-11-26 15:35:28 -08001/*
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.common.model;
18
19import android.accounts.Account;
Ihab Awad413589f2014-07-02 14:00:28 -070020import android.accounts.AccountManager;
Chiao Cheng6c712f42012-11-26 15:35:28 -080021import android.accounts.AuthenticatorDescription;
22import android.accounts.OnAccountsUpdateListener;
23import android.content.BroadcastReceiver;
24import android.content.ContentResolver;
25import android.content.Context;
Chiao Cheng6c712f42012-11-26 15:35:28 -080026import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.SyncAdapterType;
29import android.content.SyncStatusObserver;
30import android.content.pm.PackageManager;
31import android.content.pm.ResolveInfo;
Marcus Hagerottfac695a2016-08-24 17:02:40 -070032import android.database.ContentObserver;
Chiao Cheng6c712f42012-11-26 15:35:28 -080033import android.net.Uri;
34import android.os.AsyncTask;
35import android.os.Handler;
36import android.os.HandlerThread;
37import android.os.Looper;
38import android.os.Message;
Chiao Cheng6c712f42012-11-26 15:35:28 -080039import android.os.SystemClock;
40import android.provider.ContactsContract;
Marcus Hagerottfac695a2016-08-24 17:02:40 -070041import android.support.v4.content.ContextCompat;
Chiao Cheng6c712f42012-11-26 15:35:28 -080042import android.text.TextUtils;
43import android.util.Log;
44import android.util.TimingLogger;
45
46import com.android.contacts.common.MoreContactUtils;
47import com.android.contacts.common.list.ContactListFilterController;
48import com.android.contacts.common.model.account.AccountType;
49import com.android.contacts.common.model.account.AccountTypeWithDataSet;
50import com.android.contacts.common.model.account.AccountWithDataSet;
51import com.android.contacts.common.model.account.ExchangeAccountType;
52import com.android.contacts.common.model.account.ExternalAccountType;
53import com.android.contacts.common.model.account.FallbackAccountType;
54import com.android.contacts.common.model.account.GoogleAccountType;
Brian Attwell684318f2015-02-25 12:39:28 -080055import com.android.contacts.common.model.account.SamsungAccountType;
Chiao Cheng6c712f42012-11-26 15:35:28 -080056import com.android.contacts.common.model.dataitem.DataKind;
Chiao Cheng6c712f42012-11-26 15:35:28 -080057import com.android.contacts.common.util.Constants;
Marcus Hagerottfac695a2016-08-24 17:02:40 -070058import com.android.contacts.common.util.DeviceLocalAccountTypeFactory;
Marcus Hagerott6caf23f2016-08-18 15:02:42 -070059import com.android.contactsbind.ObjectFactory;
Chiao Cheng6c712f42012-11-26 15:35:28 -080060import com.google.common.annotations.VisibleForTesting;
61import com.google.common.base.Objects;
62import com.google.common.collect.Lists;
63import com.google.common.collect.Maps;
64import com.google.common.collect.Sets;
65
66import java.util.Collection;
67import java.util.Collections;
68import java.util.Comparator;
69import java.util.HashMap;
70import java.util.List;
71import java.util.Map;
72import java.util.Set;
73import java.util.concurrent.CountDownLatch;
74import java.util.concurrent.atomic.AtomicBoolean;
75
Marcus Hagerottfac695a2016-08-24 17:02:40 -070076import static com.android.contacts.common.util.DeviceLocalAccountTypeFactory.Util.isLocalAccountType;
77
Chiao Cheng6c712f42012-11-26 15:35:28 -080078/**
79 * Singleton holder for all parsed {@link AccountType} available on the
80 * system, typically filled through {@link PackageManager} queries.
81 */
82public abstract class AccountTypeManager {
83 static final String TAG = "AccountTypeManager";
84
85 private static final Object mInitializationLock = new Object();
86 private static AccountTypeManager mAccountTypeManager;
87
88 /**
89 * Requests the singleton instance of {@link AccountTypeManager} with data bound from
90 * the available authenticators. This method can safely be called from the UI thread.
91 */
92 public static AccountTypeManager getInstance(Context context) {
Marcus Hagerottfac695a2016-08-24 17:02:40 -070093 if (!hasRequiredPermissions(context)) {
94 // Hopefully any component that depends on the values returned by this class
95 // will be restarted if the permissions change.
96 return EMPTY;
97 }
Chiao Cheng6c712f42012-11-26 15:35:28 -080098 synchronized (mInitializationLock) {
99 if (mAccountTypeManager == null) {
100 context = context.getApplicationContext();
Marcus Hagerott6caf23f2016-08-18 15:02:42 -0700101 mAccountTypeManager = new AccountTypeManagerImpl(context,
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700102 ObjectFactory.getDeviceLocalAccountTypeFactory(context));
Chiao Cheng6c712f42012-11-26 15:35:28 -0800103 }
104 }
105 return mAccountTypeManager;
106 }
107
108 /**
109 * Set the instance of account type manager. This is only for and should only be used by unit
110 * tests. While having this method is not ideal, it's simpler than the alternative of
111 * holding this as a service in the ContactsApplication context class.
112 *
113 * @param mockManager The mock AccountTypeManager.
114 */
Chiao Cheng6c712f42012-11-26 15:35:28 -0800115 public static void setInstanceForTest(AccountTypeManager mockManager) {
116 synchronized (mInitializationLock) {
117 mAccountTypeManager = mockManager;
118 }
119 }
120
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700121 private static final AccountTypeManager EMPTY = new AccountTypeManager() {
122 @Override
123 public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
124 return Collections.emptyList();
125 }
126
127 @Override
128 public void sortAccounts(AccountWithDataSet defaultAccount) {
129 }
130
131 @Override
132 public List<AccountWithDataSet> getGroupWritableAccounts() {
133 return Collections.emptyList();
134 }
135
136 @Override
137 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
138 return null;
139 }
140
141 @Override
142 public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
143 return null;
144 }
145
146 @Override
147 public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
148 return Collections.emptyList();
149 }
150 };
151
Chiao Cheng6c712f42012-11-26 15:35:28 -0800152 /**
153 * Returns the list of all accounts (if contactWritableOnly is false) or just the list of
154 * contact writable accounts (if contactWritableOnly is true).
155 */
156 // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
157 public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
158
159 /**
Wenyi Wangaa0e6ff2016-07-06 17:22:42 -0700160 * Sort accounts based on default account.
161 */
162 public abstract void sortAccounts(AccountWithDataSet defaultAccount);
163
164 /**
Chiao Cheng6c712f42012-11-26 15:35:28 -0800165 * Returns the list of accounts that are group writable.
166 */
167 public abstract List<AccountWithDataSet> getGroupWritableAccounts();
168
169 public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
170
171 public final AccountType getAccountType(String accountType, String dataSet) {
172 return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
173 }
174
175 public final AccountType getAccountTypeForAccount(AccountWithDataSet account) {
Jay Shrauner2ae200d2015-01-09 11:36:20 -0800176 if (account != null) {
177 return getAccountType(account.getAccountTypeWithDataSet());
178 }
179 return getAccountType(null, null);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800180 }
181
182 /**
183 * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
184 * which support the "invite" feature and have one or more account.
185 *
186 * This is a filtered down and more "usable" list compared to
187 * {@link #getAllInvitableAccountTypes}, where usable is defined as:
188 * (1) making sure that the app that contributed the account type is not disabled
189 * (in order to avoid presenting the user with an option that does nothing), and
190 * (2) that there is at least one raw contact with that account type in the database
191 * (assuming that the user probably doesn't use that account type).
192 *
193 * Warning: Don't use on the UI thread because this can scan the database.
194 */
195 public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes();
196
197 /**
198 * Find the best {@link DataKind} matching the requested
199 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}.
200 * If no direct match found, we try searching {@link FallbackAccountType}.
201 */
202 public DataKind getKindOrFallback(AccountType type, String mimeType) {
203 return type == null ? null : type.getKindForMimetype(mimeType);
204 }
205
206 /**
207 * Returns all registered {@link AccountType}s, including extension ones.
208 *
209 * @param contactWritableOnly if true, it only returns ones that support writing contacts.
210 */
211 public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly);
212
213 /**
214 * @param contactWritableOnly if true, it only returns ones that support writing contacts.
215 * @return true when this instance contains the given account.
216 */
217 public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) {
Tingting Wang0ac73ba2016-07-05 22:33:01 -0700218 for (AccountWithDataSet account_2 : getAccounts(contactWritableOnly)) {
Chiao Cheng6c712f42012-11-26 15:35:28 -0800219 if (account.equals(account_2)) {
220 return true;
221 }
222 }
223 return false;
224 }
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700225
226 private static boolean hasRequiredPermissions(Context context) {
227 final boolean canGetAccounts = ContextCompat.checkSelfPermission(context,
228 android.Manifest.permission.GET_ACCOUNTS) == PackageManager.PERMISSION_GRANTED;
229 final boolean canReadContacts = ContextCompat.checkSelfPermission(context,
230 android.Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
231 return canGetAccounts && canReadContacts;
232 }
233
Chiao Cheng6c712f42012-11-26 15:35:28 -0800234}
235
Wenyi Wangaa0e6ff2016-07-06 17:22:42 -0700236class AccountComparator implements Comparator<AccountWithDataSet> {
237 private AccountWithDataSet mDefaultAccount;
238
239 public AccountComparator(AccountWithDataSet defaultAccount) {
240 mDefaultAccount = defaultAccount;
241 }
242
243 @Override
244 public int compare(AccountWithDataSet a, AccountWithDataSet b) {
245 if (Objects.equal(a.name, b.name) && Objects.equal(a.type, b.type)
246 && Objects.equal(a.dataSet, b.dataSet)) {
247 return 0;
248 } else if (b.name == null || b.type == null) {
249 return -1;
250 } else if (a.name == null || a.type == null) {
251 return 1;
252 } else if (isWritableGoogleAccount(a) && a.equals(mDefaultAccount)) {
253 return -1;
254 } else if (isWritableGoogleAccount(b) && b.equals(mDefaultAccount)) {
255 return 1;
256 } else if (isWritableGoogleAccount(a) && !isWritableGoogleAccount(b)) {
257 return -1;
258 } else if (isWritableGoogleAccount(b) && !isWritableGoogleAccount(a)) {
259 return 1;
260 } else {
261 int diff = a.name.compareToIgnoreCase(b.name);
262 if (diff != 0) {
263 return diff;
264 }
265 diff = a.type.compareToIgnoreCase(b.type);
266 if (diff != 0) {
267 return diff;
268 }
269
270 // Accounts without data sets get sorted before those that have them.
271 if (a.dataSet != null) {
272 return b.dataSet == null ? 1 : a.dataSet.compareToIgnoreCase(b.dataSet);
273 } else {
274 return -1;
275 }
276 }
277 }
278
279 private static boolean isWritableGoogleAccount(AccountWithDataSet account) {
280 return GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null;
281 }
282}
283
Chiao Cheng6c712f42012-11-26 15:35:28 -0800284class AccountTypeManagerImpl extends AccountTypeManager
285 implements OnAccountsUpdateListener, SyncStatusObserver {
286
287 private static final Map<AccountTypeWithDataSet, AccountType>
288 EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP =
289 Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
290
291 /**
292 * A sample contact URI used to test whether any activities will respond to an
293 * invitable intent with the given URI as the intent data. This doesn't need to be
294 * specific to a real contact because an app that intercepts the intent should probably do so
295 * for all types of contact URIs.
296 */
297 private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(
298 1, "xxx");
299
300 private Context mContext;
Ihab Awad413589f2014-07-02 14:00:28 -0700301 private AccountManager mAccountManager;
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700302 private DeviceLocalAccountTypeFactory mDeviceLocalAccountTypeFactory;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800303
304 private AccountType mFallbackAccountType;
305
306 private List<AccountWithDataSet> mAccounts = Lists.newArrayList();
307 private List<AccountWithDataSet> mContactWritableAccounts = Lists.newArrayList();
308 private List<AccountWithDataSet> mGroupWritableAccounts = Lists.newArrayList();
309 private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
310 private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
311 EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
312
313 private final InvitableAccountTypeCache mInvitableAccountTypeCache;
314
315 /**
316 * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been
317 * initialized. False otherwise.
318 */
319 private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false);
320
321 /**
322 * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing.
323 * False otherwise.
324 */
325 private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false);
326
327 private static final int MESSAGE_LOAD_DATA = 0;
328 private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
329
330 private HandlerThread mListenerThread;
331 private Handler mListenerHandler;
332
333 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
334 private final Runnable mCheckFilterValidityRunnable = new Runnable () {
335 @Override
336 public void run() {
337 ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
338 }
339 };
340
341 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
342
343 @Override
344 public void onReceive(Context context, Intent intent) {
345 Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
346 mListenerHandler.sendMessage(msg);
347 }
348
349 };
350
351 /* A latch that ensures that asynchronous initialization completes before data is used */
352 private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);
353
Chiao Cheng6c712f42012-11-26 15:35:28 -0800354 /**
355 * Internal constructor that only performs initial parsing.
356 */
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700357 public AccountTypeManagerImpl(Context context,
358 DeviceLocalAccountTypeFactory deviceLocalAccountTypeFactory) {
Chiao Cheng6c712f42012-11-26 15:35:28 -0800359 mContext = context;
360 mFallbackAccountType = new FallbackAccountType(context);
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700361 mDeviceLocalAccountTypeFactory = deviceLocalAccountTypeFactory;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800362
Ihab Awad413589f2014-07-02 14:00:28 -0700363 mAccountManager = AccountManager.get(mContext);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800364
365 mListenerThread = new HandlerThread("AccountChangeListener");
366 mListenerThread.start();
367 mListenerHandler = new Handler(mListenerThread.getLooper()) {
368 @Override
369 public void handleMessage(Message msg) {
370 switch (msg.what) {
371 case MESSAGE_LOAD_DATA:
372 loadAccountsInBackground();
373 break;
374 case MESSAGE_PROCESS_BROADCAST_INTENT:
375 processBroadcastIntent((Intent) msg.obj);
376 break;
377 }
378 }
379 };
380
381 mInvitableAccountTypeCache = new InvitableAccountTypeCache();
382
383 // Request updates when packages or accounts change
384 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
385 filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
386 filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
387 filter.addDataScheme("package");
388 mContext.registerReceiver(mBroadcastReceiver, filter);
389 IntentFilter sdFilter = new IntentFilter();
390 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
391 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
392 mContext.registerReceiver(mBroadcastReceiver, sdFilter);
393
394 // Request updates when locale is changed so that the order of each field will
395 // be able to be changed on the locale change.
396 filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
397 mContext.registerReceiver(mBroadcastReceiver, filter);
398
Ihab Awad413589f2014-07-02 14:00:28 -0700399 mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800400
401 ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
402
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700403 // Observe changes to RAW_CONTACTS so that we will update the list of "Device" accounts
404 // if a new device contact is added.
405 mContext.getContentResolver().registerContentObserver(
406 ContactsContract.RawContacts.CONTENT_URI, /* notifyDescendents */ true,
407 new ContentObserver(mListenerHandler) {
408 @Override
409 public boolean deliverSelfNotifications() {
410 return true;
411 }
412
413 @Override
414 public void onChange(boolean selfChange) {
415 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
416 }
417
418 @Override
419 public void onChange(boolean selfChange, Uri uri) {
420 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
421 }
422 });
423
Chiao Cheng6c712f42012-11-26 15:35:28 -0800424 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
425 }
426
427 @Override
428 public void onStatusChanged(int which) {
429 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
430 }
431
432 public void processBroadcastIntent(Intent intent) {
433 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
434 }
435
436 /* This notification will arrive on the background thread */
437 public void onAccountsUpdated(Account[] accounts) {
438 // Refresh to catch any changed accounts
439 loadAccountsInBackground();
440 }
441
442 /**
443 * Returns instantly if accounts and account types have already been loaded.
444 * Otherwise waits for the background thread to complete the loading.
445 */
446 void ensureAccountsLoaded() {
447 CountDownLatch latch = mInitializationLatch;
448 if (latch == null) {
449 return;
450 }
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700451
Chiao Cheng6c712f42012-11-26 15:35:28 -0800452 while (true) {
453 try {
454 latch.await();
455 return;
456 } catch (InterruptedException e) {
457 Thread.currentThread().interrupt();
458 }
459 }
460 }
461
462 /**
463 * Loads account list and corresponding account types (potentially with data sets). Always
464 * called on a background thread.
465 */
466 protected void loadAccountsInBackground() {
467 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
468 Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
469 }
470 TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground");
471 final long startTime = SystemClock.currentThreadTimeMillis();
472 final long startTimeWall = SystemClock.elapsedRealtime();
473
474 // Account types, keyed off the account type and data set concatenation.
475 final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet =
476 Maps.newHashMap();
477
478 // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}. Since there can
479 // be multiple account types (with different data sets) for the same type of account, each
480 // type string may have multiple AccountType entries.
481 final Map<String, List<AccountType>> accountTypesByType = Maps.newHashMap();
482
483 final List<AccountWithDataSet> allAccounts = Lists.newArrayList();
484 final List<AccountWithDataSet> contactWritableAccounts = Lists.newArrayList();
485 final List<AccountWithDataSet> groupWritableAccounts = Lists.newArrayList();
486 final Set<String> extensionPackages = Sets.newHashSet();
487
Ihab Awad413589f2014-07-02 14:00:28 -0700488 final AccountManager am = mAccountManager;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800489
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700490 final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes();
491 final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
Chiao Cheng6c712f42012-11-26 15:35:28 -0800492
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700493 // First process sync adapters to find any that provide contact data.
494 for (SyncAdapterType sync : syncs) {
495 if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
496 // Skip sync adapters that don't provide contact data.
497 continue;
498 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800499
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700500 // Look for the formatting details provided by each sync
501 // adapter, using the authenticator to find general resources.
502 final String type = sync.accountType;
503 final AuthenticatorDescription auth = findAuthenticator(auths, type);
504 if (auth == null) {
505 Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
506 continue;
507 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800508
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700509 AccountType accountType;
510 if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
511 accountType = new GoogleAccountType(mContext, auth.packageName);
512 } else if (ExchangeAccountType.isExchangeType(type)) {
513 accountType = new ExchangeAccountType(mContext, auth.packageName, type);
Brian Attwell684318f2015-02-25 12:39:28 -0800514 } else if (SamsungAccountType.isSamsungAccountType(mContext, type,
515 auth.packageName)) {
516 accountType = new SamsungAccountType(mContext, auth.packageName, type);
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700517 } else if (!ExternalAccountType.hasContactsXml(mContext, auth.packageName)
518 && isLocalAccountType(mDeviceLocalAccountTypeFactory, type)) {
519 // This will be loaded by the DeviceLocalAccountLocator so don't try to create an
520 // ExternalAccountType for it.
521 continue;
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700522 } else {
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700523 Log.d(TAG, "Registering external account type=" + type
524 + ", packageName=" + auth.packageName);
525 accountType = new ExternalAccountType(mContext, auth.packageName, false);
526 }
527 if (!accountType.isInitialized()) {
528 if (accountType.isEmbedded()) {
529 throw new IllegalStateException("Problem initializing embedded type "
530 + accountType.getClass().getCanonicalName());
Chiao Cheng6c712f42012-11-26 15:35:28 -0800531 } else {
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700532 // Skip external account types that couldn't be initialized.
533 continue;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800534 }
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700535 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800536
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700537 accountType.initializeFieldsFromAuthenticator(auth);
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700538
539 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
540
541 // Check to see if the account type knows of any other non-sync-adapter packages
542 // that may provide other data sets of contact data.
543 extensionPackages.addAll(accountType.getExtensionPackageNames());
544 }
545
546 // If any extension packages were specified, process them as well.
547 if (!extensionPackages.isEmpty()) {
548 Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages");
549 for (String extensionPackage : extensionPackages) {
550 ExternalAccountType accountType =
551 new ExternalAccountType(mContext, extensionPackage, true);
552 if (!accountType.isInitialized()) {
553 // Skip external account types that couldn't be initialized.
554 continue;
555 }
556 if (!accountType.hasContactsMetadata()) {
557 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
558 + " it doesn't have the CONTACTS_STRUCTURE metadata");
559 continue;
560 }
561 if (TextUtils.isEmpty(accountType.accountType)) {
562 Log.w(TAG, "Skipping extension package " + extensionPackage + " because"
563 + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
564 + " attribute");
565 continue;
566 }
567 Log.d(TAG, "Registering extension package account type="
568 + accountType.accountType + ", dataSet=" + accountType.dataSet
569 + ", packageName=" + extensionPackage);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800570
571 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800572 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800573 }
574 timings.addSplit("Loaded account types");
575
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700576 boolean foundWritableGoogleAccount = false;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800577 // Map in accounts to associate the account names with each account type entry.
Ihab Awad413589f2014-07-02 14:00:28 -0700578 Account[] accounts = mAccountManager.getAccounts();
Chiao Cheng6c712f42012-11-26 15:35:28 -0800579 for (Account account : accounts) {
Jay Shraunere7a1dfa2013-05-29 15:37:26 -0700580 boolean syncable =
581 ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
Chiao Cheng6c712f42012-11-26 15:35:28 -0800582
583 if (syncable) {
584 List<AccountType> accountTypes = accountTypesByType.get(account.type);
585 if (accountTypes != null) {
586 // Add an account-with-data-set entry for each account type that is
587 // authenticated by this account.
588 for (AccountType accountType : accountTypes) {
589 AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
590 account.name, account.type, accountType.dataSet);
591 allAccounts.add(accountWithDataSet);
592 if (accountType.areContactsWritable()) {
593 contactWritableAccounts.add(accountWithDataSet);
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700594 if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type)
595 && accountWithDataSet.dataSet == null) {
596 foundWritableGoogleAccount = true;
597 }
Walter Jang0fed9b62016-09-07 15:23:22 -0700598
599 if (accountType.isGroupMembershipEditable()) {
600 groupWritableAccounts.add(accountWithDataSet);
601 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800602 }
603 }
604 }
605 }
606 }
607
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700608 final DeviceLocalAccountLocator deviceAccounts =
609 new DeviceLocalAccountLocator(mContext.getContentResolver(),
610 mDeviceLocalAccountTypeFactory,
611 allAccounts);
612 final List<AccountWithDataSet> localAccounts = deviceAccounts.getDeviceLocalAccounts();
613 allAccounts.addAll(localAccounts);
614
615 for (AccountWithDataSet localAccount : localAccounts) {
616 // Prefer a known type if it exists. This covers the case that a local account has an
617 // authenticator with a valid contacts.xml
618 AccountType localAccountType = accountTypesByTypeAndDataSet.get(
619 localAccount.getAccountTypeWithDataSet());
620 if (localAccountType == null) {
621 localAccountType = mDeviceLocalAccountTypeFactory.getAccountType(localAccount.type);
622 }
623 accountTypesByTypeAndDataSet.put(localAccount.getAccountTypeWithDataSet(),
624 localAccountType);
625
626 // Skip the null account if there is a Google account available. This is done because
627 // the Google account's sync adapter will automatically move accounts in the "null"
628 // account. Hence, it would be confusing to still show it as an available writable
629 // account since contacts that were saved to it would magically change accounts when the
630 // sync adapter runs.
631 if (foundWritableGoogleAccount && localAccount.type == null) {
632 continue;
633 }
634 if (localAccountType.areContactsWritable()) {
635 contactWritableAccounts.add(localAccount);
Walter Jang0fed9b62016-09-07 15:23:22 -0700636
637 if (localAccountType.isGroupMembershipEditable()) {
638 groupWritableAccounts.add(localAccount);
639 }
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700640 }
641 }
642
Wenyi Wangaa0e6ff2016-07-06 17:22:42 -0700643 final AccountComparator accountComparator = new AccountComparator(null);
644 Collections.sort(allAccounts, accountComparator);
645 Collections.sort(contactWritableAccounts, accountComparator);
646 Collections.sort(groupWritableAccounts, accountComparator);
Chiao Cheng6c712f42012-11-26 15:35:28 -0800647
648 timings.addSplit("Loaded accounts");
649
650 synchronized (this) {
651 mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
652 mAccounts = allAccounts;
653 mContactWritableAccounts = contactWritableAccounts;
654 mGroupWritableAccounts = groupWritableAccounts;
655 mInvitableAccountTypes = findAllInvitableAccountTypes(
656 mContext, allAccounts, accountTypesByTypeAndDataSet);
657 }
658
659 timings.dumpToLog();
660 final long endTimeWall = SystemClock.elapsedRealtime();
661 final long endTime = SystemClock.currentThreadTimeMillis();
662
663 Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, "
664 + mAccounts.size() + " accounts in " + (endTimeWall - startTimeWall) + "ms(wall) "
665 + (endTime - startTime) + "ms(cpu)");
666
667 if (mInitializationLatch != null) {
668 mInitializationLatch.countDown();
669 mInitializationLatch = null;
670 }
671 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
672 Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
673 }
674
675 // Check filter validity since filter may become obsolete after account update. It must be
676 // done from UI thread.
677 mMainThreadHandler.post(mCheckFilterValidityRunnable);
678 }
679
680 // Bookkeeping method for tracking the known account types in the given maps.
681 private void addAccountType(AccountType accountType,
682 Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
683 Map<String, List<AccountType>> accountTypesByType) {
684 accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
685 List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
686 if (accountsForType == null) {
687 accountsForType = Lists.newArrayList();
688 }
689 accountsForType.add(accountType);
690 accountTypesByType.put(accountType.accountType, accountsForType);
691 }
692
693 /**
694 * Find a specific {@link AuthenticatorDescription} in the provided list
695 * that matches the given account type.
696 */
697 protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
698 String accountType) {
699 for (AuthenticatorDescription auth : auths) {
700 if (accountType.equals(auth.type)) {
701 return auth;
702 }
703 }
704 return null;
705 }
706
707 /**
708 * Return list of all known, contact writable {@link AccountWithDataSet}'s.
709 */
710 @Override
711 public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
712 ensureAccountsLoaded();
713 return contactWritableOnly ? mContactWritableAccounts : mAccounts;
714 }
715
716 /**
Wenyi Wangaa0e6ff2016-07-06 17:22:42 -0700717 * Sort accounts based on default account.
718 */
719 @Override
720 public void sortAccounts(AccountWithDataSet defaultAccount) {
721 Collections.sort(mAccounts, new AccountComparator(defaultAccount));
722 Collections.sort(mContactWritableAccounts, new AccountComparator(defaultAccount));
723 Collections.sort(mGroupWritableAccounts, new AccountComparator(defaultAccount));
724 }
725
726 /**
Chiao Cheng6c712f42012-11-26 15:35:28 -0800727 * Return the list of all known, group writable {@link AccountWithDataSet}'s.
728 */
729 public List<AccountWithDataSet> getGroupWritableAccounts() {
730 ensureAccountsLoaded();
731 return mGroupWritableAccounts;
732 }
733
734 /**
735 * Find the best {@link DataKind} matching the requested
736 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}.
737 * If no direct match found, we try searching {@link FallbackAccountType}.
738 */
739 @Override
740 public DataKind getKindOrFallback(AccountType type, String mimeType) {
741 ensureAccountsLoaded();
742 DataKind kind = null;
743
744 // Try finding account type and kind matching request
745 if (type != null) {
746 kind = type.getKindForMimetype(mimeType);
747 }
748
749 if (kind == null) {
750 // Nothing found, so try fallback as last resort
751 kind = mFallbackAccountType.getKindForMimetype(mimeType);
752 }
753
754 if (kind == null) {
Chiao Cheng5df73762012-12-12 11:28:35 -0800755 if (Log.isLoggable(TAG, Log.DEBUG)) {
756 Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType);
757 }
Chiao Cheng6c712f42012-11-26 15:35:28 -0800758 }
759
760 return kind;
761 }
762
763 /**
764 * Return {@link AccountType} for the given account type and data set.
765 */
766 @Override
767 public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
768 ensureAccountsLoaded();
769 synchronized (this) {
770 AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
771 return type != null ? type : mFallbackAccountType;
772 }
773 }
774
775 /**
776 * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
777 * which support the "invite" feature and have one or more account. This is an unfiltered
778 * list. See {@link #getUsableInvitableAccountTypes()}.
779 */
780 private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
781 ensureAccountsLoaded();
782 return mInvitableAccountTypes;
783 }
784
785 @Override
786 public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
787 ensureAccountsLoaded();
788 // Since this method is not thread-safe, it's possible for multiple threads to encounter
789 // the situation where (1) the cache has not been initialized yet or
790 // (2) an async task to refresh the account type list in the cache has already been
791 // started. Hence we use {@link AtomicBoolean}s and return cached values immediately
792 // while we compute the actual result in the background. We use this approach instead of
793 // using "synchronized" because computing the account type list involves a DB read, and
794 // can potentially cause a deadlock situation if this method is called from code which
795 // holds the DB lock. The trade-off of potentially having an incorrect list of invitable
796 // account types for a short period of time seems more manageable than enforcing the
797 // context in which this method is called.
798
799 // Computing the list of usable invitable account types is done on the fly as requested.
800 // If this method has never been called before, then block until the list has been computed.
801 if (!mInvitablesCacheIsInitialized.get()) {
802 mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext));
803 mInvitablesCacheIsInitialized.set(true);
804 } else {
805 // Otherwise, there is a value in the cache. If the value has expired and
806 // an async task has not already been started by another thread, then kick off a new
807 // async task to compute the list.
808 if (mInvitableAccountTypeCache.isExpired() &&
809 mInvitablesTaskIsRunning.compareAndSet(false, true)) {
810 new FindInvitablesTask().execute();
811 }
812 }
813
814 return mInvitableAccountTypeCache.getCachedValue();
815 }
816
817 /**
818 * Return all {@link AccountType}s with at least one account which supports "invite", i.e.
819 * its {@link AccountType#getInviteContactActivityClassName()} is not empty.
820 */
821 @VisibleForTesting
822 static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(Context context,
823 Collection<AccountWithDataSet> accounts,
824 Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
825 HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
826 for (AccountWithDataSet account : accounts) {
827 AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet();
828 AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
829 if (type == null) continue; // just in case
830 if (result.containsKey(accountTypeWithDataSet)) continue;
831
832 if (Log.isLoggable(TAG, Log.DEBUG)) {
833 Log.d(TAG, "Type " + accountTypeWithDataSet
834 + " inviteClass=" + type.getInviteContactActivityClassName());
835 }
836 if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
837 result.put(accountTypeWithDataSet, type);
838 }
839 }
840 return Collections.unmodifiableMap(result);
841 }
842
843 /**
844 * Return all usable {@link AccountType}s that support the "invite" feature from the
845 * list of all potential invitable account types (retrieved from
846 * {@link #getAllInvitableAccountTypes}). A usable invitable account type means:
847 * (1) there is at least 1 raw contact in the database with that account type, and
848 * (2) the app contributing the account type is not disabled.
849 *
850 * Warning: Don't use on the UI thread because this can scan the database.
851 */
852 private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes(
853 Context context) {
854 Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes();
855 if (allInvitables.isEmpty()) {
856 return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
857 }
858
859 final HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
860 result.putAll(allInvitables);
861
862 final PackageManager packageManager = context.getPackageManager();
863 for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) {
864 AccountType accountType = allInvitables.get(accountTypeWithDataSet);
865
866 // Make sure that account types don't come from apps that are disabled.
867 Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType,
868 SAMPLE_CONTACT_URI);
869 if (invitableIntent == null) {
870 result.remove(accountTypeWithDataSet);
871 continue;
872 }
873 ResolveInfo resolveInfo = packageManager.resolveActivity(invitableIntent,
874 PackageManager.MATCH_DEFAULT_ONLY);
875 if (resolveInfo == null) {
876 // If we can't find an activity to start for this intent, then there's no point in
877 // showing this option to the user.
878 result.remove(accountTypeWithDataSet);
879 continue;
880 }
881
882 // Make sure that there is at least 1 raw contact with this account type. This check
883 // is non-trivial and should not be done on the UI thread.
884 if (!accountTypeWithDataSet.hasData(context)) {
885 result.remove(accountTypeWithDataSet);
886 }
887 }
888
889 return Collections.unmodifiableMap(result);
890 }
891
892 @Override
893 public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
894 ensureAccountsLoaded();
895 final List<AccountType> accountTypes = Lists.newArrayList();
896 synchronized (this) {
897 for (AccountType type : mAccountTypesWithDataSets.values()) {
898 if (!contactWritableOnly || type.areContactsWritable()) {
899 accountTypes.add(type);
900 }
901 }
902 }
903 return accountTypes;
904 }
905
906 /**
907 * Background task to find all usable {@link AccountType}s that support the "invite" feature
908 * from the list of all potential invitable account types. Once the work is completed,
909 * the list of account types is stored in the {@link AccountTypeManager}'s
910 * {@link InvitableAccountTypeCache}.
911 */
912 private class FindInvitablesTask extends AsyncTask<Void, Void,
913 Map<AccountTypeWithDataSet, AccountType>> {
914
915 @Override
916 protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
917 return findUsableInvitableAccountTypes(mContext);
918 }
919
920 @Override
921 protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
922 mInvitableAccountTypeCache.setCachedValue(accountTypes);
923 mInvitablesTaskIsRunning.set(false);
924 }
925 }
926
927 /**
928 * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a
929 * {@link Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only
930 * for {@link #TIME_TO_LIVE} milliseconds.
931 */
932 private static final class InvitableAccountTypeCache {
933
934 /**
935 * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds
936 * has elapsed.
937 */
938 private static final long TIME_TO_LIVE = 60000;
939
940 private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes;
941
942 private long mTimeLastSet;
943
944 /**
945 * Returns true if the data in this cache is stale and needs to be refreshed. Returns false
946 * otherwise.
947 */
948 public boolean isExpired() {
949 return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE;
950 }
951
952 /**
953 * Returns the cached value. Note that the caller is responsible for checking
954 * {@link #isExpired()} to ensure that the value is not stale.
955 */
956 public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
957 return mInvitableAccountTypes;
958 }
959
960 public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
961 mInvitableAccountTypes = map;
962 mTimeLastSet = SystemClock.elapsedRealtime();
963 }
964 }
965}