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