ResolveCache.java revision 38458634f8e474786264050d3a1580e03a680fff
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
17package com.android.contacts.quickcontact;
18
19import com.android.contacts.util.PhoneCapabilityTester;
20import com.google.android.collect.Sets;
21
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.pm.ApplicationInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.ResolveInfo;
28import android.graphics.drawable.Drawable;
29import android.provider.ContactsContract.CommonDataKinds.SipAddress;
30import android.text.TextUtils;
31
32import java.lang.ref.SoftReference;
33import java.util.HashMap;
34import java.util.HashSet;
35import 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 */
41public 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