AccountTypeManager.java revision 179c9960e50019608d91661cfbcbb3cc8bc48093
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.model.AccountType.DataKind; 20import com.google.android.collect.Lists; 21import com.google.android.collect.Maps; 22import com.google.i18n.phonenumbers.PhoneNumberUtil; 23 24import android.accounts.Account; 25import android.accounts.AccountManager; 26import android.accounts.AuthenticatorDescription; 27import android.accounts.OnAccountsUpdateListener; 28import android.content.BroadcastReceiver; 29import android.content.ContentResolver; 30import android.content.Context; 31import android.content.IContentService; 32import android.content.Intent; 33import android.content.IntentFilter; 34import android.content.SyncAdapterType; 35import android.content.SyncStatusObserver; 36import android.content.pm.PackageManager; 37import android.os.Handler; 38import android.os.HandlerThread; 39import android.os.Message; 40import android.os.RemoteException; 41import android.os.SystemClock; 42import android.provider.ContactsContract; 43import android.util.Log; 44 45import java.util.ArrayList; 46import java.util.Collections; 47import java.util.Comparator; 48import java.util.HashMap; 49import java.util.Locale; 50import java.util.concurrent.CountDownLatch; 51 52/** 53 * Singleton holder for all parsed {@link AccountType} available on the 54 * system, typically filled through {@link PackageManager} queries. 55 */ 56public abstract class AccountTypeManager { 57 static final String TAG = "AccountTypeManager"; 58 59 public static final String ACCOUNT_TYPE_SERVICE = "contactAccountTypes"; 60 61 /** 62 * Requests the singleton instance of {@link AccountTypeManager} with data bound from 63 * the available authenticators. This method can safely be called from the UI thread. 64 */ 65 public static AccountTypeManager getInstance(Context context) { 66 AccountTypeManager service = 67 (AccountTypeManager) context.getSystemService(ACCOUNT_TYPE_SERVICE); 68 if (service == null) { 69 service = createAccountTypeManager(context); 70 Log.e(TAG, "No account type service in context: " + context); 71 } 72 return service; 73 } 74 75 public static synchronized AccountTypeManager createAccountTypeManager(Context context) { 76 return new AccountTypeManagerImpl(context); 77 } 78 79 public abstract ArrayList<Account> getAccounts(boolean writableOnly); 80 81 public abstract AccountType getAccountType(String accountType); 82 83 /** 84 * Find the best {@link DataKind} matching the requested 85 * {@link AccountType#accountType} and {@link DataKind#mimeType}. If no 86 * direct match found, we try searching {@link FallbackAccountType}. 87 */ 88 public DataKind getKindOrFallback(String accountType, String mimeType) { 89 final AccountType type = getAccountType(accountType); 90 return type == null ? null : type.getKindForMimetype(mimeType); 91 } 92} 93 94class AccountTypeManagerImpl extends AccountTypeManager 95 implements OnAccountsUpdateListener, SyncStatusObserver { 96 97 private Context mContext; 98 private AccountManager mAccountManager; 99 100 private AccountType mFallbackAccountType = new FallbackAccountType(); 101 102 private ArrayList<Account> mAccounts = Lists.newArrayList(); 103 private ArrayList<Account> mWritableAccounts = Lists.newArrayList(); 104 private HashMap<String, AccountType> mAccountTypes = Maps.newHashMap(); 105 106 private static final int MESSAGE_LOAD_DATA = 0; 107 private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1; 108 109 private HandlerThread mListenerThread; 110 private Handler mListenerHandler; 111 112 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 113 114 @Override 115 public void onReceive(Context context, Intent intent) { 116 Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent); 117 mListenerHandler.sendMessage(msg); 118 } 119 120 }; 121 122 /* A latch that ensures that asynchronous initialization completes before data is used */ 123 private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1); 124 125 private static final Comparator<Account> ACCOUNT_COMPARATOR = new Comparator<Account>() { 126 127 @Override 128 public int compare(Account account1, Account account2) { 129 int diff = account1.name.compareTo(account2.name); 130 if (diff != 0) { 131 return diff; 132 } 133 return account1.type.compareTo(account2.type); 134 } 135 }; 136 137 /** 138 * Internal constructor that only performs initial parsing. 139 */ 140 public AccountTypeManagerImpl(Context context) { 141 mContext = context; 142 mAccountManager = AccountManager.get(mContext); 143 144 mListenerThread = new HandlerThread("AccountChangeListener"); 145 mListenerThread.start(); 146 mListenerHandler = new Handler(mListenerThread.getLooper()) { 147 @Override 148 public void handleMessage(Message msg) { 149 switch (msg.what) { 150 case MESSAGE_LOAD_DATA: 151 loadAccountsInBackground(); 152 break; 153 case MESSAGE_PROCESS_BROADCAST_INTENT: 154 processBroadcastIntent((Intent) msg.obj); 155 break; 156 } 157 } 158 }; 159 160 // Request updates when packages or accounts change 161 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 162 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 163 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 164 filter.addDataScheme("package"); 165 mContext.registerReceiver(mBroadcastReceiver, filter); 166 IntentFilter sdFilter = new IntentFilter(); 167 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); 168 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); 169 mContext.registerReceiver(mBroadcastReceiver, sdFilter); 170 171 // Request updates when locale is changed so that the order of each field will 172 // be able to be changed on the locale change. 173 filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); 174 mContext.registerReceiver(mBroadcastReceiver, filter); 175 176 mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false); 177 178 ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this); 179 180 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); 181 } 182 183 @Override 184 public void onStatusChanged(int which) { 185 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); 186 } 187 188 public void processBroadcastIntent(Intent intent) { 189 mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA); 190 } 191 192 /* This notification will arrive on the background thread */ 193 public void onAccountsUpdated(Account[] accounts) { 194 // Refresh to catch any changed accounts 195 loadAccountsInBackground(); 196 } 197 198 /** 199 * Returns instantly if accounts and account types have already been loaded. 200 * Otherwise waits for the background thread to complete the loading. 201 */ 202 void ensureAccountsLoaded() { 203 CountDownLatch latch = mInitializationLatch; 204 if (latch == null) { 205 return; 206 } 207 while (true) { 208 try { 209 latch.await(); 210 return; 211 } catch (InterruptedException e) { 212 Thread.currentThread().interrupt(); 213 } 214 } 215 } 216 217 /** 218 * Loads account list and corresponding account types. Always called on a 219 * background thread. 220 */ 221 protected void loadAccountsInBackground() { 222 long startTime = SystemClock.currentThreadTimeMillis(); 223 224 HashMap<String, AccountType> accountTypes = Maps.newHashMap(); 225 ArrayList<Account> allAccounts = Lists.newArrayList(); 226 ArrayList<Account> writableAccounts = Lists.newArrayList(); 227 228 final AccountManager am = mAccountManager; 229 final IContentService cs = ContentResolver.getContentService(); 230 231 try { 232 final SyncAdapterType[] syncs = cs.getSyncAdapterTypes(); 233 final AuthenticatorDescription[] auths = am.getAuthenticatorTypes(); 234 235 for (SyncAdapterType sync : syncs) { 236 if (!ContactsContract.AUTHORITY.equals(sync.authority)) { 237 // Skip sync adapters that don't provide contact data. 238 continue; 239 } 240 241 // Look for the formatting details provided by each sync 242 // adapter, using the authenticator to find general resources. 243 final String type = sync.accountType; 244 final AuthenticatorDescription auth = findAuthenticator(auths, type); 245 if (auth == null) { 246 Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it."); 247 continue; 248 } 249 250 AccountType accountType; 251 if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) { 252 accountType = new GoogleAccountType(mContext, auth.packageName); 253 } else if (ExchangeAccountType.ACCOUNT_TYPE.equals(type)) { 254 accountType = new ExchangeAccountType(mContext, auth.packageName); 255 } else { 256 // TODO: use syncadapter package instead, since it provides resources 257 Log.d(TAG, "Registering external account type=" + type 258 + ", packageName=" + auth.packageName); 259 accountType = new ExternalAccountType(mContext, auth.packageName); 260 accountType.readOnly = !sync.supportsUploading(); 261 } 262 263 accountType.accountType = auth.type; 264 accountType.titleRes = auth.labelId; 265 accountType.iconRes = auth.iconId; 266 267 accountTypes.put(accountType.accountType, accountType); 268 } 269 } catch (RemoteException e) { 270 Log.w(TAG, "Problem loading accounts: " + e.toString()); 271 } 272 273 Account[] accounts = mAccountManager.getAccounts(); 274 for (Account account : accounts) { 275 boolean syncable = false; 276 try { 277 int isSyncable = cs.getIsSyncable(account, ContactsContract.AUTHORITY); 278 if (isSyncable > 0) { 279 syncable = true; 280 } 281 } catch (RemoteException e) { 282 Log.e(TAG, "Cannot obtain sync flag for account: " + account, e); 283 } 284 285 if (syncable) { 286 // Ensure we have details loaded for each account 287 final AccountType accountType = accountTypes.get(account.type); 288 if (accountType != null) { 289 allAccounts.add(account); 290 if (!accountType.readOnly) { 291 writableAccounts.add(account); 292 } 293 } 294 } 295 } 296 297 Collections.sort(allAccounts, ACCOUNT_COMPARATOR); 298 Collections.sort(writableAccounts, ACCOUNT_COMPARATOR); 299 300 // The UI will need a phone number formatter. We can preload meta data for the 301 // current locale to prevent a delay later on. 302 PhoneNumberUtil.getInstance().getAsYouTypeFormatter(Locale.getDefault().getCountry()); 303 304 long endTime = SystemClock.currentThreadTimeMillis(); 305 306 synchronized (this) { 307 mAccountTypes = accountTypes; 308 mAccounts = allAccounts; 309 mWritableAccounts = writableAccounts; 310 } 311 312 Log.i(TAG, "Loaded meta-data for " + mAccountTypes.size() + " account types, " 313 + mAccounts.size() + " accounts in " + (endTime - startTime) + "ms"); 314 315 if (mInitializationLatch != null) { 316 mInitializationLatch.countDown(); 317 mInitializationLatch = null; 318 } 319 } 320 321 /** 322 * Find a specific {@link AuthenticatorDescription} in the provided list 323 * that matches the given account type. 324 */ 325 protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths, 326 String accountType) { 327 for (AuthenticatorDescription auth : auths) { 328 if (accountType.equals(auth.type)) { 329 return auth; 330 } 331 } 332 return null; 333 } 334 335 /** 336 * Return list of all known, writable {@link Account}'s. 337 */ 338 @Override 339 public ArrayList<Account> getAccounts(boolean writableOnly) { 340 ensureAccountsLoaded(); 341 return writableOnly ? mWritableAccounts : mAccounts; 342 } 343 344 /** 345 * Find the best {@link DataKind} matching the requested 346 * {@link AccountType#accountType} and {@link DataKind#mimeType}. If no 347 * direct match found, we try searching {@link FallbackAccountType}. 348 */ 349 @Override 350 public DataKind getKindOrFallback(String accountType, String mimeType) { 351 ensureAccountsLoaded(); 352 DataKind kind = null; 353 354 // Try finding account type and kind matching request 355 final AccountType type = mAccountTypes.get(accountType); 356 if (type != null) { 357 kind = type.getKindForMimetype(mimeType); 358 } 359 360 if (kind == null) { 361 // Nothing found, so try fallback as last resort 362 kind = mFallbackAccountType.getKindForMimetype(mimeType); 363 } 364 365 if (kind == null) { 366 Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType); 367 } 368 369 return kind; 370 } 371 372 /** 373 * Return {@link AccountType} for the given account type. 374 */ 375 @Override 376 public AccountType getAccountType(String accountType) { 377 ensureAccountsLoaded(); 378 synchronized (this) { 379 AccountType type = mAccountTypes.get(accountType); 380 return type != null ? type : mFallbackAccountType; 381 } 382 } 383} 384