package com.android.contacts.ui;
+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.ContactsSource;
import com.android.contacts.model.ContactsSource.DataKind;
import com.android.contacts.ui.widget.CheckableImageView;
import com.android.contacts.util.Constants;
+import com.android.contacts.util.DataStatus;
import com.android.contacts.util.NotifyingAsyncQueryHandler;
import com.android.internal.policy.PolicyManager;
-import com.google.android.collect.Lists;
import com.google.android.collect.Sets;
import android.content.ActivityNotFoundException;
-import android.content.ContentValues;
import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
-import android.content.EntityIterator;
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.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.os.Handler;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.QuickContact;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.SocialContract.Activities;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
import android.text.TextUtils;
-import android.text.format.DateUtils;
+import android.util.AttributeSet;
import android.util.Log;
-import android.util.Pool;
-import android.util.Poolable;
-import android.util.PoolableManager;
-import android.util.Pools;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
-import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.Window;
import android.view.WindowManager;
-import android.view.Window.Callback;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.ListView;
+import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
*/
public class QuickContactWindow implements Window.Callback,
NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
- AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback {
+ AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback,
+ OnGlobalLayoutListener {
private static final String TAG = "QuickContactWindow";
/**
public void onDismiss(QuickContactWindow dialog);
}
+ /**
+ * Custom layout the sole purpose of which is to intercept the BACK key and
+ * close QC even when the soft keyboard is open.
+ */
+ public static class RootLayout extends RelativeLayout {
+
+ QuickContactWindow mQuickContactWindow;
+
+ public RootLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /**
+ * Intercepts the BACK key event and dismisses QuickContact window.
+ */
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ mQuickContactWindow.onBackPressed();
+ return true;
+ } else {
+ return super.dispatchKeyEventPreIme(event);
+ }
+ }
+ }
+
private final Context mContext;
private final LayoutInflater mInflater;
private final WindowManager mWindowManager;
private View mDecor;
private final Rect mRect = new Rect();
+ private boolean mDismissed = false;
private boolean mQuerying = false;
private boolean mShowing = false;
private Uri mLookupUri;
private Rect mAnchor;
- private int mShadowHeight;
+ private int mShadowHoriz;
+ private int mShadowVert;
+ private int mShadowTouch;
+
+ private int mScreenWidth;
+ private int mScreenHeight;
+ private int mRequestedY;
private boolean mHasValidSocial = false;
private boolean mHasData = false;
private ImageView mArrowDown;
private int mMode;
+ private RootLayout mRootView;
private View mHeader;
private HorizontalScrollView mTrackScroll;
private ViewGroup mTrack;
private String[] mExcludeMimes;
/**
- * Specific MIME-types that should be bumped to the front of the dialog.
- * Other MIME-types not appearing in this list follow in alphabetic order.
+ * {@link #PRECEDING_MIMETYPES} and {@link #FOLLOWING_MIMETYPES} are used to sort MIME-types.
+ *
+ * <p>The MIME-types in {@link #PRECEDING_MIMETYPES} appear in the front of the dialog,
+ * in the order in the array.
+ *
+ * <p>The ones in {@link #FOLLOWING_MIMETYPES} appear in the end of the dialog, in alphabetical
+ * order.
+ *
+ * <p>The rest go between them, in the order in the array.
*/
- private static final String[] ORDERED_MIMETYPES = new String[] {
+ private static final String[] PRECEDING_MIMETYPES = new String[] {
Phone.CONTENT_ITEM_TYPE,
Contacts.CONTENT_ITEM_TYPE,
Constants.MIME_SMS_ADDRESS,
};
/**
+ * See {@link #PRECEDING_MIMETYPES}.
+ */
+ private static final String[] FOLLOWING_MIMETYPES = new String[] {
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ 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(new String[] {
+ 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;
mWindow = PolicyManager.makeNewWindow(mContext);
mWindow.setCallback(this);
mWindow.setWindowManager(mWindowManager, null, null);
+ mWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED);
mWindow.setContentView(R.layout.quickcontact);
+ mRootView = (RootLayout)mWindow.findViewById(R.id.root);
+ mRootView.mQuickContactWindow = this;
+ mRootView.setFocusable(true);
+ mRootView.setFocusableInTouchMode(true);
+ mRootView.setDescendantFocusability(RootLayout.FOCUS_AFTER_DESCENDANTS);
+
mArrowUp = (ImageView)mWindow.findViewById(R.id.arrow_up);
mArrowDown = (ImageView)mWindow.findViewById(R.id.arrow_down);
mResolveCache = new ResolveCache(mContext);
final Resources res = mContext.getResources();
- mShadowHeight = res.getDimensionPixelSize(R.dimen.quickcontact_shadow);
+ mShadowHoriz = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_horiz);
+ mShadowVert = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_vert);
+ mShadowTouch = res.getDimensionPixelSize(R.dimen.quickcontact_shadow_touch);
+
+ mScreenWidth = mWindowManager.getDefaultDisplay().getWidth();
+ mScreenHeight = mWindowManager.getDefaultDisplay().getHeight();
mTrack = (ViewGroup)mWindow.findViewById(R.id.quickcontact);
mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
// Inflate actual header if we picked a stub
final ViewStub stub = (ViewStub)header;
header = stub.inflate();
- } else {
+ } else if (header != null) {
header.setVisibility(View.VISIBLE);
}
* Start showing a dialog for the given {@link Contacts#_ID} pointing
* towards the given location.
*/
- public void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
- if (mShowing || mQuerying) {
- Log.w(TAG, "already in process of showing");
- return;
+ public synchronized void show(Uri lookupUri, Rect anchor, int mode, String[] excludeMimes) {
+ if (mQuerying || mShowing) {
+ Log.w(TAG, "dismissing before showing");
+ dismissInternal();
}
if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) {
android.os.Debug.startMethodTracing(TRACE_TAG);
}
+ // Validate incoming parameters
+ final boolean validMode = (mode == QuickContact.MODE_SMALL
+ || mode == QuickContact.MODE_MEDIUM || mode == QuickContact.MODE_LARGE);
+ if (!validMode) {
+ throw new IllegalArgumentException("Invalid mode, expecting MODE_LARGE, "
+ + "MODE_MEDIUM, or MODE_SMALL");
+ }
+
+ if (anchor == null) {
+ throw new IllegalArgumentException("Missing anchor rectangle");
+ }
+
// Prepare header view for requested mode
mLookupUri = lookupUri;
mAnchor = new Rect(anchor);
setHeaderText(R.id.timestamp, null);
setHeaderImage(R.id.presence, null);
- setHeaderImage(R.id.source, null);
+
+ resetTrack();
+
+ // We need to have a focused view inside the QuickContact window so
+ // that the BACK key event can be intercepted
+ mRootView.requestFocus();
mHasValidSocial = false;
+ mDismissed = false;
mQuerying = true;
// Start background query for data, but only select photo rows when they
// Only request photo data when required by mode
if (mMode == QuickContact.MODE_LARGE) {
// Select photos, but only super-primary
- mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ "!=? OR (" + Data.MIMETYPE + "=? AND " + Data._ID + "=" + Contacts.PHOTO_ID
+ ")", new String[] { Photo.CONTENT_ITEM_TYPE, Photo.CONTENT_ITEM_TYPE }, null);
} else {
// Exclude all photos from cursor
- mHandler.startQuery(TOKEN_DATA, null, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ mHandler.startQuery(TOKEN_DATA, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
}
}
*/
private void showInternal() {
mDecor = mWindow.getDecorView();
+ mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this);
WindowManager.LayoutParams l = mWindow.getAttributes();
- l.width = WindowManager.LayoutParams.FILL_PARENT;
+ l.width = mScreenWidth + mShadowHoriz + mShadowHoriz;
l.height = WindowManager.LayoutParams.WRAP_CONTENT;
// Force layout measuring pass so we have baseline numbers
mDecor.measure(l.width, l.height);
-
final int blockHeight = mDecor.getMeasuredHeight();
l.gravity = Gravity.TOP | Gravity.LEFT;
- l.x = 0;
+ l.x = -mShadowHoriz;
if (mAnchor.top > blockHeight) {
// Show downwards callout when enough room, aligning bottom block
// edge with top of anchor area, and adjusting to inset arrow.
showArrow(R.id.arrow_down, mAnchor.centerX());
- l.y = mAnchor.top - blockHeight + mShadowHeight;
+ l.y = mAnchor.top - blockHeight + mShadowVert;
l.windowAnimations = R.style.QuickContactAboveAnimation;
} else {
// Otherwise show upwards callout, aligning block top with bottom of
// anchor area, and adjusting to inset arrow.
showArrow(R.id.arrow_up, mAnchor.centerX());
- l.y = mAnchor.bottom - mShadowHeight;
+ l.y = mAnchor.bottom - mShadowVert;
l.windowAnimations = R.style.QuickContactBelowAnimation;
}
l.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
- | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+ | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
+ mRequestedY = l.y;
mWindowManager.addView(mDecor, l);
mShowing = true;
mQuerying = false;
+ mDismissed = false;
mTrack.startAnimation(mTrackAnim);
}
}
+ /** {@inheritDoc} */
+ public void onGlobalLayout() {
+ layoutInScreen();
+ }
+
+ /**
+ * Adjust vertical {@link WindowManager.LayoutParams} to fit window as best
+ * as possible, shifting up to display content as needed.
+ */
+ private void layoutInScreen() {
+ if (!mShowing) return;
+
+ final WindowManager.LayoutParams l = mWindow.getAttributes();
+ final int originalY = l.y;
+
+ final int blockHeight = mDecor.getHeight();
+
+ l.y = mRequestedY;
+ if (mRequestedY + blockHeight > mScreenHeight) {
+ // Shift up from bottom when overflowing
+ l.y = mScreenHeight - blockHeight;
+ }
+
+ if (originalY != l.y) {
+ // Only update when value is changed
+ mWindow.setAttributes(l);
+ }
+ }
+
/**
* Dismiss this dialog if showing.
*/
- public void dismiss() {
+ public synchronized void dismiss() {
// Notify any listeners that we've been dismissed
if (mDismissListener != null) {
mDismissListener.onDismiss(this);
}
- if (!isShowing()) {
- if (LOGD) Log.d(TAG, "not visible, ignore");
- return;
- }
+ dismissInternal();
+ }
+ private void dismissInternal() {
+ // Remove any attached window decor for recycling
boolean hadDecor = mDecor != null;
if (hadDecor) {
mWindowManager.removeView(mDecor);
+ mWindowRecycled++;
+ mDecor.getViewTreeObserver().removeGlobalOnLayoutListener(this);
mDecor = null;
mWindow.closeAllPanels();
}
+ mShowing = false;
+ mDismissed = true;
- // Release reference to last chiclet.
- mLastAction = null;
+ // Cancel any pending queries
+ mHandler.cancelOperation(TOKEN_DATA);
+ mQuerying = false;
- // Completely hide header from current mode
+ // Completely hide header and reset track
mHeader.setVisibility(View.GONE);
+ resetTrack();
+ }
- // Cancel any pending queries
- mHandler.cancelOperation(TOKEN_DATA);
+ /**
+ * Reset track to initial state, recycling any chiclets.
+ */
+ private void resetTrack() {
+ // Release reference to last chiclet
+ mLastAction = null;
// Clear track actions and scroll to hard left
mResolveCache.clear();
mTrackScroll.fullScroll(View.FOCUS_LEFT);
mWasDownArrow = false;
- setResolveVisible(false, null);
-
- mQuerying = false;
-
- if (!hadDecor || !mShowing) {
- if (LOGD) Log.d(TAG, "not showing, ignore");
- return;
- }
-
- mShowing = false;
- mWindowRecycled++;
- }
+ // Clear any primary requests
+ mMakePrimary = false;
+ mSetPrimaryCheckBox.setChecked(false);
- /**
- * Returns true if this dialog is showing or querying.
- */
- public boolean isShowing() {
- return mShowing || mQuerying;
+ setResolveVisible(false, null);
}
/**
* Consider showing this window, which will only call through to
* {@link #showInternal()} when all data items are present.
*/
- private synchronized void considerShowing() {
- if (mHasData && !mShowing) {
+ private void considerShowing() {
+ if (mHasData && !mShowing && !mDismissed) {
if (mMode == QuickContact.MODE_MEDIUM && !mHasValidSocial) {
// Missing valid social, swap medium for small header
mHeader.setVisibility(View.GONE);
}
/** {@inheritDoc} */
- public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ public synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ // Bail early when query is stale
+ if (cookie != mLookupUri) return;
+
if (cursor == null) {
// Problem while running query, so bail without showing
Log.w(TAG, "Missing cursor for token=" + token);
}
/** Assign this image to the view, if found in {@link #mHeader}. */
- private void setHeaderImage(int id, int resId) {
- setHeaderImage(id, mContext.getResources().getDrawable(resId));
- }
-
- /** Assign this image to the view, if found in {@link #mHeader}. */
private void setHeaderImage(int id, Drawable drawable) {
final View view = mHeader.findViewById(id);
if (view instanceof ImageView) {
}
/**
- * Find the presence icon for showing in summary header.
- */
- private Drawable getPresenceIcon(int status) {
- int resId = -1;
- switch (status) {
- case StatusUpdates.AVAILABLE:
- resId = android.R.drawable.presence_online;
- break;
- case StatusUpdates.IDLE:
- case StatusUpdates.AWAY:
- resId = android.R.drawable.presence_away;
- break;
- case StatusUpdates.DO_NOT_DISTURB:
- resId = android.R.drawable.presence_busy;
- break;
- }
- if (resId != -1) {
- return mContext.getResources().getDrawable(resId);
- } else {
- return null;
- }
- }
-
- /**
* Find the QuickContact-specific presence icon for showing in chiclets.
*/
private Drawable getTrackPresenceIcon(int status) {
- int resId = -1;
+ int resId;
switch (status) {
case StatusUpdates.AVAILABLE:
resId = R.drawable.quickcontact_slider_presence_active;
* Abstract definition of an action that could be performed, along with
* string description and icon.
*/
- private interface Action {
+ private interface Action extends Collapser.Collapsible<Action> {
public CharSequence getHeader();
public CharSequence getBody();
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));
- final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
- getAsInt(cursor, Im.PROTOCOL);
-
- 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 (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;
+ }
- 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);
+ 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);
+ }
}
}
}
// Always launch as new task, since we're like a launcher
- mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ 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} */
public Intent getIntent() {
return mIntent;
}
+
+ /** {@inheritDoc} */
+ public boolean collapseWith(Action other) {
+ if (!shouldCollapseWith(other)) {
+ return false;
+ }
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ 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.areDataEqual(mContext, mMimeType, mBody, other.mMimeType,
+ other.mBody)) {
+ return false;
+ }
+ if (!TextUtils.equals(mMimeType, other.mMimeType)
+ || !ContactsUtils.areIntentActionEqual(mIntent, other.mIntent)
+ ) {
+ return false;
+ }
+ return true;
+ }
}
/**
/** {@inheritDoc} */
public Intent getIntent() {
final Intent intent = new Intent(Intent.ACTION_VIEW, mLookupUri);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
return intent;
}
return null;
}
+ /** {@inheritDoc} */
+ public boolean collapseWith(Action t) {
+ return false; // Never dup.
+ }
+
+ /** {@inheritDoc} */
+ public boolean shouldCollapseWith(Action t) {
+ return false; // Never dup.
+ }
}
/**
* queries, keyed internally on MIME-type.
*/
private static class ResolveCache {
- private Context mContext;
private PackageManager mPackageManager;
/**
private HashMap<String, Entry> mCache = new HashMap<String, Entry>();
public ResolveCache(Context context) {
- mContext = context;
mPackageManager = context.getPackageManager();
}
if (size == 1) {
bestResolve = matches.get(0);
} else if (size > 1) {
- bestResolve = getBestResolve(matches);
+ bestResolve = getBestResolve(intent, matches);
}
if (bestResolve != null) {
* displaying in the track, and does not shortcut the system
* {@link Intent} disambiguation dialog.
*/
- protected ResolveInfo getBestResolve(List<ResolveInfo> matches) {
+ 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 isPrefer = QuickContactWindow.sPreferResolve
.contains(info.activityInfo.applicationInfo.packageName);
+
+
if (isPrefer) return info;
if (isSystem && firstSystem != null) firstSystem = info;
}
* Provide a strongly-typed {@link LinkedList} that holds a list of
* {@link Action} objects.
*/
- private class ActionList extends LinkedList<Action> {
+ private class ActionList extends ArrayList<Action> {
}
/**
}
/**
- * Internal storage for the latest social status, as built when walking
- * across a {@Link DataQuery} query. Will always keep record of at
- * least the first status it encounters, but will replace it with newer
- * statuses, as determined by timestamps.
- */
- private static class LatestStatus {
- private String mStatus = null;
- private long mTimestamp = -1;
-
- private String mResPackage = null;
- private int mIconRes = -1;
- private int mLabelRes = -1;
-
- private int getCursorInt(Cursor cursor, int columnIndex, int missingValue) {
- if (cursor.isNull(columnIndex)) return missingValue;
- return cursor.getInt(columnIndex);
- }
-
- /**
- * Attempt updating this {@link LatestStatus} based on values at the
- * current row of the given {@link Cursor}. Assumes that query
- * projection was {@link DataQuery#PROJECTION}.
- */
- public void possibleUpdate(Cursor cursor) {
- final boolean hasStatus = !cursor.isNull(DataQuery.STATUS);
- final boolean hasTimestamp = !cursor.isNull(DataQuery.STATUS_TIMESTAMP);
-
- // Bail early when not valid status, or when previous status was
- // found and we can't compare this one.
- if (!hasStatus) return;
- if (isValid() && !hasTimestamp) return;
-
- if (hasTimestamp) {
- // Compare timestamps and bail if older status
- final long newTimestamp = cursor.getLong(DataQuery.STATUS_TIMESTAMP);
- if (newTimestamp < mTimestamp) return;
-
- mTimestamp = newTimestamp;
- }
-
- // Fill in remaining details from cursor
- mStatus = cursor.getString(DataQuery.STATUS);
- mResPackage = cursor.getString(DataQuery.STATUS_RES_PACKAGE);
- mIconRes = getCursorInt(cursor, DataQuery.STATUS_ICON, -1);
- mLabelRes = getCursorInt(cursor, DataQuery.STATUS_LABEL, -1);
- }
-
- public boolean isValid() {
- return !TextUtils.isEmpty(mStatus);
- }
-
- public CharSequence getStatus() {
- return mStatus;
- }
-
- /**
- * Build any timestamp and label into a single string.
- */
- public CharSequence getTimestampLabel(Context context) {
- final PackageManager pm = context.getPackageManager();
-
- final boolean validTimestamp = mTimestamp > 0;
- final boolean validLabel = mResPackage != null && mLabelRes != -1;
-
- final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
- mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
- DateUtils.FORMAT_ABBREV_RELATIVE) : null;
- final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
- null) : null;
-
- if (validTimestamp && validLabel) {
- return context.getString(
- com.android.internal.R.string.contact_status_update_attribution_with_date,
- timeClause, labelClause);
- } else if (validLabel) {
- return context.getString(
- com.android.internal.R.string.contact_status_update_attribution,
- labelClause);
- } else if (validTimestamp) {
- return timeClause;
- } else {
- return null;
- }
- }
-
- public Drawable getIcon(Context context) {
- final PackageManager pm = context.getPackageManager();
- final boolean validIcon = mResPackage != null && mIconRes != -1;
- return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
- }
- }
-
- /**
* Handle the result from the {@link #TOKEN_DATA} query.
*/
private void handleData(Cursor cursor) {
mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
}
- final LatestStatus status = new LatestStatus();
+ final DataStatus status = new DataStatus();
final Sources sources = Sources.getInstance(mContext);
final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
while (cursor.moveToNext()) {
final long dataId = cursor.getLong(DataQuery._ID);
final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
- final String resPackage = cursor.getString(DataQuery.RES_PACKAGE);
final String mimeType = cursor.getString(DataQuery.MIMETYPE);
// Handle any social status updates from this row
// Read contact information from last data row
final String name = cursor.getString(DataQuery.DISPLAY_NAME);
final int presence = cursor.getInt(DataQuery.CONTACT_PRESENCE);
- final Drawable statusIcon = getPresenceIcon(presence);
+ final Drawable statusIcon = ContactPresenceIconUtil.getPresenceIcon(mContext, presence);
setHeaderText(R.id.name, name);
setHeaderImage(R.id.presence, statusIcon);
// Update status when valid was found
setHeaderText(R.id.status, status.getStatus());
setHeaderText(R.id.timestamp, status.getTimestampLabel(mContext));
+ }
+
+ // Turn our list of actions into UI elements
- final Drawable icon = status.getIcon(mContext);
- setHeaderImage(R.id.source, icon);
+ // Index where we start adding child views.
+ int index = mTrack.getChildCount() - 1;
- if (mMode == QuickContact.MODE_MEDIUM) {
- // Hide medium divider when missing icon
- final boolean validIcon = icon != null;
- mHeader.findViewById(R.id.source_divider).setVisibility(
- validIcon ? View.VISIBLE : View.GONE);
+ // All the mime-types to add.
+ final Set<String> containedTypes = new HashSet<String>(mActions.keySet());
+
+ // First, add PRECEDING_MIMETYPES, which are most common.
+ for (String mimeType : PRECEDING_MIMETYPES) {
+ if (containedTypes.contains(mimeType)) {
+ mTrack.addView(inflateAction(mimeType), index++);
+ containedTypes.remove(mimeType);
}
}
- // Turn our list of actions into UI elements, starting with common types
- final Set<String> containedTypes = mActions.keySet();
- for (String mimeType : ORDERED_MIMETYPES) {
+ // Keep the current index to append non PRECEDING/FOLLOWING items.
+ final int indexAfterPreceding = index;
+
+ // Then, add FOLLOWING_MIMETYPES, which are least common.
+ for (String mimeType : FOLLOWING_MIMETYPES) {
if (containedTypes.contains(mimeType)) {
- final int index = mTrack.getChildCount() - 1;
- mTrack.addView(inflateAction(mimeType), index);
+ mTrack.addView(inflateAction(mimeType), index++);
containedTypes.remove(mimeType);
}
}
- // Then continue with remaining MIME-types in alphabetical order
+ // Go back to just after PRECEDING_MIMETYPES, and append the rest.
+ index = indexAfterPreceding;
final String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
Arrays.sort(remainingTypes);
for (String mimeType : remainingTypes) {
- final int index = mTrack.getChildCount() - 1;
- mTrack.addView(inflateAction(mimeType), index);
+ mTrack.addView(inflateAction(mimeType), index++);
}
}
}
/**
- * Inflate the in-track view for the action of the given MIME-type. Will use
- * the icon provided by the {@link DataKind}.
+ * 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) {
final CheckableImageView view = (CheckableImageView)obtainView();
// Add direct intent if single child, otherwise flag for multiple
ActionList children = mActions.get(mimeType);
+ if (children.size() > 1) {
+ Collapser.collapseList(children);
+ }
Action firstInfo = children.get(0);
if (children.size() == 1) {
view.setTag(firstInfo);
if (tag instanceof Action) {
// Incoming tag is concrete intent, so try launching
final Action action = (Action)tag;
+ final boolean makePrimary = mMakePrimary;
try {
mContext.startActivity(action.getIntent());
}
// Hide the resolution list, if present
- setResolveVisible(false, actionView);
+ setResolveVisible(false, null);
this.dismiss();
- if (mMakePrimary) {
+ if (makePrimary) {
ContentValues values = new ContentValues(1);
values.put(Data.IS_SUPER_PRIMARY, 1);
final Uri dataUri = action.getDataUri();
// Set action title based on summary value
final Action action = (Action)getItem(position);
- final Drawable icon = mResolveCache.getIcon(action);
TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
});
// Make sure we resize to make room for ListView
- onWindowAttributesChanged(mWindow.getAttributes());
+ mDecor.forceLayout();
+ mDecor.invalidate();
}
}
// it will close the entire dialog.
if (mFooterDisambig.getVisibility() == View.VISIBLE) {
setResolveVisible(false, null);
+ mDecor.forceLayout();
+ mDecor.invalidate();
} else {
dismiss();
}
* window, which usually means we should dismiss.
*/
protected void detectEventOutside(MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN && mDecor != null) {
// Only try detecting outside events on down-press
mDecor.getHitRect(mRect);
- mRect.top = mRect.top + mDecor.getPaddingTop();
- mRect.bottom = mRect.bottom - mDecor.getPaddingBottom();
+ mRect.top = mRect.top + mShadowTouch;
+ mRect.bottom = mRect.bottom - mShadowTouch;
final int x = (int)event.getX();
final int y = (int)event.getY();
if (!mRect.contains(x, y)) {
}
/** {@inheritDoc} */
- public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
- // No actions
- }
-
- /** {@inheritDoc} */
public void onAttachedToWindow() {
// No actions
}