Home | History | Annotate | Download | only in quickcontact
      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 import android.text.TextUtils;
     29 
     30 import com.android.contacts.util.PhoneCapabilityTester;
     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             "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, String name) {
    210         final ResolveInfo info = getEntry(action).bestResolve;
    211         final CharSequence infoStr = info != null ? info.loadLabel(mPackageManager) : null;
    212         CharSequence actionDesc = action.getSubtitle();
    213         CharSequence strs[];
    214         if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(actionDesc)) {
    215             strs = new CharSequence[]{infoStr, name, actionDesc};
    216         } else {
    217             strs = new CharSequence[]{infoStr, action.getBody()};
    218         }
    219         CharSequence desc = TextUtils.join(" ", strs);
    220         return !TextUtils.isEmpty(desc) ? desc : null;
    221     }
    222 
    223     /**
    224      * Return the best icon for the given {@link Action}, which is usually
    225      * based on the {@link ResolveInfo} found through a
    226      * {@link PackageManager} query.
    227      */
    228     public Drawable getIcon(Action action) {
    229         return getEntry(action).icon;
    230     }
    231 
    232     public void clear() {
    233         mCache.clear();
    234     }
    235 }
    236