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