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     * 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 {@link Action},
121     * or create and populate a new one if it doesn't exist.
122     */
123    protected Entry getEntry(Action action) {
124        final String mimeType = action.getMimeType();
125        Entry entry = mCache.get(mimeType);
126        if (entry != null) return entry;
127        entry = new Entry();
128
129        Intent intent = action.getIntent();
130        if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)
131                && !PhoneCapabilityTester.isSipPhone(mContext)) {
132            intent = null;
133        }
134
135        if (intent != null) {
136            final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
137                    PackageManager.MATCH_DEFAULT_ONLY);
138
139            // Pick first match, otherwise best found
140            ResolveInfo bestResolve = null;
141            final int size = matches.size();
142            if (size == 1) {
143                bestResolve = matches.get(0);
144            } else if (size > 1) {
145                bestResolve = getBestResolve(intent, matches);
146            }
147
148            if (bestResolve != null) {
149                final Drawable icon = bestResolve.loadIcon(mPackageManager);
150
151                entry.bestResolve = bestResolve;
152                entry.icon = icon;
153            }
154        }
155
156        mCache.put(mimeType, entry);
157        return entry;
158    }
159
160    /**
161     * Best {@link ResolveInfo} when multiple found. Ties are broken by
162     * selecting first from the {@link QuickContactActivity#sPreferResolve} list of
163     * preferred packages, second by apps that live on the system partition,
164     * otherwise the app from the top of the list. This is
165     * <strong>only</strong> used for selecting a default icon for
166     * displaying in the track, and does not shortcut the system
167     * {@link Intent} disambiguation dialog.
168     */
169    protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) {
170        // Try finding preferred activity, otherwise detect disambig
171        final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent,
172                PackageManager.MATCH_DEFAULT_ONLY);
173        final boolean foundDisambig = (foundResolve.match &
174                IntentFilter.MATCH_CATEGORY_MASK) == 0;
175
176        if (!foundDisambig) {
177            // Found concrete match, so return directly
178            return foundResolve;
179        }
180
181        // Accept any package from prefer list, otherwise first system app
182        ResolveInfo firstSystem = null;
183        for (ResolveInfo info : matches) {
184            final boolean isSystem = (info.activityInfo.applicationInfo.flags
185                    & ApplicationInfo.FLAG_SYSTEM) != 0;
186            final boolean isPrefer = sPreferResolve
187                    .contains(info.activityInfo.applicationInfo.packageName);
188
189            if (isPrefer) return info;
190            if (isSystem && firstSystem == null) firstSystem = info;
191        }
192
193        // Return first system found, otherwise first from list
194        return firstSystem != null ? firstSystem : matches.get(0);
195    }
196
197    /**
198     * Check {@link PackageManager} to see if any apps offer to handle the
199     * given {@link Action}.
200     */
201    public boolean hasResolve(Action action) {
202        return getEntry(action).bestResolve != null;
203    }
204
205    /**
206     * Find the best description for the given {@link Action}, usually used
207     * for accessibility purposes.
208     */
209    public CharSequence getDescription(Action action) {
210        final CharSequence actionSubtitle = action.getSubtitle();
211        final ResolveInfo info = getEntry(action).bestResolve;
212        if (info != null) {
213            return info.loadLabel(mPackageManager);
214        } else if (!TextUtils.isEmpty(actionSubtitle)) {
215            return actionSubtitle;
216        } else {
217            return null;
218        }
219    }
220
221    /**
222     * Return the best icon for the given {@link Action}, which is usually
223     * based on the {@link ResolveInfo} found through a
224     * {@link PackageManager} query.
225     */
226    public Drawable getIcon(Action action) {
227        return getEntry(action).icon;
228    }
229
230    public void clear() {
231        mCache.clear();
232    }
233}
234