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