Don't flush the ResolveCache each time. Turned inner classes into classes
Change-Id: Ic4d3a6a96988bd1979924ac4c2f807114624cf73
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b39d3f4..09d5a6e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -350,6 +350,26 @@
</intent-filter>
</activity>
+ <!-- Flushes the QuickContact IntentCache -->
+ <receiver android:name=".quickcontact.PackageIntentReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_ADDED" />
+ <data android:scheme="package" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_REPLACED" />
+ <data android:scheme="package" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_REMOVED" />
+ <data android:scheme="package" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_CHANGED" />
+ <data android:scheme="package" />
+ </intent-filter>
+ </receiver>
+
<activity-alias android:name="ContactShortcut"
android:targetActivity=".activities.ContactSelectionActivity"
android:label="@string/shortcutContact"
diff --git a/src/com/android/contacts/Collapser.java b/src/com/android/contacts/Collapser.java
index d072dce..5b5d5a0 100644
--- a/src/com/android/contacts/Collapser.java
+++ b/src/com/android/contacts/Collapser.java
@@ -17,7 +17,7 @@
package com.android.contacts;
import java.util.Iterator;
-import java.util.ArrayList;
+import java.util.List;
/**
* Class used for collapsing data items into groups of similar items. The data items that should be
@@ -46,9 +46,9 @@
* if {@link Collapsible#shouldCollapseWith(Object)} returns strue, and are collapsed
* through the {@Link Collapsible#collapseWith(Object)} function implemented by the data item.
*
- * @param list ArrayList of Objects of type <T extends Collapsible<T>> to be collapsed.
+ * @param list List of Objects of type <T extends Collapsible<T>> to be collapsed.
*/
- public static <T extends Collapsible<T>> void collapseList(ArrayList<T> list) {
+ public static <T extends Collapsible<T>> void collapseList(List<T> list) {
int listSize = list.size();
diff --git a/src/com/android/contacts/quickcontact/Action.java b/src/com/android/contacts/quickcontact/Action.java
new file mode 100644
index 0000000..c9823b7
--- /dev/null
+++ b/src/com/android/contacts/quickcontact/Action.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.quickcontact;
+
+import com.android.contacts.Collapser;
+
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+
+/**
+ * Abstract definition of an action that could be performed, along with
+ * string description and icon.
+ */
+public interface Action extends Collapser.Collapsible<Action> {
+ public CharSequence getHeader();
+ public CharSequence getBody();
+
+ public String getMimeType();
+ public Drawable getFallbackIcon();
+
+ /**
+ * Build an {@link Intent} that will perform this action.
+ */
+ public Intent getIntent();
+
+ /**
+ * Checks if the contact data for this action is primary.
+ */
+ public Boolean isPrimary();
+
+ /**
+ * Returns a lookup (@link Uri) for the contact data item.
+ */
+ public Uri getDataUri();
+}
diff --git a/src/com/android/contacts/quickcontact/ActionMultiMap.java b/src/com/android/contacts/quickcontact/ActionMultiMap.java
new file mode 100644
index 0000000..21234ce
--- /dev/null
+++ b/src/com/android/contacts/quickcontact/ActionMultiMap.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.quickcontact;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Provide a simple way of collecting one or more {@link Action} objects
+ * under a MIME-type key.
+ */
+public class ActionMultiMap extends HashMap<String, ArrayList<Action>> {
+ public void put(String mimeType, Action info) {
+ // Create list for this MIME-type when needed
+ ArrayList<Action> collectList = get(mimeType);
+ if (collectList == null) {
+ collectList = new ArrayList<Action>();
+ put(mimeType, collectList);
+ }
+ collectList.add(info);
+ }
+}
diff --git a/src/com/android/contacts/quickcontact/DataAction.java b/src/com/android/contacts/quickcontact/DataAction.java
new file mode 100644
index 0000000..b4d5454
--- /dev/null
+++ b/src/com/android/contacts/quickcontact/DataAction.java
@@ -0,0 +1,267 @@
+package com.android.contacts.quickcontact;
+
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.contacts.model.AccountType.DataKind;
+import com.android.contacts.util.Constants;
+import com.android.contacts.util.PhoneCapabilityTester;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Description of a specific {@link Data#_ID} item, with style information
+ * defined by a {@link DataKind}.
+ */
+public class DataAction implements Action {
+ private static final String TAG = "DataAction";
+
+ private final Context mContext;
+ private final DataKind mKind;
+ private final String mMimeType;
+
+ private CharSequence mHeader;
+ private CharSequence mBody;
+ private Intent mIntent;
+
+ private boolean mAlternate;
+ private Uri mDataUri;
+ private boolean mIsPrimary;
+
+ /**
+ * Create an action from common {@link Data} elements.
+ */
+ public DataAction(Context context, String mimeType, DataKind kind,
+ long dataId, Cursor cursor) {
+ mContext = context;
+ mKind = kind;
+ mMimeType = mimeType;
+
+ // Inflate strings from cursor
+ mAlternate = Constants.MIME_SMS_ADDRESS.equals(mimeType);
+ if (mAlternate && mKind.actionAltHeader != null) {
+ mHeader = mKind.actionAltHeader.inflateUsing(context, cursor);
+ } else if (mKind.actionHeader != null) {
+ mHeader = mKind.actionHeader.inflateUsing(context, cursor);
+ }
+
+ if (getAsInt(cursor, Data.IS_SUPER_PRIMARY) != 0) {
+ mIsPrimary = true;
+ }
+
+ if (mKind.actionBody != null) {
+ mBody = mKind.actionBody.inflateUsing(context, cursor);
+ }
+
+ mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
+
+ // Handle well-known MIME-types with special care
+ if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ if (PhoneCapabilityTester.isPhone(mContext)) {
+ final String number = getAsString(cursor, Phone.NUMBER);
+ if (!TextUtils.isEmpty(number)) {
+ final Uri callUri = Uri.fromParts(Constants.SCHEME_TEL, number, null);
+ mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
+ }
+ }
+ } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ if (PhoneCapabilityTester.isSipPhone(mContext)) {
+ final String address = getAsString(cursor, SipAddress.SIP_ADDRESS);
+ if (!TextUtils.isEmpty(address)) {
+ final Uri callUri = Uri.fromParts(Constants.SCHEME_SIP, address, null);
+ mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
+ // Note that this item will get a SIP-specific variant
+ // of the "call phone" icon, rather than the standard
+ // app icon for the Phone app (which we show for
+ // regular phone numbers.) That's because the phone
+ // app explicitly specifies an android:icon attribute
+ // for the SIP-related intent-filters in its manifest.
+ }
+ }
+ } else if (Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
+ if (PhoneCapabilityTester.isSmsIntentRegistered(mContext)) {
+ final String number = getAsString(cursor, Phone.NUMBER);
+ if (!TextUtils.isEmpty(number)) {
+ final Uri smsUri = Uri.fromParts(Constants.SCHEME_SMSTO, number, null);
+ mIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
+ }
+ }
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final String address = getAsString(cursor, Email.DATA);
+ if (!TextUtils.isEmpty(address)) {
+ final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
+ mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
+ }
+
+ } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final String url = getAsString(cursor, Website.URL);
+ if (!TextUtils.isEmpty(url)) {
+ mIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ }
+
+ } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
+ getAsString(cursor, Data.MIMETYPE));
+ if (isEmail || isProtocolValid(cursor)) {
+ final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
+ getAsInt(cursor, Im.PROTOCOL);
+
+ if (isEmail) {
+ // Use Google Talk string when using Email, and clear data
+ // Uri so we don't try saving Email as primary.
+ mHeader = context.getText(R.string.chat_gtalk);
+ mDataUri = null;
+ }
+
+ String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
+ String data = getAsString(cursor,
+ isEmail ? Email.DATA : Im.DATA);
+ if (protocol != Im.PROTOCOL_CUSTOM) {
+ // Try bringing in a well-known host for specific protocols
+ host = ContactsUtils.lookupProviderNameFromId(protocol);
+ }
+
+ if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
+ final String authority = host.toLowerCase();
+ final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
+ authority).appendPath(data).build();
+ mIntent = new Intent(Intent.ACTION_SENDTO, imUri);
+ }
+ }
+ }
+
+ if (mIntent == null) {
+ // Otherwise fall back to default VIEW action
+ mIntent = new Intent(Intent.ACTION_VIEW, mDataUri);
+ }
+
+ // Always launch as new task, since we're like a launcher
+ mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ }
+
+ /** Read {@link String} from the given {@link Cursor}. */
+ private static String getAsString(Cursor cursor, String columnName) {
+ final int index = cursor.getColumnIndex(columnName);
+ return cursor.getString(index);
+ }
+
+ /** Read {@link Integer} from the given {@link Cursor}. */
+ private static int getAsInt(Cursor cursor, String columnName) {
+ final int index = cursor.getColumnIndex(columnName);
+ return cursor.getInt(index);
+ }
+
+ private boolean isProtocolValid(Cursor cursor) {
+ final int columnIndex = cursor.getColumnIndex(Im.PROTOCOL);
+ if (cursor.isNull(columnIndex)) {
+ return false;
+ }
+ try {
+ Integer.valueOf(cursor.getString(columnIndex));
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public CharSequence getHeader() {
+ return mHeader;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public CharSequence getBody() {
+ return mBody;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getMimeType() {
+ return mMimeType;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Uri getDataUri() {
+ return mDataUri;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Boolean isPrimary() {
+ return mIsPrimary;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Drawable getFallbackIcon() {
+ // Bail early if no valid resources
+ final String resPackageName = mKind.resPackageName;
+ if (resPackageName == null) return null;
+
+ final PackageManager pm = mContext.getPackageManager();
+ if (mAlternate && mKind.iconAltRes != -1) {
+ return pm.getDrawable(resPackageName, mKind.iconAltRes, null);
+ } else if (mKind.iconRes != -1) {
+ return pm.getDrawable(resPackageName, mKind.iconRes, null);
+ } else {
+ return null;
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean collapseWith(Action other) {
+ if (!shouldCollapseWith(other)) {
+ return false;
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean shouldCollapseWith(Action t) {
+ if (t == null) {
+ return false;
+ }
+ if (!(t instanceof DataAction)) {
+ Log.e(TAG, "t must be DataAction");
+ return false;
+ }
+ DataAction other = (DataAction)t;
+ if (!ContactsUtils.areObjectsEqual(mKind, other.mKind)) {
+ return false;
+ }
+ if (!ContactsUtils.shouldCollapse(mContext, mMimeType, mBody, other.mMimeType,
+ other.mBody)) {
+ return false;
+ }
+ if (!TextUtils.equals(mMimeType, other.mMimeType)
+ || !ContactsUtils.areIntentActionEqual(mIntent, other.mIntent)
+ ) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/com/android/contacts/quickcontact/PackageIntentReceiver.java b/src/com/android/contacts/quickcontact/PackageIntentReceiver.java
new file mode 100644
index 0000000..7af4005
--- /dev/null
+++ b/src/com/android/contacts/quickcontact/PackageIntentReceiver.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.quickcontact;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Package intent receiver that flushes the {@link ResolveCache} so that Packages are rescanned next
+ * time
+ */
+public class PackageIntentReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ResolveCache.flush();
+ }
+}
diff --git a/src/com/android/contacts/quickcontact/ProfileAction.java b/src/com/android/contacts/quickcontact/ProfileAction.java
new file mode 100644
index 0000000..47102a5
--- /dev/null
+++ b/src/com/android/contacts/quickcontact/ProfileAction.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.quickcontact;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+
+/**
+ * Specific action that launches the profile card.
+ */
+public class ProfileAction implements Action {
+ private final Context mContext;
+ private final Uri mLookupUri;
+
+ public ProfileAction(Context context, Uri lookupUri) {
+ mContext = context;
+ mLookupUri = lookupUri;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public CharSequence getHeader() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public CharSequence getBody() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String getMimeType() {
+ return Contacts.CONTENT_ITEM_TYPE;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Drawable getFallbackIcon() {
+ return mContext.getResources().getDrawable(R.drawable.ic_contacts_details);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Intent getIntent() {
+ final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ return intent;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Boolean isPrimary() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Uri getDataUri() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean collapseWith(Action t) {
+ return false; // Never dup.
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean shouldCollapseWith(Action t) {
+ return false; // Never dup.
+ }
+}
diff --git a/src/com/android/contacts/quickcontact/QuickContactBackgroundDrawable.java b/src/com/android/contacts/quickcontact/QuickContactBackgroundDrawable.java
index ef970d5..fb87bfe 100644
--- a/src/com/android/contacts/quickcontact/QuickContactBackgroundDrawable.java
+++ b/src/com/android/contacts/quickcontact/QuickContactBackgroundDrawable.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.contacts.quickcontact;
import com.android.contacts.R;
diff --git a/src/com/android/contacts/quickcontact/QuickContactWindow.java b/src/com/android/contacts/quickcontact/QuickContactWindow.java
index e773759..cf7f126 100644
--- a/src/com/android/contacts/quickcontact/QuickContactWindow.java
+++ b/src/com/android/contacts/quickcontact/QuickContactWindow.java
@@ -18,7 +18,6 @@
import com.android.contacts.Collapser;
import com.android.contacts.ContactPresenceIconUtil;
-import com.android.contacts.ContactsUtils;
import com.android.contacts.R;
import com.android.contacts.model.AccountType;
import com.android.contacts.model.AccountType.DataKind;
@@ -26,9 +25,7 @@
import com.android.contacts.util.Constants;
import com.android.contacts.util.DataStatus;
import com.android.contacts.util.NotifyingAsyncQueryHandler;
-import com.android.contacts.util.PhoneCapabilityTester;
import com.android.internal.policy.PolicyManager;
-import com.google.android.collect.Sets;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -37,11 +34,7 @@
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -89,10 +82,8 @@
import android.widget.TextView;
import android.widget.Toast;
-import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
@@ -133,7 +124,6 @@
private NotifyingAsyncQueryHandler mHandler;
private OnDismissListener mDismissListener;
- private ResolveCache mResolveCache;
private Uri mLookupUri;
private Rect mAnchor;
@@ -163,9 +153,9 @@
/**
* Set of {@link Action} that are associated with the aggregate currently
* displayed by this dialog, represented as a map from {@link String}
- * MIME-type to {@link ActionList}.
+ * MIME-type to a list of {@link Action}.
*/
- private ActionMap mActions = new ActionMap();
+ private ActionMultiMap mActions = new ActionMultiMap();
/**
* Pool of unused {@link CheckableImageView} that have previously been
@@ -202,25 +192,8 @@
Website.CONTENT_ITEM_TYPE,
};
- /**
- * Specific list {@link ApplicationInfo#packageName} of apps that are
- * prefered <strong>only</strong> for the purposes of default icons when
- * multiple {@link ResolveInfo} are found to match. This only happens when
- * the user has not selected a default app yet, and they will still be
- * presented with the system disambiguation dialog.
- */
- private static final HashSet<String> sPreferResolve = Sets.newHashSet(
- "com.android.email",
- "com.android.calendar",
- "com.android.contacts",
- "com.android.mms",
- "com.android.phone",
- "com.android.browser");
-
private static final int TOKEN_DATA = 1;
- static final boolean LOGD = false;
-
static final boolean TRACE_LAUNCH = false;
static final String TRACE_TAG = "quickcontact";
@@ -248,11 +221,9 @@
mBackground = new QuickContactBackgroundDrawable();
mRootView.setBackgroundDrawable(mBackground);
- mResolveCache = new ResolveCache(mContext);
-
mScreenWidth = mWindowManager.getDefaultDisplay().getWidth();
// Status bar height
- int screenMarginBottom = context.getResources().getDimensionPixelSize(
+ final int screenMarginBottom = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.screen_margin_bottom);
mUseableScreenHeight = mWindowManager.getDefaultDisplay().getHeight() - screenMarginBottom;
@@ -537,7 +508,6 @@
mLastAction = null;
// Clear track actions and scroll to hard left
- mResolveCache.clear();
mActions.clear();
// Recycle any chiclets in use
@@ -618,501 +588,6 @@
}
}
- /** Read {@link String} from the given {@link Cursor}. */
- private static String getAsString(Cursor cursor, String columnName) {
- final int index = cursor.getColumnIndex(columnName);
- return cursor.getString(index);
- }
-
- /** Read {@link Integer} from the given {@link Cursor}. */
- private static int getAsInt(Cursor cursor, String columnName) {
- final int index = cursor.getColumnIndex(columnName);
- return cursor.getInt(index);
- }
-
- /**
- * Abstract definition of an action that could be performed, along with
- * string description and icon.
- */
- private interface Action extends Collapser.Collapsible<Action> {
- public CharSequence getHeader();
- public CharSequence getBody();
-
- public String getMimeType();
- public Drawable getFallbackIcon();
-
- /**
- * Build an {@link Intent} that will perform this action.
- */
- public Intent getIntent();
-
- /**
- * Checks if the contact data for this action is primary.
- */
- public Boolean isPrimary();
-
- /**
- * Returns a lookup (@link Uri) for the contact data item.
- */
- public Uri getDataUri();
- }
-
- /**
- * Description of a specific {@link Data#_ID} item, with style information
- * defined by a {@link DataKind}.
- */
- private static class DataAction implements Action {
- private final Context mContext;
- private final DataKind mKind;
- private final String mMimeType;
-
- private CharSequence mHeader;
- private CharSequence mBody;
- private Intent mIntent;
-
- private boolean mAlternate;
- private Uri mDataUri;
- private boolean mIsPrimary;
-
- /**
- * Create an action from common {@link Data} elements.
- */
- public DataAction(Context context, String mimeType, DataKind kind,
- long dataId, Cursor cursor) {
- mContext = context;
- mKind = kind;
- mMimeType = mimeType;
-
- // Inflate strings from cursor
- mAlternate = Constants.MIME_SMS_ADDRESS.equals(mimeType);
- if (mAlternate && mKind.actionAltHeader != null) {
- mHeader = mKind.actionAltHeader.inflateUsing(context, cursor);
- } else if (mKind.actionHeader != null) {
- mHeader = mKind.actionHeader.inflateUsing(context, cursor);
- }
-
- if (getAsInt(cursor, Data.IS_SUPER_PRIMARY) != 0) {
- mIsPrimary = true;
- }
-
- if (mKind.actionBody != null) {
- mBody = mKind.actionBody.inflateUsing(context, cursor);
- }
-
- mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
-
- // Handle well-known MIME-types with special care
- if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
- if (PhoneCapabilityTester.isPhone(mContext)) {
- final String number = getAsString(cursor, Phone.NUMBER);
- if (!TextUtils.isEmpty(number)) {
- final Uri callUri = Uri.fromParts(Constants.SCHEME_TEL, number, null);
- mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
- }
- }
- } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
- if (PhoneCapabilityTester.isSipPhone(mContext)) {
- final String address = getAsString(cursor, SipAddress.SIP_ADDRESS);
- if (!TextUtils.isEmpty(address)) {
- final Uri callUri = Uri.fromParts(Constants.SCHEME_SIP, address, null);
- mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
- // Note that this item will get a SIP-specific variant
- // of the "call phone" icon, rather than the standard
- // app icon for the Phone app (which we show for
- // regular phone numbers.) That's because the phone
- // app explicitly specifies an android:icon attribute
- // for the SIP-related intent-filters in its manifest.
- }
- }
- } else if (Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
- if (PhoneCapabilityTester.isSmsIntentRegistered(mContext)) {
- final String number = getAsString(cursor, Phone.NUMBER);
- if (!TextUtils.isEmpty(number)) {
- final Uri smsUri = Uri.fromParts(Constants.SCHEME_SMSTO, number, null);
- mIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
- }
- }
- } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
- final String address = getAsString(cursor, Email.DATA);
- if (!TextUtils.isEmpty(address)) {
- final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
- mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
- }
-
- } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
- final String url = getAsString(cursor, Website.URL);
- if (!TextUtils.isEmpty(url)) {
- mIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- }
-
- } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
- final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(
- getAsString(cursor, Data.MIMETYPE));
- if (isEmail || isProtocolValid(cursor)) {
- final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
- getAsInt(cursor, Im.PROTOCOL);
-
- if (isEmail) {
- // Use Google Talk string when using Email, and clear data
- // Uri so we don't try saving Email as primary.
- mHeader = context.getText(R.string.chat_gtalk);
- mDataUri = null;
- }
-
- String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
- String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA);
- if (protocol != Im.PROTOCOL_CUSTOM) {
- // Try bringing in a well-known host for specific protocols
- host = ContactsUtils.lookupProviderNameFromId(protocol);
- }
-
- if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
- final String authority = host.toLowerCase();
- final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
- authority).appendPath(data).build();
- mIntent = new Intent(Intent.ACTION_SENDTO, imUri);
- }
- }
- }
-
- if (mIntent == null) {
- // Otherwise fall back to default VIEW action
- mIntent = new Intent(Intent.ACTION_VIEW, mDataUri);
- }
-
- // Always launch as new task, since we're like a launcher
- mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
- }
-
- private boolean isProtocolValid(Cursor cursor) {
- final int columnIndex = cursor.getColumnIndex(Im.PROTOCOL);
- if (cursor.isNull(columnIndex)) {
- return false;
- }
- try {
- Integer.valueOf(cursor.getString(columnIndex));
- } catch (NumberFormatException e) {
- return false;
- }
- return true;
- }
-
- /** {@inheritDoc} */
- @Override
- public CharSequence getHeader() {
- return mHeader;
- }
-
- /** {@inheritDoc} */
- @Override
- public CharSequence getBody() {
- return mBody;
- }
-
- /** {@inheritDoc} */
- @Override
- public String getMimeType() {
- return mMimeType;
- }
-
- /** {@inheritDoc} */
- @Override
- public Uri getDataUri() {
- return mDataUri;
- }
-
- /** {@inheritDoc} */
- @Override
- public Boolean isPrimary() {
- return mIsPrimary;
- }
-
- /** {@inheritDoc} */
- @Override
- public Drawable getFallbackIcon() {
- // Bail early if no valid resources
- final String resPackageName = mKind.resPackageName;
- if (resPackageName == null) return null;
-
- final PackageManager pm = mContext.getPackageManager();
- if (mAlternate && mKind.iconAltRes != -1) {
- return pm.getDrawable(resPackageName, mKind.iconAltRes, null);
- } else if (mKind.iconRes != -1) {
- return pm.getDrawable(resPackageName, mKind.iconRes, null);
- } else {
- return null;
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public Intent getIntent() {
- return mIntent;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean collapseWith(Action other) {
- if (!shouldCollapseWith(other)) {
- return false;
- }
- return true;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean shouldCollapseWith(Action t) {
- if (t == null) {
- return false;
- }
- if (!(t instanceof DataAction)) {
- Log.e(TAG, "t must be DataAction");
- return false;
- }
- DataAction other = (DataAction)t;
- if (!ContactsUtils.areObjectsEqual(mKind, other.mKind)) {
- return false;
- }
- if (!ContactsUtils.shouldCollapse(mContext, mMimeType, mBody, other.mMimeType,
- other.mBody)) {
- return false;
- }
- if (!TextUtils.equals(mMimeType, other.mMimeType)
- || !ContactsUtils.areIntentActionEqual(mIntent, other.mIntent)
- ) {
- return false;
- }
- return true;
- }
- }
-
- /**
- * Specific action that launches the profile card.
- */
- private static class ProfileAction implements Action {
- private final Context mContext;
- private final Uri mLookupUri;
-
- public ProfileAction(Context context, Uri lookupUri) {
- mContext = context;
- mLookupUri = lookupUri;
- }
-
- /** {@inheritDoc} */
- @Override
- public CharSequence getHeader() {
- return null;
- }
-
- /** {@inheritDoc} */
- @Override
- public CharSequence getBody() {
- return null;
- }
-
- /** {@inheritDoc} */
- @Override
- public String getMimeType() {
- return Contacts.CONTENT_ITEM_TYPE;
- }
-
- /** {@inheritDoc} */
- @Override
- public Drawable getFallbackIcon() {
- return mContext.getResources().getDrawable(R.drawable.ic_contacts_details);
- }
-
- /** {@inheritDoc} */
- @Override
- public Intent getIntent() {
- final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
- return intent;
- }
-
- /** {@inheritDoc} */
- @Override
- public Boolean isPrimary() {
- return null;
- }
-
- /** {@inheritDoc} */
- @Override
- public Uri getDataUri() {
- return null;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean collapseWith(Action t) {
- return false; // Never dup.
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean shouldCollapseWith(Action t) {
- return false; // Never dup.
- }
- }
-
- /**
- * Internally hold a cache of scaled icons based on {@link PackageManager}
- * queries, keyed internally on MIME-type.
- */
- private static class ResolveCache {
- private PackageManager mPackageManager;
-
- /**
- * Cached entry holding the best {@link ResolveInfo} for a specific
- * MIME-type, along with a {@link SoftReference} to its icon.
- */
- private static class Entry {
- public ResolveInfo bestResolve;
- public Drawable icon;
- }
-
- private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
-
- public ResolveCache(Context context) {
- mPackageManager = context.getPackageManager();
- }
-
- /**
- * Get the {@link Entry} best associated with the given {@link Action},
- * or create and populate a new one if it doesn't exist.
- */
- protected Entry getEntry(Action action) {
- final String mimeType = action.getMimeType();
- Entry entry = mCache.get(mimeType);
- if (entry != null) return entry;
- entry = new Entry();
-
- final Intent intent = action.getIntent();
- if (intent != null) {
- final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
- PackageManager.MATCH_DEFAULT_ONLY);
-
- // Pick first match, otherwise best found
- ResolveInfo bestResolve = null;
- final int size = matches.size();
- if (size == 1) {
- bestResolve = matches.get(0);
- } else if (size > 1) {
- bestResolve = getBestResolve(intent, matches);
- }
-
- if (bestResolve != null) {
- final Drawable icon = bestResolve.loadIcon(mPackageManager);
-
- entry.bestResolve = bestResolve;
- entry.icon = icon;
- }
- }
-
- mCache.put(mimeType, entry);
- return entry;
- }
-
- /**
- * Best {@link ResolveInfo} when multiple found. Ties are broken by
- * selecting first from the {QuickContactWindow#sPreferResolve} list of
- * preferred packages, second by apps that live on the system partition,
- * otherwise the app from the top of the list. This is
- * <strong>only</strong> used for selecting a default icon for
- * displaying in the track, and does not shortcut the system
- * {@link Intent} disambiguation dialog.
- */
- protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) {
- // Try finding preferred activity, otherwise detect disambig
- final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent,
- PackageManager.MATCH_DEFAULT_ONLY);
- final boolean foundDisambig = (foundResolve.match &
- IntentFilter.MATCH_CATEGORY_MASK) == 0;
-
- if (!foundDisambig) {
- // Found concrete match, so return directly
- return foundResolve;
- }
-
- // Accept any package from prefer list, otherwise first system app
- ResolveInfo firstSystem = null;
- for (ResolveInfo info : matches) {
- final boolean isSystem = (info.activityInfo.applicationInfo.flags
- & ApplicationInfo.FLAG_SYSTEM) != 0;
- final boolean isPrefer = QuickContactWindow.sPreferResolve
- .contains(info.activityInfo.applicationInfo.packageName);
-
-
-
- if (isPrefer) return info;
- if (isSystem && firstSystem == null) firstSystem = info;
- }
-
- // Return first system found, otherwise first from list
- return firstSystem != null ? firstSystem : matches.get(0);
- }
-
- /**
- * Check {@link PackageManager} to see if any apps offer to handle the
- * given {@link Action}.
- */
- public boolean hasResolve(Action action) {
- return getEntry(action).bestResolve != null;
- }
-
- /**
- * Find the best description for the given {@link Action}, usually used
- * for accessibility purposes.
- */
- public CharSequence getDescription(Action action) {
- final CharSequence actionHeader = action.getHeader();
- final ResolveInfo info = getEntry(action).bestResolve;
- if (!TextUtils.isEmpty(actionHeader)) {
- return actionHeader;
- } else if (info != null) {
- return info.loadLabel(mPackageManager);
- } else {
- return null;
- }
- }
-
- /**
- * Return the best icon for the given {@link Action}, which is usually
- * based on the {@link ResolveInfo} found through a
- * {@link PackageManager} query.
- */
- public Drawable getIcon(Action action) {
- return getEntry(action).icon;
- }
-
- public void clear() {
- mCache.clear();
- }
- }
-
- /**
- * Provide a strongly-typed {@link LinkedList} that holds a list of
- * {@link Action} objects.
- */
- private class ActionList extends ArrayList<Action> {
- }
-
- /**
- * Provide a simple way of collecting one or more {@link Action} objects
- * under a MIME-type key.
- */
- private class ActionMap extends HashMap<String, ActionList> {
- private void collect(String mimeType, Action info) {
- // Create list for this MIME-type when needed
- ActionList collectList = get(mimeType);
- if (collectList == null) {
- collectList = new ActionList();
- put(mimeType, collectList);
- }
- collectList.add(info);
- }
- }
-
/**
* Check if the given MIME-type appears in the list of excluded MIME-types
* that the most-recent caller requested.
@@ -1131,6 +606,7 @@
* Handle the result from the {@link #TOKEN_DATA} query.
*/
private void handleData(Cursor cursor) {
+ final ResolveCache cache = ResolveCache.getInstance(mContext.getPackageManager());
if (cursor == null) return;
if (cursor.getCount() == 0) {
Toast.makeText(mContext, R.string.invalidContactMessage, Toast.LENGTH_LONG).show();
@@ -1141,7 +617,7 @@
if (!isMimeExcluded(Contacts.CONTENT_ITEM_TYPE)) {
// Add the profile shortcut action
final Action action = new ProfileAction(mContext, mLookupUri);
- mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
+ mActions.put(Contacts.CONTENT_ITEM_TYPE, action);
}
final DataStatus status = new DataStatus();
@@ -1178,14 +654,14 @@
// element, build its summary from the cursor, and collect it
// along with all others of this MIME-type.
final Action action = new DataAction(mContext, mimeType, kind, dataId, cursor);
- considerAdd(action, mimeType);
+ considerAdd(action, mimeType, cache);
}
// If phone number, also insert as text message action
if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) {
final Action action = new DataAction(mContext, Constants.MIME_SMS_ADDRESS,
kind, dataId, cursor);
- considerAdd(action, Constants.MIME_SMS_ADDRESS);
+ considerAdd(action, Constants.MIME_SMS_ADDRESS, cache);
}
// Handle Email rows with presence data as Im entry
@@ -1196,7 +672,7 @@
if (imKind != null) {
final Action action = new DataAction(mContext, Im.CONTENT_ITEM_TYPE, imKind,
dataId, cursor);
- considerAdd(action, Im.CONTENT_ITEM_TYPE);
+ considerAdd(action, Im.CONTENT_ITEM_TYPE, cache);
}
}
}
@@ -1238,7 +714,7 @@
for (String mimeType : PRECEDING_MIMETYPES) {
if (containedTypes.contains(mimeType)) {
hasData = true;
- mTrack.addView(inflateAction(mimeType), index++);
+ mTrack.addView(inflateAction(mimeType, cache), index++);
containedTypes.remove(mimeType);
}
}
@@ -1250,7 +726,7 @@
for (String mimeType : FOLLOWING_MIMETYPES) {
if (containedTypes.contains(mimeType)) {
hasData = true;
- mTrack.addView(inflateAction(mimeType), index++);
+ mTrack.addView(inflateAction(mimeType, cache), index++);
containedTypes.remove(mimeType);
}
}
@@ -1261,7 +737,7 @@
if (remainingTypes.length > 0) hasData = true;
Arrays.sort(remainingTypes);
for (String mimeType : remainingTypes) {
- mTrack.addView(inflateAction(mimeType), index++);
+ mTrack.addView(inflateAction(mimeType, cache), index++);
}
// When there is no data to display, add a TextView to show the user there's no data
@@ -1276,9 +752,9 @@
* {@link PackageManager} finds an application to handle
* {@link Action#getIntent()}.
*/
- private void considerAdd(Action action, String mimeType) {
- if (mResolveCache.hasResolve(action)) {
- mActions.collect(mimeType, action);
+ private void considerAdd(Action action, String mimeType, ResolveCache resolveCache) {
+ if (resolveCache.hasResolve(action)) {
+ mActions.put(mimeType, action);
}
}
@@ -1313,12 +789,12 @@
* Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
* Will use the icon provided by the {@link DataKind}.
*/
- private View inflateAction(String mimeType) {
+ private View inflateAction(String mimeType, ResolveCache resolveCache) {
final CheckableImageView view = (CheckableImageView)obtainView();
boolean isActionSet = false;
// Add direct intent if single child, otherwise flag for multiple
- ActionList children = mActions.get(mimeType);
+ List<Action> children = mActions.get(mimeType);
if (children.size() > 1) {
Collapser.collapseList(children);
}
@@ -1339,8 +815,8 @@
}
// Set icon and listen for clicks
- final CharSequence descrip = mResolveCache.getDescription(firstInfo);
- final Drawable icon = mResolveCache.getIcon(firstInfo);
+ final CharSequence descrip = resolveCache.getDescription(firstInfo);
+ final Drawable icon = resolveCache.getIcon(firstInfo);
view.setChecked(false);
view.setContentDescription(descrip);
view.setImageDrawable(icon);
@@ -1459,7 +935,7 @@
// Defer the action to make the window properly repaint
new Handler().post(startAppRunnable);
}
- } else if (tag instanceof ActionList) {
+ } else if (tag instanceof ArrayList<?>) {
// Don't do anything if already open
if (actionView != mLastAction) {
final Runnable configureListRunnable = new Runnable() {
@@ -1469,7 +945,7 @@
setNewActionViewChecked(actionView);
// Incoming tag is a MIME-type, so show resolution list
- final ActionList children = (ActionList)tag;
+ final ArrayList<?> children = (ArrayList<?>)tag;
mResolveList.setOnItemClickListener(QuickContactWindow.this);
mResolveList.setAdapter(new BaseAdapter() {
diff --git a/src/com/android/contacts/quickcontact/ResolveCache.java b/src/com/android/contacts/quickcontact/ResolveCache.java
new file mode 100644
index 0000000..5d84253
--- /dev/null
+++ b/src/com/android/contacts/quickcontact/ResolveCache.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.quickcontact;
+
+import com.google.android.collect.Sets;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+
+import java.lang.ref.SoftReference;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Internally hold a cache of scaled icons based on {@link PackageManager}
+ * queries, keyed internally on MIME-type.
+ */
+public class ResolveCache {
+ /**
+ * Specific list {@link ApplicationInfo#packageName} of apps that are
+ * prefered <strong>only</strong> for the purposes of default icons when
+ * multiple {@link ResolveInfo} are found to match. This only happens when
+ * the user has not selected a default app yet, and they will still be
+ * presented with the system disambiguation dialog.
+ */
+ private static final HashSet<String> sPreferResolve = Sets.newHashSet(
+ "com.android.email",
+ "com.android.calendar",
+ "com.android.contacts",
+ "com.android.mms",
+ "com.android.phone",
+ "com.android.browser");
+
+ private final PackageManager mPackageManager;
+
+ private static ResolveCache sInstance;
+
+ /**
+ * Returns an instance of the ResolveCache. Only one internal instance is kept, so
+ * the argument packageManagers is ignored for all but the first call
+ */
+ public synchronized static ResolveCache getInstance(PackageManager packageManager) {
+ if (sInstance == null) {
+ return sInstance = new ResolveCache(packageManager);
+ }
+ return sInstance;
+ }
+
+ public synchronized static void flush() {
+ sInstance = null;
+ }
+
+ /**
+ * Cached entry holding the best {@link ResolveInfo} for a specific
+ * MIME-type, along with a {@link SoftReference} to its icon.
+ */
+ private static class Entry {
+ public ResolveInfo bestResolve;
+ public Drawable icon;
+ }
+
+ private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
+
+ private ResolveCache(PackageManager packageManager) {
+ mPackageManager = packageManager;
+ }
+
+ /**
+ * Get the {@link Entry} best associated with the given {@link Action},
+ * or create and populate a new one if it doesn't exist.
+ */
+ protected Entry getEntry(Action action) {
+ final String mimeType = action.getMimeType();
+ Entry entry = mCache.get(mimeType);
+ if (entry != null) return entry;
+ entry = new Entry();
+
+ final Intent intent = action.getIntent();
+ if (intent != null) {
+ final List<ResolveInfo> matches = mPackageManager.queryIntentActivities(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+
+ // Pick first match, otherwise best found
+ ResolveInfo bestResolve = null;
+ final int size = matches.size();
+ if (size == 1) {
+ bestResolve = matches.get(0);
+ } else if (size > 1) {
+ bestResolve = getBestResolve(intent, matches);
+ }
+
+ if (bestResolve != null) {
+ final Drawable icon = bestResolve.loadIcon(mPackageManager);
+
+ entry.bestResolve = bestResolve;
+ entry.icon = icon;
+ }
+ }
+
+ mCache.put(mimeType, entry);
+ return entry;
+ }
+
+ /**
+ * Best {@link ResolveInfo} when multiple found. Ties are broken by
+ * selecting first from the {QuickContactWindow#sPreferResolve} list of
+ * preferred packages, second by apps that live on the system partition,
+ * otherwise the app from the top of the list. This is
+ * <strong>only</strong> used for selecting a default icon for
+ * displaying in the track, and does not shortcut the system
+ * {@link Intent} disambiguation dialog.
+ */
+ protected ResolveInfo getBestResolve(Intent intent, List<ResolveInfo> matches) {
+ // Try finding preferred activity, otherwise detect disambig
+ final ResolveInfo foundResolve = mPackageManager.resolveActivity(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ final boolean foundDisambig = (foundResolve.match &
+ IntentFilter.MATCH_CATEGORY_MASK) == 0;
+
+ if (!foundDisambig) {
+ // Found concrete match, so return directly
+ return foundResolve;
+ }
+
+ // Accept any package from prefer list, otherwise first system app
+ ResolveInfo firstSystem = null;
+ for (ResolveInfo info : matches) {
+ final boolean isSystem = (info.activityInfo.applicationInfo.flags
+ & ApplicationInfo.FLAG_SYSTEM) != 0;
+ final boolean isPrefer = sPreferResolve
+ .contains(info.activityInfo.applicationInfo.packageName);
+
+ if (isPrefer) return info;
+ if (isSystem && firstSystem == null) firstSystem = info;
+ }
+
+ // Return first system found, otherwise first from list
+ return firstSystem != null ? firstSystem : matches.get(0);
+ }
+
+ /**
+ * Check {@link PackageManager} to see if any apps offer to handle the
+ * given {@link Action}.
+ */
+ public boolean hasResolve(Action action) {
+ return getEntry(action).bestResolve != null;
+ }
+
+ /**
+ * Find the best description for the given {@link Action}, usually used
+ * for accessibility purposes.
+ */
+ public CharSequence getDescription(Action action) {
+ final CharSequence actionHeader = action.getHeader();
+ final ResolveInfo info = getEntry(action).bestResolve;
+ if (!TextUtils.isEmpty(actionHeader)) {
+ return actionHeader;
+ } else if (info != null) {
+ return info.loadLabel(mPackageManager);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return the best icon for the given {@link Action}, which is usually
+ * based on the {@link ResolveInfo} found through a
+ * {@link PackageManager} query.
+ */
+ public Drawable getIcon(Action action) {
+ return getEntry(action).icon;
+ }
+
+ public void clear() {
+ mCache.clear();
+ }
+}