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