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 
29 import com.android.contacts.util.PhoneCapabilityTester;
30 
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      */
getInstance(Context context)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 
flush()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 
ResolveCache(Context context)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      */
getEntry(String mimeType, Intent intent)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      */
getBestResolve(Intent intent, List<ResolveInfo> matches)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      */
hasResolve(String mimeType, Intent intent)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      */
getIcon(String mimeType, Intent intent)209     public Drawable getIcon(String mimeType, Intent intent) {
210         return getEntry(mimeType, intent).icon;
211     }
212 
clear()213     public void clear() {
214         mCache.clear();
215     }
216 }
217