blob: 8662573d7a05dacc540ed615be4420500fe63f1e [file] [log] [blame]
/*
* Copyright (C) 2009 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.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.Sources;
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.Sets;
import android.content.ActivityNotFoundException;
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.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.QuickContact;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.Window;
import android.view.WindowManager;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RelativeLayout;
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;
import java.util.Set;
/**
* Window that shows QuickContact dialog for a specific {@link Contacts#_ID}.
*/
public class QuickContactWindow implements Window.Callback,
NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
AbsListView.OnItemClickListener, CompoundButton.OnCheckedChangeListener, KeyEvent.Callback,
OnGlobalLayoutListener {
private static final String TAG = "QuickContactWindow";
/**
* Interface used to allow the person showing a {@link QuickContactWindow} to
* know when the window has been dismissed.
*/
public interface OnDismissListener {
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 Window mWindow;
private View mDecor;
private final Rect mRect = new Rect();
private boolean mDismissed = false;
private boolean mQuerying = false;
private boolean mShowing = false;
private NotifyingAsyncQueryHandler mHandler;
private OnDismissListener mDismissListener;
private ResolveCache mResolveCache;
private Uri mLookupUri;
private Rect mAnchor;
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 boolean mMakePrimary = false;
private ImageView mArrowUp;
private ImageView mArrowDown;
private int mMode;
private RootLayout mRootView;
private View mHeader;
private HorizontalScrollView mTrackScroll;
private ViewGroup mTrack;
private Animation mTrackAnim;
private View mFooter;
private View mFooterDisambig;
private ListView mResolveList;
private CheckableImageView mLastAction;
private CheckBox mSetPrimaryCheckBox;
private int mWindowRecycled = 0;
private int mActionRecycled = 0;
/**
* 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}.
*/
private ActionMap mActions = new ActionMap();
/**
* Pool of unused {@link CheckableImageView} that have previously been
* inflated, and are ready to be recycled through {@link #obtainView()}.
*/
private LinkedList<View> mActionPool = new LinkedList<View>();
private String[] mExcludeMimes;
/**
* {@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[] PRECEDING_MIMETYPES = new String[] {
Phone.CONTENT_ITEM_TYPE,
SipAddress.CONTENT_ITEM_TYPE,
Contacts.CONTENT_ITEM_TYPE,
Constants.MIME_SMS_ADDRESS,
Email.CONTENT_ITEM_TYPE,
};
/**
* 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(
"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";
/**
* Prepare a dialog to show in the given {@link Context}.
*/
public QuickContactWindow(Context context) {
mContext = new ContextThemeWrapper(context, R.style.QuickContact);
mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
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();
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);
mFooter = mWindow.findViewById(R.id.footer);
mFooterDisambig = mWindow.findViewById(R.id.footer_disambig);
mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
mSetPrimaryCheckBox = (CheckBox)mWindow.findViewById(android.R.id.checkbox);
mSetPrimaryCheckBox.setOnCheckedChangeListener(this);
// Prepare track entrance animation
mTrackAnim = AnimationUtils.loadAnimation(mContext, R.anim.quickcontact);
mTrackAnim.setInterpolator(new Interpolator() {
public float getInterpolation(float t) {
// Pushes past the target area, then snaps back into place.
// Equation for graphing: 1.2-((x*1.6)-1.1)^2
final float inner = (t * 1.55f) - 1.1f;
return 1.2f - inner * inner;
}
});
mHandler = new NotifyingAsyncQueryHandler(mContext, this);
}
/**
* Prepare a dialog to show in the given {@link Context}, and notify the
* given {@link OnDismissListener} each time this dialog is dismissed.
*/
public QuickContactWindow(Context context, OnDismissListener dismissListener) {
this(context);
mDismissListener = dismissListener;
}
private View getHeaderView(int mode) {
View header = null;
switch (mode) {
case QuickContact.MODE_SMALL:
header = mWindow.findViewById(R.id.header_small);
break;
case QuickContact.MODE_MEDIUM:
header = mWindow.findViewById(R.id.header_medium);
break;
case QuickContact.MODE_LARGE:
header = mWindow.findViewById(R.id.header_large);
break;
}
if (header instanceof ViewStub) {
// Inflate actual header if we picked a stub
final ViewStub stub = (ViewStub)header;
header = stub.inflate();
} else if (header != null) {
header.setVisibility(View.VISIBLE);
}
return header;
}
/**
* Start showing a dialog for the given {@link Contacts#_ID} pointing
* towards the given location.
*/
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);
mMode = mode;
mExcludeMimes = excludeMimes;
mHeader = getHeaderView(mode);
setHeaderText(R.id.name, R.string.quickcontact_missing_name);
setHeaderText(R.id.status, null);
setHeaderText(R.id.timestamp, null);
setHeaderImage(R.id.presence, 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
// directly match the super-primary PHOTO_ID.
final Uri dataUri = getDataUri(lookupUri);
mHandler.cancelOperation(TOKEN_DATA);
// Only request photo data when required by mode
if (mMode == QuickContact.MODE_LARGE) {
// Select photos, but only super-primary
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, lookupUri, dataUri, DataQuery.PROJECTION, Data.MIMETYPE
+ "!=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
}
}
/**
* Build a {@link Uri} into the {@link Data} table for the requested
* {@link Contacts#CONTENT_LOOKUP_URI} style {@link Uri}.
*/
private Uri getDataUri(Uri lookupUri) {
// TODO: Formalize method of extracting LOOKUP_KEY
final List<String> path = lookupUri.getPathSegments();
final boolean validLookup = path.size() >= 3 && "lookup".equals(path.get(1));
if (!validLookup) {
// We only accept valid lookup-style Uris
throw new IllegalArgumentException("Expecting lookup-style Uri");
} else if (path.size() == 3) {
// No direct _ID provided, so force a lookup
lookupUri = Contacts.lookupContact(mContext.getContentResolver(), lookupUri);
}
final long contactId = ContentUris.parseId(lookupUri);
return Uri.withAppendedPath(ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
Contacts.Data.CONTENT_DIRECTORY);
}
/**
* Show the correct call-out arrow based on a {@link R.id} reference.
*/
private void showArrow(int whichArrow, int requestedX) {
final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
final int arrowWidth = mArrowUp.getMeasuredWidth();
showArrow.setVisibility(View.VISIBLE);
ViewGroup.MarginLayoutParams param = (ViewGroup.MarginLayoutParams)showArrow.getLayoutParams();
param.leftMargin = requestedX - arrowWidth / 2;
hideArrow.setVisibility(View.INVISIBLE);
}
/**
* Actual internal method to show this dialog. Called only by
* {@link #considerShowing()} when all data requirements have been met.
*/
private void showInternal() {
mDecor = mWindow.getDecorView();
mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this);
WindowManager.LayoutParams l = mWindow.getAttributes();
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 = -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 + 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 - mShadowVert;
l.windowAnimations = R.style.QuickContactBelowAnimation;
}
l.flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
mRequestedY = l.y;
mWindowManager.addView(mDecor, l);
mShowing = true;
mQuerying = false;
mDismissed = false;
mTrack.startAnimation(mTrackAnim);
if (TRACE_LAUNCH) {
android.os.Debug.stopMethodTracing();
Log.d(TAG, "Window recycled " + mWindowRecycled + " times, chiclets "
+ mActionRecycled + " times");
}
}
/** {@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 synchronized void dismiss() {
// Notify any listeners that we've been dismissed
if (mDismissListener != null) {
mDismissListener.onDismiss(this);
}
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;
// Cancel any pending queries
mHandler.cancelOperation(TOKEN_DATA);
mQuerying = false;
// Completely hide header and reset track
mHeader.setVisibility(View.GONE);
resetTrack();
}
/**
* 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();
mActions.clear();
// Recycle any chiclets in use
while (mTrack.getChildCount() > 2) {
this.releaseView(mTrack.getChildAt(1));
mTrack.removeViewAt(1);
}
mTrackScroll.fullScroll(View.FOCUS_LEFT);
mWasDownArrow = false;
// Clear any primary requests
mMakePrimary = false;
mSetPrimaryCheckBox.setChecked(false);
setResolveVisible(false, null);
}
/**
* Consider showing this window, which will only call through to
* {@link #showInternal()} when all data items are present.
*/
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);
mHeader = getHeaderView(QuickContact.MODE_SMALL);
}
// All queries have returned, pull curtain
showInternal();
}
}
/** {@inheritDoc} */
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);
this.dismiss();
return;
}
handleData(cursor);
mHasData = true;
if (!cursor.isClosed()) {
cursor.close();
}
considerShowing();
}
/** Assign this string to the view, if found in {@link #mHeader}. */
private void setHeaderText(int id, int resId) {
setHeaderText(id, mContext.getResources().getText(resId));
}
/** Assign this string to the view, if found in {@link #mHeader}. */
private void setHeaderText(int id, CharSequence value) {
final View view = mHeader.findViewById(id);
if (view instanceof TextView) {
((TextView)view).setText(value);
view.setVisibility(TextUtils.isEmpty(value) ? View.GONE : View.VISIBLE);
}
}
/** 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) {
((ImageView)view).setImageDrawable(drawable);
view.setVisibility(drawable == null ? View.GONE : View.VISIBLE);
}
}
/**
* Find the QuickContact-specific presence icon for showing in chiclets.
*/
private Drawable getTrackPresenceIcon(int status) {
int resId;
switch (status) {
case StatusUpdates.AVAILABLE:
resId = R.drawable.quickcontact_slider_presence_active;
break;
case StatusUpdates.IDLE:
case StatusUpdates.AWAY:
resId = R.drawable.quickcontact_slider_presence_away;
break;
case StatusUpdates.DO_NOT_DISTURB:
resId = R.drawable.quickcontact_slider_presence_busy;
break;
case StatusUpdates.INVISIBLE:
resId = R.drawable.quickcontact_slider_presence_inactive;
break;
case StatusUpdates.OFFLINE:
default:
resId = R.drawable.quickcontact_slider_presence_inactive;
}
return mContext.getResources().getDrawable(resId);
}
/** 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)) {
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)) {
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)) {
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} */
public CharSequence getHeader() {
return mHeader;
}
/** {@inheritDoc} */
public CharSequence getBody() {
return mBody;
}
/** {@inheritDoc} */
public String getMimeType() {
return mMimeType;
}
/** {@inheritDoc} */
public Uri getDataUri() {
return mDataUri;
}
/** {@inheritDoc} */
public Boolean isPrimary() {
return mIsPrimary;
}
/** {@inheritDoc} */
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} */
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.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} */
public CharSequence getHeader() {
return null;
}
/** {@inheritDoc} */
public CharSequence getBody() {
return null;
}
/** {@inheritDoc} */
public String getMimeType() {
return Contacts.CONTENT_ITEM_TYPE;
}
/** {@inheritDoc} */
public Drawable getFallbackIcon() {
return mContext.getResources().getDrawable(R.drawable.ic_contacts_details);
}
/** {@inheritDoc} */
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} */
public Boolean isPrimary() {
return null;
}
/** {@inheritDoc} */
public Uri getDataUri() {
return null;
}
/** {@inheritDoc} */
public boolean collapseWith(Action t) {
return false; // Never dup.
}
/** {@inheritDoc} */
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 SoftReference<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 = new SoftReference<Drawable>(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) {
final SoftReference<Drawable> iconRef = getEntry(action).icon;
return (iconRef == null) ? null : iconRef.get();
}
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.
*/
private boolean isMimeExcluded(String mimeType) {
if (mExcludeMimes == null) return false;
for (String excludedMime : mExcludeMimes) {
if (TextUtils.equals(excludedMime, mimeType)) {
return true;
}
}
return false;
}
/**
* Handle the result from the {@link #TOKEN_DATA} query.
*/
private void handleData(Cursor cursor) {
if (cursor == null) return;
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);
}
final DataStatus status = new DataStatus();
final Sources sources = Sources.getInstance(mContext);
final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
Bitmap photoBitmap = null;
while (cursor.moveToNext()) {
final long dataId = cursor.getLong(DataQuery._ID);
final String accountType = cursor.getString(DataQuery.ACCOUNT_TYPE);
final String mimeType = cursor.getString(DataQuery.MIMETYPE);
// Handle any social status updates from this row
status.possibleUpdate(cursor);
// Skip this data item if MIME-type excluded
if (isMimeExcluded(mimeType)) continue;
// Handle photos included as data row
if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
final byte[] photoBlob = cursor.getBlob(colPhoto);
if (photoBlob != null) {
photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
}
continue;
}
final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
ContactsSource.LEVEL_MIMETYPES);
if (kind != null) {
// Build an action for this data entry, find a mapping to a UI
// 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);
}
// 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);
}
// Handle Email rows with presence data as Im entry
final boolean hasPresence = !cursor.isNull(DataQuery.PRESENCE);
if (hasPresence && Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
final DataKind imKind = sources.getKindOrFallback(accountType,
Im.CONTENT_ITEM_TYPE, mContext, ContactsSource.LEVEL_MIMETYPES);
if (imKind != null) {
final Action action = new DataAction(mContext, Im.CONTENT_ITEM_TYPE, imKind,
dataId, cursor);
considerAdd(action, Im.CONTENT_ITEM_TYPE);
}
}
}
if (cursor.moveToLast()) {
// 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 = ContactPresenceIconUtil.getPresenceIcon(mContext, presence);
setHeaderText(R.id.name, name);
setHeaderImage(R.id.presence, statusIcon);
}
if (photoView != null) {
// Place photo when discovered in data, otherwise hide
photoView.setVisibility(photoBitmap != null ? View.VISIBLE : View.GONE);
photoView.setImageBitmap(photoBitmap);
}
mHasValidSocial = status.isValid();
if (mHasValidSocial && mMode != QuickContact.MODE_SMALL) {
// 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
// Index where we start adding child views.
int index = mTrack.getChildCount() - 1;
// 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);
}
}
// 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)) {
mTrack.addView(inflateAction(mimeType), index++);
containedTypes.remove(mimeType);
}
}
// 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) {
mTrack.addView(inflateAction(mimeType), index++);
}
}
/**
* Consider adding the given {@link Action}, which will only happen if
* {@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);
}
}
/**
* Obtain a new {@link CheckableImageView} for a new chiclet, either by
* recycling one from {@link #mActionPool}, or by inflating a new one. When
* finished, use {@link #releaseView(View)} to return back into the pool for
* later recycling.
*/
private synchronized View obtainView() {
View view = mActionPool.poll();
if (view == null || QuickContactActivity.FORCE_CREATE) {
view = mInflater.inflate(R.layout.quickcontact_item, mTrack, false);
}
return view;
}
/**
* Return the given {@link CheckableImageView} into our internal pool for
* possible recycling during another pass.
*/
private synchronized void releaseView(View view) {
mActionPool.offer(view);
mActionRecycled++;
}
/**
* 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();
boolean isActionSet = false;
// 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);
} else {
for (Action action : children) {
if (action.isPrimary()) {
view.setTag(action);
isActionSet = true;
break;
}
}
if (!isActionSet) {
view.setTag(children);
}
}
// Set icon and listen for clicks
final CharSequence descrip = mResolveCache.getDescription(firstInfo);
final Drawable icon = mResolveCache.getIcon(firstInfo);
view.setChecked(false);
view.setContentDescription(descrip);
view.setImageDrawable(icon);
view.setOnClickListener(this);
return view;
}
/** {@inheritDoc} */
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// Pass list item clicks along so that Intents are handled uniformly
onClick(view);
}
/**
* Flag indicating if {@link #mArrowDown} was visible during the last call
* to {@link #setResolveVisible(boolean, CheckableImageView)}. Used to
* decide during a later call if the arrow should be restored.
*/
private boolean mWasDownArrow = false;
/**
* Helper for showing and hiding {@link #mFooterDisambig}, which will
* correctly manage {@link #mArrowDown} as needed.
*/
private void setResolveVisible(boolean visible, CheckableImageView actionView) {
// Show or hide the resolve list if needed
boolean visibleNow = mFooterDisambig.getVisibility() == View.VISIBLE;
if (mLastAction != null) mLastAction.setChecked(false);
if (actionView != null) actionView.setChecked(true);
mLastAction = actionView;
// Bail early if already in desired state
if (visible == visibleNow) return;
mFooter.setVisibility(visible ? View.GONE : View.VISIBLE);
mFooterDisambig.setVisibility(visible ? View.VISIBLE : View.GONE);
if (visible) {
// If showing list, then hide and save state of down arrow
mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
mArrowDown.setVisibility(View.INVISIBLE);
} else {
// If hiding list, restore any down arrow state
mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
}
}
/** {@inheritDoc} */
public void onClick(View view) {
final boolean isActionView = (view instanceof CheckableImageView);
final CheckableImageView actionView = isActionView ? (CheckableImageView)view : null;
final Object tag = view.getTag();
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());
} catch (ActivityNotFoundException e) {
Toast.makeText(mContext, R.string.quickcontact_missing_app, Toast.LENGTH_SHORT)
.show();
}
// Hide the resolution list, if present
setResolveVisible(false, null);
this.dismiss();
if (makePrimary) {
ContentValues values = new ContentValues(1);
values.put(Data.IS_SUPER_PRIMARY, 1);
final Uri dataUri = action.getDataUri();
if (dataUri != null) {
mContext.getContentResolver().update(dataUri, values, null, null);
}
}
} else if (tag instanceof ActionList) {
// Incoming tag is a MIME-type, so show resolution list
final ActionList children = (ActionList)tag;
// Show resolution list and set adapter
setResolveVisible(true, actionView);
mResolveList.setOnItemClickListener(this);
mResolveList.setAdapter(new BaseAdapter() {
public int getCount() {
return children.size();
}
public Object getItem(int position) {
return children.get(position);
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(
R.layout.quickcontact_resolve_item, parent, false);
}
// Set action title based on summary value
final Action action = (Action)getItem(position);
TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
text1.setText(action.getHeader());
text2.setText(action.getBody());
convertView.setTag(action);
return convertView;
}
});
// Make sure we resize to make room for ListView
mDecor.forceLayout();
mDecor.invalidate();
}
}
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mMakePrimary = isChecked;
}
private void onBackPressed() {
// Back key will first dismiss any expanded resolve list, otherwise
// it will close the entire dialog.
if (mFooterDisambig.getVisibility() == View.VISIBLE) {
setResolveVisible(false, null);
mDecor.forceLayout();
mDecor.invalidate();
} else {
dismiss();
}
}
/** {@inheritDoc} */
public boolean dispatchKeyEvent(KeyEvent event) {
if (mWindow.superDispatchKeyEvent(event)) {
return true;
}
return event.dispatch(this, mDecor != null
? mDecor.getKeyDispatcherState() : null, this);
}
/** {@inheritDoc} */
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
event.startTracking();
return true;
}
return false;
}
/** {@inheritDoc} */
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
&& !event.isCanceled()) {
onBackPressed();
return true;
}
return false;
}
/** {@inheritDoc} */
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
return false;
}
/** {@inheritDoc} */
public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
return false;
}
/** {@inheritDoc} */
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
// TODO: make this window accessible
return false;
}
/**
* Detect if the given {@link MotionEvent} is outside the boundaries of this
* window, which usually means we should dismiss.
*/
protected void detectEventOutside(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN && mDecor != null) {
// Only try detecting outside events on down-press
mDecor.getHitRect(mRect);
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)) {
event.setAction(MotionEvent.ACTION_OUTSIDE);
}
}
}
/** {@inheritDoc} */
public boolean dispatchTouchEvent(MotionEvent event) {
detectEventOutside(event);
if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
dismiss();
return true;
} else {
return mWindow.superDispatchTouchEvent(event);
}
}
/** {@inheritDoc} */
public boolean dispatchTrackballEvent(MotionEvent event) {
return mWindow.superDispatchTrackballEvent(event);
}
/** {@inheritDoc} */
public void onContentChanged() {
}
/** {@inheritDoc} */
public boolean onCreatePanelMenu(int featureId, Menu menu) {
return false;
}
/** {@inheritDoc} */
public View onCreatePanelView(int featureId) {
return null;
}
/** {@inheritDoc} */
public boolean onMenuItemSelected(int featureId, MenuItem item) {
return false;
}
/** {@inheritDoc} */
public boolean onMenuOpened(int featureId, Menu menu) {
return false;
}
/** {@inheritDoc} */
public void onPanelClosed(int featureId, Menu menu) {
}
/** {@inheritDoc} */
public boolean onPreparePanel(int featureId, View view, Menu menu) {
return false;
}
/** {@inheritDoc} */
public boolean onSearchRequested() {
return false;
}
/** {@inheritDoc} */
public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
if (mDecor != null) {
mWindowManager.updateViewLayout(mDecor, attrs);
}
}
/** {@inheritDoc} */
public void onWindowFocusChanged(boolean hasFocus) {
}
/** {@inheritDoc} */
public void onAttachedToWindow() {
// No actions
}
/** {@inheritDoc} */
public void onDetachedFromWindow() {
// No actions
}
private interface DataQuery {
final String[] PROJECTION = new String[] {
Data._ID,
RawContacts.ACCOUNT_TYPE,
Contacts.STARRED,
Contacts.DISPLAY_NAME,
Contacts.CONTACT_PRESENCE,
Data.STATUS,
Data.STATUS_RES_PACKAGE,
Data.STATUS_ICON,
Data.STATUS_LABEL,
Data.STATUS_TIMESTAMP,
Data.PRESENCE,
Data.RES_PACKAGE,
Data.MIMETYPE,
Data.IS_PRIMARY,
Data.IS_SUPER_PRIMARY,
Data.RAW_CONTACT_ID,
Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
};
final int _ID = 0;
final int ACCOUNT_TYPE = 1;
final int STARRED = 2;
final int DISPLAY_NAME = 3;
final int CONTACT_PRESENCE = 4;
final int STATUS = 5;
final int STATUS_RES_PACKAGE = 6;
final int STATUS_ICON = 7;
final int STATUS_LABEL = 8;
final int STATUS_TIMESTAMP = 9;
final int PRESENCE = 10;
final int RES_PACKAGE = 11;
final int MIMETYPE = 12;
final int IS_PRIMARY = 13;
final int IS_SUPER_PRIMARY = 14;
}
}