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