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