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 17 package com.android.loaderapp.model; 18 19 import com.android.loaderapp.model.ContactsSource.DataKind; 20 import com.google.android.collect.Lists; 21 import com.google.android.collect.Maps; 22 import com.google.android.collect.Sets; 23 24 import android.accounts.Account; 25 import android.accounts.AccountManager; 26 import android.accounts.AuthenticatorDescription; 27 import android.accounts.OnAccountsUpdateListener; 28 import android.content.BroadcastReceiver; 29 import android.content.ContentResolver; 30 import android.content.Context; 31 import android.content.IContentService; 32 import android.content.Intent; 33 import android.content.IntentFilter; 34 import android.content.SyncAdapterType; 35 import android.content.pm.PackageManager; 36 import android.os.RemoteException; 37 import android.provider.ContactsContract; 38 import android.text.TextUtils; 39 import android.util.Log; 40 41 import java.lang.ref.SoftReference; 42 import java.util.ArrayList; 43 import java.util.HashMap; 44 import java.util.HashSet; 45 import java.util.Locale; 46 47 /** 48 * Singleton holder for all parsed {@link ContactsSource} available on the 49 * system, typically filled through {@link PackageManager} queries. 50 */ 51 public class Sources extends BroadcastReceiver implements OnAccountsUpdateListener { 52 private static final String TAG = "Sources"; 53 54 private Context mContext; 55 private Context mApplicationContext; 56 private AccountManager mAccountManager; 57 58 private ContactsSource mFallbackSource = null; 59 60 private HashMap<String, ContactsSource> mSources = Maps.newHashMap(); 61 private HashSet<String> mKnownPackages = Sets.newHashSet(); 62 63 private static SoftReference<Sources> sInstance = null; 64 65 /** 66 * Requests the singleton instance of {@link Sources} with data bound from 67 * the available authenticators. This method blocks until its interaction 68 * with {@link AccountManager} is finished, so don't call from a UI thread. 69 */ 70 public static synchronized Sources getInstance(Context context) { 71 Sources sources = sInstance == null ? null : sInstance.get(); 72 if (sources == null) { 73 sources = new Sources(context); 74 sInstance = new SoftReference<Sources>(sources); 75 } 76 return sources; 77 } 78 79 /** 80 * Internal constructor that only performs initial parsing. 81 */ 82 private Sources(Context context) { 83 mContext = context; 84 mApplicationContext = context.getApplicationContext(); 85 mAccountManager = AccountManager.get(mApplicationContext); 86 87 // Create fallback contacts source for on-phone contacts 88 mFallbackSource = new FallbackSource(); 89 90 queryAccounts(); 91 92 // Request updates when packages or accounts change 93 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 94 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 95 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 96 filter.addDataScheme("package"); 97 mApplicationContext.registerReceiver(this, filter); 98 IntentFilter sdFilter = new IntentFilter(); 99 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); 100 sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); 101 mApplicationContext.registerReceiver(this, sdFilter); 102 103 // Request updates when locale is changed so that the order of each field will 104 // be able to be changed on the locale change. 105 filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); 106 mApplicationContext.registerReceiver(this, filter); 107 108 mAccountManager.addOnAccountsUpdatedListener(this, null, false); 109 } 110 111 /** @hide exposed for unit tests */ 112 public Sources(ContactsSource... sources) { 113 for (ContactsSource source : sources) { 114 addSource(source); 115 } 116 } 117 118 protected void addSource(ContactsSource source) { 119 mSources.put(source.accountType, source); 120 mKnownPackages.add(source.resPackageName); 121 } 122 123 /** {@inheritDoc} */ 124 @Override 125 public void onReceive(Context context, Intent intent) { 126 final String action = intent.getAction(); 127 128 if (Intent.ACTION_PACKAGE_REMOVED.equals(action) 129 || Intent.ACTION_PACKAGE_ADDED.equals(action) 130 || Intent.ACTION_PACKAGE_CHANGED.equals(action) || 131 Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action) || 132 Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { 133 String[] pkgList = null; 134 // Handle applications on sdcard. 135 if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action) || 136 Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { 137 pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); 138 } else { 139 final String packageName = intent.getData().getSchemeSpecificPart(); 140 pkgList = new String[] { packageName }; 141 } 142 if (pkgList != null) { 143 for (String packageName : pkgList) { 144 final boolean knownPackage = mKnownPackages.contains(packageName); 145 if (knownPackage) { 146 // Invalidate cache of existing source 147 invalidateCache(packageName); 148 } else { 149 // Unknown source, so reload from scratch 150 queryAccounts(); 151 } 152 } 153 } 154 } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { 155 invalidateAllCache(); 156 } 157 } 158 159 protected void invalidateCache(String packageName) { 160 for (ContactsSource source : mSources.values()) { 161 if (TextUtils.equals(packageName, source.resPackageName)) { 162 // Invalidate any cache for the changed package 163 source.invalidateCache(); 164 } 165 } 166 } 167 168 protected void invalidateAllCache() { 169 mFallbackSource.invalidateCache(); 170 for (ContactsSource source : mSources.values()) { 171 source.invalidateCache(); 172 } 173 } 174 175 /** {@inheritDoc} */ 176 public void onAccountsUpdated(Account[] accounts) { 177 // Refresh to catch any changed accounts 178 queryAccounts(); 179 } 180 181 /** 182 * Blocking call to load all {@link AuthenticatorDescription} known by the 183 * {@link AccountManager} on the system. 184 */ 185 protected synchronized void queryAccounts() { 186 mSources.clear(); 187 mKnownPackages.clear(); 188 189 final AccountManager am = mAccountManager; 190 final IContentService cs = ContentResolver.getContentService(); 191 192 try { 193 final SyncAdapterType[] syncs = cs.getSyncAdapterTypes(); 194 final AuthenticatorDescription[] auths = am.getAuthenticatorTypes(); 195 196 for (SyncAdapterType sync : syncs) { 197 if (!ContactsContract.AUTHORITY.equals(sync.authority)) { 198 // Skip sync adapters that don't provide contact data. 199 continue; 200 } 201 202 // Look for the formatting details provided by each sync 203 // adapter, using the authenticator to find general resources. 204 final String accountType = sync.accountType; 205 final AuthenticatorDescription auth = findAuthenticator(auths, accountType); 206 207 ContactsSource source; 208 if (GoogleSource.ACCOUNT_TYPE.equals(accountType)) { 209 source = new GoogleSource(auth.packageName); 210 } else if (ExchangeSource.ACCOUNT_TYPE.equals(accountType)) { 211 source = new ExchangeSource(auth.packageName); 212 } else { 213 // TODO: use syncadapter package instead, since it provides resources 214 Log.d(TAG, "Creating external source for type=" + accountType 215 + ", packageName=" + auth.packageName); 216 source = new ExternalSource(auth.packageName); 217 source.readOnly = !sync.supportsUploading(); 218 } 219 220 source.accountType = auth.type; 221 source.titleRes = auth.labelId; 222 source.iconRes = auth.iconId; 223 224 addSource(source); 225 } 226 } catch (RemoteException e) { 227 Log.w(TAG, "Problem loading accounts: " + e.toString()); 228 } 229 } 230 231 /** 232 * Find a specific {@link AuthenticatorDescription} in the provided list 233 * that matches the given account type. 234 */ 235 protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths, 236 String accountType) { 237 for (AuthenticatorDescription auth : auths) { 238 if (accountType.equals(auth.type)) { 239 return auth; 240 } 241 } 242 throw new IllegalStateException("Couldn't find authenticator for specific account type"); 243 } 244 245 /** 246 * Return list of all known, writable {@link ContactsSource}. Sources 247 * returned may require inflation before they can be used. 248 */ 249 public ArrayList<Account> getAccounts(boolean writableOnly) { 250 final AccountManager am = mAccountManager; 251 final Account[] accounts = am.getAccounts(); 252 final ArrayList<Account> matching = Lists.newArrayList(); 253 254 for (Account account : accounts) { 255 // Ensure we have details loaded for each account 256 final ContactsSource source = getInflatedSource(account.type, 257 ContactsSource.LEVEL_SUMMARY); 258 final boolean hasContacts = source != null; 259 final boolean matchesWritable = (!writableOnly || (writableOnly && !source.readOnly)); 260 if (hasContacts && matchesWritable) { 261 matching.add(account); 262 } 263 } 264 return matching; 265 } 266 267 /** 268 * Find the best {@link DataKind} matching the requested 269 * {@link ContactsSource#accountType} and {@link DataKind#mimeType}. If no 270 * direct match found, we try searching {@link #mFallbackSource}. 271 * When fourceRefresh is set to true, cache is refreshed and inflation of each 272 * EditField will occur. 273 */ 274 public DataKind getKindOrFallback(String accountType, String mimeType, Context context, 275 int inflateLevel) { 276 DataKind kind = null; 277 278 // Try finding source and kind matching request 279 final ContactsSource source = mSources.get(accountType); 280 if (source != null) { 281 source.ensureInflated(context, inflateLevel); 282 kind = source.getKindForMimetype(mimeType); 283 } 284 285 if (kind == null) { 286 // Nothing found, so try fallback as last resort 287 mFallbackSource.ensureInflated(context, inflateLevel); 288 kind = mFallbackSource.getKindForMimetype(mimeType); 289 } 290 291 if (kind == null) { 292 Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType); 293 } 294 295 return kind; 296 } 297 298 /** 299 * Return {@link ContactsSource} for the given account type. 300 */ 301 public ContactsSource getInflatedSource(String accountType, int inflateLevel) { 302 // Try finding specific source, otherwise use fallback 303 ContactsSource source = mSources.get(accountType); 304 if (source == null) source = mFallbackSource; 305 306 if (source.isInflated(inflateLevel)) { 307 // Already inflated, so return directly 308 return source; 309 } else { 310 // Not inflated, but requested that we force-inflate 311 source.ensureInflated(mContext, inflateLevel); 312 return source; 313 } 314 } 315 } 316