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 "org.chromium.webview_shell", 61 "com.google.android.browser", 62 "com.android.browser"); 63 64 private final Context mContext; 65 private final PackageManager mPackageManager; 66 67 private static ResolveCache sInstance; 68 69 /** 70 * Returns an instance of the ResolveCache. Only one internal instance is kept, so 71 * the argument packageManagers is ignored for all but the first call 72 */ 73 public synchronized static ResolveCache getInstance(Context context) { 74 if (sInstance == null) { 75 final Context applicationContext = context.getApplicationContext(); 76 sInstance = new ResolveCache(applicationContext); 77 78 // Register for package-changes so that we can flush our cache 79 final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 80 filter.addAction(Intent.ACTION_PACKAGE_REPLACED); 81 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 82 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 83 filter.addDataScheme("package"); 84 applicationContext.registerReceiver(sInstance.mPackageIntentReceiver, filter); 85 } 86 return sInstance; 87 } 88 89 private synchronized static void flush() { 90 sInstance = null; 91 } 92 93 /** 94 * Called anytime a package is installed, uninstalled etc, so that we can wipe our cache 95 */ 96 private BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() { 97 @Override 98 public void onReceive(Context context, Intent intent) { 99 flush(); 100 } 101 }; 102 103 /** 104 * Cached entry holding the best {@link ResolveInfo} for a specific 105 * MIME-type, along with a {@link SoftReference} to its icon. 106 */ 107 private static class Entry { 108 public ResolveInfo bestResolve; 109 public Drawable icon; 110 } 111 112 private HashMap<String, Entry> mCache = new HashMap<String, Entry>(); 113 114 115 private ResolveCache(Context context) { 116 mContext = context; 117 mPackageManager = context.getPackageManager(); 118 } 119 120 /** 121 * Get the {@link Entry} best associated with the given mimetype and intent, 122 * or create and populate a new one if it doesn't exist. 123 */ 124 protected Entry getEntry(String mimeType, Intent intent) { 125 Entry entry = mCache.get(mimeType); 126 if (entry != null) return entry; 127 entry = new Entry(); 128 129 if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType) 130 && !PhoneCapabilityTester.isSipPhone(mContext)) { 131 intent = null; 132 } 133 134 if (intent != null) { 135 final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent, 136 PackageManager.MATCH_DEFAULT_ONLY); 137 138 // Pick first match, otherwise best found 139 ResolveInfo bestResolve = null; 140 final int size = matches.size(); 141 if (size == 1) { 142 bestResolve = matches.get(0); 143 } else if (size > 1) { 144 bestResolve = getBestResolve(intent, matches); 145 } 146 147 if (bestResolve != null) { 148 final Drawable icon = bestResolve.loadIcon(mPackageManager); 149 150 entry.bestResolve = bestResolve; 151 entry.icon = icon; 152 } 153 } 154 155 mCache.put(mimeType, entry); 156 return entry; 157 } 158 159 /** 160 * Best {@link ResolveInfo} when multiple found. Ties are broken by 161 * selecting first from the {@link QuickContactActivity#sPreferResolve} list of 162 * preferred packages, second by apps that live on the system partition, 163 * otherwise the app from the top of the list. This is 164 * <strong>only</strong> used for selecting a default icon for 165 * displaying in the track, and does not shortcut the system 166 * {@link Intent} disambiguation dialog. 167 */ 168 protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) { 169 // Try finding preferred activity, otherwise detect disambig 170 final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent, 171 PackageManager.MATCH_DEFAULT_ONLY); 172 final boolean foundDisambig = (foundResolve.match & 173 IntentFilter.MATCH_CATEGORY_MASK) == 0; 174 175 if (!foundDisambig) { 176 // Found concrete match, so return directly 177 return foundResolve; 178 } 179 180 // Accept any package from prefer list, otherwise first system app 181 ResolveInfo firstSystem = null; 182 for (ResolveInfo info : matches) { 183 final boolean isSystem = (info.activityInfo.applicationInfo.flags 184 & ApplicationInfo.FLAG_SYSTEM) != 0; 185 final boolean isPrefer = sPreferResolve 186 .contains(info.activityInfo.applicationInfo.packageName); 187 188 if (isPrefer) return info; 189 if (isSystem && firstSystem == null) firstSystem = info; 190 } 191 192 // Return first system found, otherwise first from list 193 return firstSystem != null ? firstSystem : matches.get(0); 194 } 195 196 /** 197 * Check {@link PackageManager} to see if any apps offer to handle the 198 * given {@link Intent}. 199 */ 200 public boolean hasResolve(String mimeType, Intent intent) { 201 return getEntry(mimeType, intent).bestResolve != null; 202 } 203 204 /** 205 * Return the best icon for the given {@link Action}, which is usually 206 * based on the {@link ResolveInfo} found through a 207 * {@link PackageManager} query. 208 */ 209 public Drawable getIcon(String mimeType, Intent intent) { 210 return getEntry(mimeType, intent).icon; 211 } 212 213 public void clear() { 214 mCache.clear(); 215 } 216 } 217