blob: f664fb1d13d87f5efca857872da544848ac473b9 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.contacts.model;
import com.android.contacts.model.ContactsSource.DataKind;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import com.google.android.collect.Sets;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.accounts.OnAccountsUpdateListener;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentService;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SyncAdapterType;
import android.content.pm.PackageManager;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.util.Log;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
/**
* Singleton holder for all parsed {@link ContactsSource} available on the
* system, typically filled through {@link PackageManager} queries.
*/
public class Sources extends BroadcastReceiver implements OnAccountsUpdateListener {
private static final String TAG = "Sources";
private Context mApplicationContext;
private AccountManager mAccountManager;
private ContactsSource mFallbackSource = null;
private HashMap<String, ContactsSource> mSources = Maps.newHashMap();
private HashSet<String> mKnownPackages = Sets.newHashSet();
private static SoftReference<Sources> sInstance = null;
/**
* Requests the singleton instance of {@link Sources} with data bound from
* the available authenticators. This method blocks until its interaction
* with {@link AccountManager} is finished, so don't call from a UI thread.
*/
public static synchronized Sources getInstance(Context context) {
Sources sources = sInstance == null ? null : sInstance.get();
if (sources == null) {
sources = new Sources(context);
sInstance = new SoftReference<Sources>(sources);
}
return sources;
}
/**
* Internal constructor that only performs initial parsing.
*/
private Sources(Context context) {
mApplicationContext = context.getApplicationContext();
mAccountManager = AccountManager.get(mApplicationContext);
// Create fallback contacts source for on-phone contacts
mFallbackSource = new FallbackSource();
queryAccounts();
// Request updates when packages or accounts change
final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
mApplicationContext.registerReceiver(this, filter);
mAccountManager.addOnAccountsUpdatedListener(this, null, false);
}
/** @hide exposed for unit tests */
public Sources(ContactsSource... sources) {
for (ContactsSource source : sources) {
addSource(source);
}
}
protected void addSource(ContactsSource source) {
mSources.put(source.accountType, source);
mKnownPackages.add(source.resPackageName);
}
/** {@inheritDoc} */
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
final String packageName = intent.getData().getSchemeSpecificPart();
if (Intent.ACTION_PACKAGE_REMOVED.equals(action)
|| Intent.ACTION_PACKAGE_ADDED.equals(action)
|| Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
final boolean knownPackage = mKnownPackages.contains(packageName);
if (knownPackage) {
// Invalidate cache of existing source
invalidateCache(packageName);
} else {
// Unknown source, so reload from scratch
queryAccounts();
}
}
}
protected void invalidateCache(String packageName) {
for (ContactsSource source : mSources.values()) {
if (TextUtils.equals(packageName, source.resPackageName)) {
// Invalidate any cache for the changed package
source.invalidateCache();
}
}
}
/** {@inheritDoc} */
public void onAccountsUpdated(Account[] accounts) {
// Refresh to catch any changed accounts
queryAccounts();
}
/**
* Blocking call to load all {@link AuthenticatorDescription} known by the
* {@link AccountManager} on the system.
*/
protected synchronized void queryAccounts() {
mSources.clear();
mKnownPackages.clear();
final AccountManager am = mAccountManager;
final IContentService cs = ContentResolver.getContentService();
try {
final SyncAdapterType[] syncs = cs.getSyncAdapterTypes();
final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
for (SyncAdapterType sync : syncs) {
if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
// Skip sync adapters that don't provide contact data.
continue;
}
// Look for the formatting details provided by each sync
// adapter, using the authenticator to find general resources.
final String accountType = sync.accountType;
final AuthenticatorDescription auth = findAuthenticator(auths, accountType);
ContactsSource source;
if (GoogleSource.ACCOUNT_TYPE.equals(accountType)) {
source = new GoogleSource(auth.packageName);
} else if (ExchangeSource.ACCOUNT_TYPE.equals(accountType)) {
source = new ExchangeSource(auth.packageName);
} else {
// TODO: use syncadapter package instead, since it provides resources
Log.d(TAG, "Creating external source for type=" + accountType
+ ", packageName=" + auth.packageName);
source = new ExternalSource(auth.packageName);
source.readOnly = !sync.supportsUploading();
}
source.accountType = auth.type;
source.titleRes = auth.labelId;
source.iconRes = auth.iconId;
addSource(source);
}
} catch (RemoteException e) {
Log.w(TAG, "Problem loading accounts: " + e.toString());
}
}
/**
* Find a specific {@link AuthenticatorDescription} in the provided list
* that matches the given account type.
*/
protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
String accountType) {
for (AuthenticatorDescription auth : auths) {
if (accountType.equals(auth.type)) {
return auth;
}
}
throw new IllegalStateException("Couldn't find authenticator for specific account type");
}
/**
* Return list of all known, writable {@link ContactsSource}. Sources
* returned may require inflation before they can be used.
*/
public ArrayList<Account> getAccounts(boolean writableOnly) {
final AccountManager am = mAccountManager;
final Account[] accounts = am.getAccounts();
final ArrayList<Account> matching = Lists.newArrayList();
for (Account account : accounts) {
// Ensure we have details loaded for each account
final ContactsSource source = getInflatedSource(account.type,
ContactsSource.LEVEL_SUMMARY);
final boolean hasContacts = source != null;
final boolean matchesWritable = (!writableOnly || (writableOnly && !source.readOnly));
if (hasContacts && matchesWritable) {
matching.add(account);
}
}
return matching;
}
/**
* Find the best {@link DataKind} matching the requested
* {@link ContactsSource#accountType} and {@link DataKind#mimeType}. If no
* direct match found, we try searching {@link #mFallbackSource}.
*/
public DataKind getKindOrFallback(String accountType, String mimeType, Context context,
int inflateLevel) {
DataKind kind = null;
// Try finding source and kind matching request
final ContactsSource source = mSources.get(accountType);
if (source != null) {
source.ensureInflated(context, inflateLevel);
kind = source.getKindForMimetype(mimeType);
}
if (kind == null) {
// Nothing found, so try fallback as last resort
mFallbackSource.ensureInflated(context, inflateLevel);
kind = mFallbackSource.getKindForMimetype(mimeType);
}
if (kind == null) {
Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType);
}
return kind;
}
/**
* Return {@link ContactsSource} for the given account type.
*/
public ContactsSource getInflatedSource(String accountType, int inflateLevel) {
// Try finding specific source, otherwise use fallback
ContactsSource source = mSources.get(accountType);
if (source == null) source = mFallbackSource;
if (source.isInflated(inflateLevel)) {
// Already inflated, so return directly
return source;
} else {
// Not inflated, but requested that we force-inflate
source.ensureInflated(mApplicationContext, inflateLevel);
return source;
}
}
}