blob: ec59b942534ed1cb774e19228755fe58e3171e40 [file] [log] [blame]
/*
* Copyright (C) 2010 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.quickcontact;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.text.TextUtils;
import com.android.contacts.util.PhoneCapabilityTester;
import com.google.common.collect.Sets;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
* Internally hold a cache of scaled icons based on {@link PackageManager}
* queries, keyed internally on MIME-type.
*/
public class ResolveCache {
/**
* Specific list {@link ApplicationInfo#packageName} of apps that are
* prefered <strong>only</strong> for the purposes of default icons when
* multiple {@link ResolveInfo} are found to match. This only happens when
* the user has not selected a default app yet, and they will still be
* presented with the system disambiguation dialog.
* If several of this list match (e.g. Android Browser vs. Chrome), we will pick either one
*/
private static final HashSet<String> sPreferResolve = Sets.newHashSet(
"com.android.email",
"com.google.android.email",
"com.android.phone",
"com.google.android.apps.maps",
"com.android.chrome",
"com.google.android.browser",
"com.android.browser");
private final Context mContext;
private final PackageManager mPackageManager;
private static ResolveCache sInstance;
/**
* Returns an instance of the ResolveCache. Only one internal instance is kept, so
* the argument packageManagers is ignored for all but the first call
*/
public synchronized static ResolveCache getInstance(Context context) {
if (sInstance == null) {
final Context applicationContext = context.getApplicationContext();
sInstance = new ResolveCache(applicationContext);
// Register for package-changes so that we can flush our cache
final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
applicationContext.registerReceiver(sInstance.mPackageIntentReceiver, filter);
}
return sInstance;
}
private synchronized static void flush() {
sInstance = null;
}
/**
* Called anytime a package is installed, uninstalled etc, so that we can wipe our cache
*/
private BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
flush();
}
};
/**
* Cached entry holding the best {@link ResolveInfo} for a specific
* MIME-type, along with a {@link SoftReference} to its icon.
*/
private static class Entry {
public ResolveInfo bestResolve;
public Drawable icon;
}
private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
private ResolveCache(Context context) {
mContext = context;
mPackageManager = context.getPackageManager();
}
/**
* Get the {@link Entry} best associated with the given {@link Action},
* or create and populate a new one if it doesn't exist.
*/
protected Entry getEntry(Action action) {
final String mimeType = action.getMimeType();
Entry entry = mCache.get(mimeType);
if (entry != null) return entry;
entry = new Entry();
Intent intent = action.getIntent();
if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)
&& !PhoneCapabilityTester.isSipPhone(mContext)) {
intent = null;
}
if (intent != null) {
final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY);
// Pick first match, otherwise best found
ResolveInfo bestResolve = null;
final int size = matches.size();
if (size == 1) {
bestResolve = matches.get(0);
} else if (size > 1) {
bestResolve = getBestResolve(intent, matches);
}
if (bestResolve != null) {
final Drawable icon = bestResolve.loadIcon(mPackageManager);
entry.bestResolve = bestResolve;
entry.icon = icon;
}
}
mCache.put(mimeType, entry);
return entry;
}
/**
* Best {@link ResolveInfo} when multiple found. Ties are broken by
* selecting first from the {@link QuickContactActivity#sPreferResolve} list of
* preferred packages, second by apps that live on the system partition,
* otherwise the app from the top of the list. This is
* <strong>only</strong> used for selecting a default icon for
* displaying in the track, and does not shortcut the system
* {@link Intent} disambiguation dialog.
*/
protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) {
// Try finding preferred activity, otherwise detect disambig
final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent,
PackageManager.MATCH_DEFAULT_ONLY);
final boolean foundDisambig = (foundResolve.match &
IntentFilter.MATCH_CATEGORY_MASK) == 0;
if (!foundDisambig) {
// Found concrete match, so return directly
return foundResolve;
}
// Accept any package from prefer list, otherwise first system app
ResolveInfo firstSystem = null;
for (ResolveInfo info : matches) {
final boolean isSystem = (info.activityInfo.applicationInfo.flags
& ApplicationInfo.FLAG_SYSTEM) != 0;
final boolean isPrefer = sPreferResolve
.contains(info.activityInfo.applicationInfo.packageName);
if (isPrefer) return info;
if (isSystem && firstSystem == null) firstSystem = info;
}
// Return first system found, otherwise first from list
return firstSystem != null ? firstSystem : matches.get(0);
}
/**
* Check {@link PackageManager} to see if any apps offer to handle the
* given {@link Action}.
*/
public boolean hasResolve(Action action) {
return getEntry(action).bestResolve != null;
}
/**
* Find the best description for the given {@link Action}, usually used
* for accessibility purposes.
*/
public CharSequence getDescription(Action action) {
final CharSequence actionSubtitle = action.getSubtitle();
final ResolveInfo info = getEntry(action).bestResolve;
if (info != null) {
return info.loadLabel(mPackageManager);
} else if (!TextUtils.isEmpty(actionSubtitle)) {
return actionSubtitle;
} else {
return null;
}
}
/**
* Return the best icon for the given {@link Action}, which is usually
* based on the {@link ResolveInfo} found through a
* {@link PackageManager} query.
*/
public Drawable getIcon(Action action) {
return getEntry(action).icon;
}
public void clear() {
mCache.clear();
}
}