AccountTypeManager.java revision 3572293500a7406813e61e6fedd63ca606debc19
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.contacts.model; 18 19import com.android.internal.util.Objects; 20import com.google.android.collect.Lists; 21import com.google.android.collect.Maps; 22import com.google.android.collect.Sets; 23import com.google.common.annotations.VisibleForTesting; 24import com.google.i18n.phonenumbers.PhoneNumberUtil; 25 26import android.accounts.Account; 27import android.accounts.AccountManager; 28import android.accounts.AuthenticatorDescription; 29import android.accounts.OnAccountsUpdateListener; 30import android.content.BroadcastReceiver; 31import android.content.ContentResolver; 32import android.content.Context; 33import android.content.IContentService; 34import android.content.Intent; 35import android.content.IntentFilter; 36import android.content.SyncAdapterType; 37import android.content.SyncStatusObserver; 38import android.content.pm.PackageManager; 39import android.os.Handler; 40import android.os.HandlerThread; 41import android.os.Message; 42import android.os.RemoteException; 43import android.os.SystemClock; 44import android.provider.ContactsContract; 45import android.text.TextUtils; 46import android.util.Log; 47 48import java.util.Collection; 49import java.util.Collections; 50import java.util.Comparator; 51import java.util.HashMap; 52import java.util.List; 53import java.util.Locale; 54import java.util.Map; 55import java.util.Set; 56import java.util.concurrent.CountDownLatch; 57 58/** 59 * Singleton holder for all parsed {@link AccountType} available on the 60 * system, typically filled through {@link PackageManager} queries. 61 */ 62public abstract class AccountTypeManager { 63 static final String TAG = "AccountTypeManager"; 64 65 public static final String ACCOUNT_TYPE_SERVICE = "contactAccountTypes"; 66 67 /** 68 * Requests the singleton instance of {@link AccountTypeManager} with data bound from 69 * the available authenticators. This method can safely be called from the UI thread. 70 */ 71 public static AccountTypeManager getInstance(Context context) { 72 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 account type strings to {@link AccountType}s which support 92 * the "invite" feature and have one or more account. 93 */ 94 public abstract Map<String, 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<String, AccountType> mAccountTypesWithDataSets = Maps.newHashMap(); 118 private Map<String, AccountType> mInvitableAccountTypes = Collections.unmodifiableMap( 119 new HashMap<String, 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<String, 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 Log.d(TAG, "Registering extension package account type=" 342 + accountType.accountType + ", dataSet=" + accountType.dataSet 343 + ", packageName=" + extensionPackage); 344 345 addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType); 346 } 347 } 348 } catch (RemoteException e) { 349 Log.w(TAG, "Problem loading accounts: " + e.toString()); 350 } 351 352 // Map in accounts to associate the account names with each account type entry. 353 Account[] accounts = mAccountManager.getAccounts(); 354 for (Account account : accounts) { 355 boolean syncable = false; 356 try { 357 syncable = cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0; 358 } catch (RemoteException e) { 359 Log.e(TAG, "Cannot obtain sync flag for account: " + account, e); 360 } 361 362 if (syncable) { 363 List<AccountType> accountTypes = accountTypesByType.get(account.type); 364 if (accountTypes != null) { 365 // Add an account-with-data-set entry for each account type that is 366 // authenticated by this account. 367 for (AccountType accountType : accountTypes) { 368 AccountWithDataSet accountWithDataSet = new AccountWithDataSet( 369 account.name, account.type, accountType.dataSet); 370 allAccounts.add(accountWithDataSet); 371 if (!accountType.readOnly) { 372 writableAccounts.add(accountWithDataSet); 373 } 374 } 375 } 376 } 377 } 378 379 Collections.sort(allAccounts, ACCOUNT_COMPARATOR); 380 Collections.sort(writableAccounts, ACCOUNT_COMPARATOR); 381 382 // The UI will need a phone number formatter. We can preload meta data for the 383 // current locale to prevent a delay later on. 384 PhoneNumberUtil.getInstance().getAsYouTypeFormatter(Locale.getDefault().getCountry()); 385 386 long endTime = SystemClock.currentThreadTimeMillis(); 387 388 synchronized (this) { 389 mAccountTypesWithDataSets = accountTypesByTypeAndDataSet; 390 mAccounts = allAccounts; 391 mWritableAccounts = writableAccounts; 392 mInvitableAccountTypes = findInvitableAccountTypes( 393 mContext, allAccounts, accountTypesByTypeAndDataSet); 394 } 395 396 Log.i(TAG, "Loaded meta-data for " + mAccountTypesWithDataSets.size() + " account types, " 397 + mAccounts.size() + " accounts in " + (endTime - startTime) + "ms"); 398 399 if (mInitializationLatch != null) { 400 mInitializationLatch.countDown(); 401 mInitializationLatch = null; 402 } 403 } 404 405 // Bookkeeping method for tracking the known account types in the given maps. 406 private void addAccountType(AccountType accountType, 407 Map<String, AccountType> accountTypesByTypeAndDataSet, 408 Map<String, List<AccountType>> accountTypesByType) { 409 accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType); 410 List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType); 411 if (accountsForType == null) { 412 accountsForType = Lists.newArrayList(); 413 } 414 accountsForType.add(accountType); 415 accountTypesByType.put(accountType.accountType, accountsForType); 416 } 417 418 /** 419 * Find a specific {@link AuthenticatorDescription} in the provided list 420 * that matches the given account type. 421 */ 422 protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths, 423 String accountType) { 424 for (AuthenticatorDescription auth : auths) { 425 if (accountType.equals(auth.type)) { 426 return auth; 427 } 428 } 429 return null; 430 } 431 432 /** 433 * Return list of all known, writable {@link AccountWithDataSet}'s. 434 */ 435 @Override 436 public List<AccountWithDataSet> getAccounts(boolean writableOnly) { 437 ensureAccountsLoaded(); 438 return writableOnly ? mWritableAccounts : mAccounts; 439 } 440 441 /** 442 * Find the best {@link DataKind} matching the requested 443 * {@link AccountType#accountType}, {@link AccountType#dataSet}, and {@link DataKind#mimeType}. 444 * If no direct match found, we try searching {@link FallbackAccountType}. 445 */ 446 @Override 447 public DataKind getKindOrFallback(String accountType, String dataSet, String mimeType) { 448 ensureAccountsLoaded(); 449 DataKind kind = null; 450 451 // Try finding account type and kind matching request 452 final AccountType type = mAccountTypesWithDataSets.get( 453 AccountType.getAccountTypeAndDataSet(accountType, dataSet)); 454 if (type != null) { 455 kind = type.getKindForMimetype(mimeType); 456 } 457 458 if (kind == null) { 459 // Nothing found, so try fallback as last resort 460 kind = mFallbackAccountType.getKindForMimetype(mimeType); 461 } 462 463 if (kind == null) { 464 Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType); 465 } 466 467 return kind; 468 } 469 470 /** 471 * Return {@link AccountType} for the given account type and data set. 472 */ 473 @Override 474 public AccountType getAccountType(String accountType, String dataSet) { 475 ensureAccountsLoaded(); 476 synchronized (this) { 477 AccountType type = mAccountTypesWithDataSets.get( 478 AccountType.getAccountTypeAndDataSet(accountType, dataSet)); 479 return type != null ? type : mFallbackAccountType; 480 } 481 } 482 483 @Override 484 public Map<String, AccountType> getInvitableAccountTypes() { 485 return mInvitableAccountTypes; 486 } 487 488 /** 489 * Return all {@link AccountType}s with at least one account which supports "invite", i.e. 490 * its {@link AccountType#getInviteContactActivityClassName()} is not empty. 491 */ 492 @VisibleForTesting 493 static Map<String, AccountType> findInvitableAccountTypes(Context context, 494 Collection<AccountWithDataSet> accounts, 495 Map<String, AccountType> accountTypesByTypeAndDataSet) { 496 HashMap<String, AccountType> result = Maps.newHashMap(); 497 for (AccountWithDataSet account : accounts) { 498 String accountTypeWithDataSet = account.getAccountTypeWithDataSet(); 499 AccountType type = accountTypesByTypeAndDataSet.get( 500 account.getAccountTypeWithDataSet()); 501 if (type == null) continue; // just in case 502 if (result.containsKey(accountTypeWithDataSet)) continue; 503 504 if (Log.isLoggable(TAG, Log.DEBUG)) { 505 Log.d(TAG, "Type " + accountTypeWithDataSet 506 + " inviteClass=" + type.getInviteContactActivityClassName() 507 + " inviteAction=" + type.getInviteContactActionLabel(context)); 508 } 509 if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) { 510 result.put(accountTypeWithDataSet, type); 511 } 512 } 513 return Collections.unmodifiableMap(result); 514 } 515} 516