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