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 */ getInstance(Context context)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 flush()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 ResolveCache(Context context)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 mimetype and intent, 121 * or create and populate a new one if it doesn't exist. 122 */ getEntry(String mimeType, Intent intent)123 protected Entry getEntry(String mimeType, Intent intent) { 124 Entry entry = mCache.get(mimeType); 125 if (entry != null) return entry; 126 entry = new Entry(); 127 128 if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType) 129 && !PhoneCapabilityTester.isSipPhone(mContext)) { 130 intent = null; 131 } 132 133 if (intent != null) { 134 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent, 135 PackageManager.MATCH_DEFAULT_ONLY); 136 137 // Pick first match, otherwise best found 138 ResolveInfo bestResolve = null; 139 final int size = matches.size(); 140 if (size == 1) { 141 bestResolve = matches.get(0); 142 } else if (size > 1) { 143 bestResolve = getBestResolve(intent, matches); 144 } 145 146 if (bestResolve != null) { 147 final Drawable icon = bestResolve.loadIcon(mPackageManager); 148 149 entry.bestResolve = bestResolve; 150 entry.icon = icon; 151 } 152 } 153 154 mCache.put(mimeType, entry); 155 return entry; 156 } 157 158 /** 159 * Best {@link ResolveInfo} when multiple found. Ties are broken by 160 * selecting first from the {@link QuickContactActivity#sPreferResolve} list of 161 * preferred packages, second by apps that live on the system partition, 162 * otherwise the app from the top of the list. This is 163 * <strong>only</strong> used for selecting a default icon for 164 * displaying in the track, and does not shortcut the system 165 * {@link Intent} disambiguation dialog. 166 */ getBestResolve(Intent intent, List<ResolveInfo> matches)167 protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) { 168 // Try finding preferred activity, otherwise detect disambig 169 final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent, 170 PackageManager.MATCH_DEFAULT_ONLY); 171 final boolean foundDisambig = (foundResolve.match & 172 IntentFilter.MATCH_CATEGORY_MASK) == 0; 173 174 if (!foundDisambig) { 175 // Found concrete match, so return directly 176 return foundResolve; 177 } 178 179 // Accept any package from prefer list, otherwise first system app 180 ResolveInfo firstSystem = null; 181 for (ResolveInfo info : matches) { 182 final boolean isSystem = (info.activityInfo.applicationInfo.flags 183 & ApplicationInfo.FLAG_SYSTEM) != 0; 184 final boolean isPrefer = sPreferResolve 185 .contains(info.activityInfo.applicationInfo.packageName); 186 187 if (isPrefer) return info; 188 if (isSystem && firstSystem == null) firstSystem = info; 189 } 190 191 // Return first system found, otherwise first from list 192 return firstSystem != null ? firstSystem : matches.get(0); 193 } 194 195 /** 196 * Check {@link PackageManager} to see if any apps offer to handle the 197 * given {@link Intent}. 198 */ hasResolve(String mimeType, Intent intent)199 public boolean hasResolve(String mimeType, Intent intent) { 200 return getEntry(mimeType, intent).bestResolve != null; 201 } 202 203 /** 204 * Return the best icon for the given {@link Action}, which is usually 205 * based on the {@link ResolveInfo} found through a 206 * {@link PackageManager} query. 207 */ getIcon(String mimeType, Intent intent)208 public Drawable getIcon(String mimeType, Intent intent) { 209 return getEntry(mimeType, intent).icon; 210 } 211 clear()212 public void clear() { 213 mCache.clear(); 214 } 215 } 216