1/* 2 * Copyright (C) 2010 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.quickcontact; 18 19import com.android.contacts.util.PhoneCapabilityTester; 20import com.google.android.collect.Sets; 21 22import android.content.BroadcastReceiver; 23import android.content.Context; 24import android.content.Intent; 25import android.content.IntentFilter; 26import android.content.pm.ApplicationInfo; 27import android.content.pm.PackageManager; 28import android.content.pm.ResolveInfo; 29import android.graphics.drawable.Drawable; 30import android.provider.ContactsContract.CommonDataKinds.SipAddress; 31import android.text.TextUtils; 32 33import java.lang.ref.SoftReference; 34import java.util.HashMap; 35import java.util.HashSet; 36import java.util.List; 37 38/** 39 * Internally hold a cache of scaled icons based on {@link PackageManager} 40 * queries, keyed internally on MIME-type. 41 */ 42public class ResolveCache { 43 /** 44 * Specific list {@link ApplicationInfo#packageName} of apps that are 45 * prefered <strong>only</strong> for the purposes of default icons when 46 * multiple {@link ResolveInfo} are found to match. This only happens when 47 * the user has not selected a default app yet, and they will still be 48 * presented with the system disambiguation dialog. 49 */ 50 private static final HashSet<String> sPreferResolve = Sets.newHashSet( 51 "com.android.email", 52 "com.android.calendar", 53 "com.android.contacts", 54 "com.android.mms", 55 "com.android.phone", 56 "com.android.browser"); 57 58 private final Context mContext; 59 private final PackageManager mPackageManager; 60 61 private static ResolveCache sInstance; 62 63 /** 64 * Returns an instance of the ResolveCache. Only one internal instance is kept, so 65 * the argument packageManagers is ignored for all but the first call 66 */ 67 public synchronized static ResolveCache getInstance(Context context) { 68 if (sInstance == null) { 69 final Context applicationContext = context.getApplicationContext(); 70 sInstance = new ResolveCache(applicationContext); 71 72 // Register for package-changes so that we can flush our cache 73 final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 74 filter.addAction(Intent.ACTION_PACKAGE_REPLACED); 75 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 76 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 77 filter.addDataScheme("package"); 78 applicationContext.registerReceiver(sInstance.mPackageIntentReceiver, filter); 79 } 80 return sInstance; 81 } 82 83 private synchronized static void flush() { 84 sInstance = null; 85 } 86 87 /** 88 * Called anytime a package is installed, uninstalled etc, so that we can wipe our cache 89 */ 90 private BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() { 91 @Override 92 public void onReceive(Context context, Intent intent) { 93 flush(); 94 } 95 }; 96 97 /** 98 * Cached entry holding the best {@link ResolveInfo} for a specific 99 * MIME-type, along with a {@link SoftReference} to its icon. 100 */ 101 private static class Entry { 102 public ResolveInfo bestResolve; 103 public Drawable icon; 104 } 105 106 private HashMap<String, Entry> mCache = new HashMap<String, Entry>(); 107 108 109 private ResolveCache(Context context) { 110 mContext = context; 111 mPackageManager = context.getPackageManager(); 112 } 113 114 /** 115 * Get the {@link Entry} best associated with the given {@link Action}, 116 * or create and populate a new one if it doesn't exist. 117 */ 118 protected Entry getEntry(Action action) { 119 final String mimeType = action.getMimeType(); 120 Entry entry = mCache.get(mimeType); 121 if (entry != null) return entry; 122 entry = new Entry(); 123 124 Intent intent = action.getIntent(); 125 if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType) 126 && !PhoneCapabilityTester.isSipPhone(mContext)) { 127 intent = null; 128 } 129 130 if (intent != null) { 131 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent, 132 PackageManager.MATCH_DEFAULT_ONLY); 133 134 // Pick first match, otherwise best found 135 ResolveInfo bestResolve = null; 136 final int size = matches.size(); 137 if (size == 1) { 138 bestResolve = matches.get(0); 139 } else if (size > 1) { 140 bestResolve = getBestResolve(intent, matches); 141 } 142 143 if (bestResolve != null) { 144 final Drawable icon = bestResolve.loadIcon(mPackageManager); 145 146 entry.bestResolve = bestResolve; 147 entry.icon = icon; 148 } 149 } 150 151 mCache.put(mimeType, entry); 152 return entry; 153 } 154 155 /** 156 * Best {@link ResolveInfo} when multiple found. Ties are broken by 157 * selecting first from the {@link QuickContactActivity#sPreferResolve} list of 158 * preferred packages, second by apps that live on the system partition, 159 * otherwise the app from the top of the list. This is 160 * <strong>only</strong> used for selecting a default icon for 161 * displaying in the track, and does not shortcut the system 162 * {@link Intent} disambiguation dialog. 163 */ 164 protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) { 165 // Try finding preferred activity, otherwise detect disambig 166 final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent, 167 PackageManager.MATCH_DEFAULT_ONLY); 168 final boolean foundDisambig = (foundResolve.match & 169 IntentFilter.MATCH_CATEGORY_MASK) == 0; 170 171 if (!foundDisambig) { 172 // Found concrete match, so return directly 173 return foundResolve; 174 } 175 176 // Accept any package from prefer list, otherwise first system app 177 ResolveInfo firstSystem = null; 178 for (ResolveInfo info : matches) { 179 final boolean isSystem = (info.activityInfo.applicationInfo.flags 180 & ApplicationInfo.FLAG_SYSTEM) != 0; 181 final boolean isPrefer = sPreferResolve 182 .contains(info.activityInfo.applicationInfo.packageName); 183 184 if (isPrefer) return info; 185 if (isSystem && firstSystem == null) firstSystem = info; 186 } 187 188 // Return first system found, otherwise first from list 189 return firstSystem != null ? firstSystem : matches.get(0); 190 } 191 192 /** 193 * Check {@link PackageManager} to see if any apps offer to handle the 194 * given {@link Action}. 195 */ 196 public boolean hasResolve(Action action) { 197 return getEntry(action).bestResolve != null; 198 } 199 200 /** 201 * Find the best description for the given {@link Action}, usually used 202 * for accessibility purposes. 203 */ 204 public CharSequence getDescription(Action action) { 205 final CharSequence actionSubtitle = action.getSubtitle(); 206 final ResolveInfo info = getEntry(action).bestResolve; 207 if (info != null) { 208 return info.loadLabel(mPackageManager); 209 } else if (!TextUtils.isEmpty(actionSubtitle)) { 210 return actionSubtitle; 211 } else { 212 return null; 213 } 214 } 215 216 /** 217 * Return the best icon for the given {@link Action}, which is usually 218 * based on the {@link ResolveInfo} found through a 219 * {@link PackageManager} query. 220 */ 221 public Drawable getIcon(Action action) { 222 return getEntry(action).icon; 223 } 224 225 public void clear() { 226 mCache.clear(); 227 } 228} 229