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