Bring back Fast-Track with yummy assets and animations.
Fast-Track had been quite neglected, so brought it back up
to speed with ContactsContract changes. Also rearranged
the Action launching logic to use ContactsSource.DataKind
for any icons and label generation. Wrote slide animation
that looks awesome.
Added Fast-Track triggering to normal contacts list, since
it has photos now. Also supports being launching in various
modes, which for now are three sizes.
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 2b878bc..7361019 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -18,6 +18,7 @@
import com.android.contacts.DisplayGroupsActivity.Prefs;
import com.android.contacts.ui.EditContactActivity;
+import com.android.contacts.ui.FastTrackWindow;
import android.app.Activity;
import android.app.AlertDialog;
@@ -35,6 +36,7 @@
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -91,7 +93,7 @@
* Displays a list of contacts. Usually is embedded into the ContactsActivity.
*/
public final class ContactsListActivity extends ListActivity implements
- View.OnCreateContextMenuListener {
+ View.OnCreateContextMenuListener, View.OnClickListener {
private static final String TAG = "ContactsListActivity";
private static final String LIST_STATE_KEY = "liststate";
@@ -245,9 +247,8 @@
private static final int QUERY_TOKEN = 42;
- /*
- */
- ContactItemListAdapter mAdapter;
+ private FastTrackWindow mFastTrack;
+ private ContactItemListAdapter mAdapter;
int mMode = MODE_DEFAULT;
@@ -474,6 +475,7 @@
// Set the proper empty string
setEmptyText();
+ mFastTrack = new FastTrackWindow(this);
mAdapter = new ContactItemListAdapter(this);
setListAdapter(mAdapter);
getListView().setOnScrollListener(mAdapter);
@@ -509,6 +511,30 @@
// }
}
+ private int[] mLocation = new int[2];
+ private Rect mRect = new Rect();
+
+ private void showFastTrack(View anchor, long contactId) {
+ final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+
+ anchor.getLocationInWindow(mLocation);
+ mRect.left = mLocation[0];
+ mRect.top = mLocation[1];
+ mRect.right = mRect.left + anchor.getWidth();
+ mRect.bottom = mRect.top + anchor.getHeight();
+
+ mFastTrack.dismiss();
+ mFastTrack.show(contactUri, mRect, Intents.MODE_MEDIUM);
+ }
+
+ /** {@inheritDoc} */
+ public void onClick(View v) {
+ // Clicked on photo, so show fast-track
+ final int position = (Integer)v.getTag();
+ final long contactId = this.getListView().getItemIdAtPosition(position);
+ showFastTrack(v, contactId);
+ }
+
private void setEmptyText() {
TextView empty = (TextView) findViewById(R.id.emptyText);
// Center the text by default
@@ -828,13 +854,20 @@
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK: {
+ if (mFastTrack.isShowing()) {
+ // Back key dismisses fast-track when its visible
+ mFastTrack.dismiss();
+ return true;
+ }
+ break;
+ }
case KeyEvent.KEYCODE_CALL: {
if (callSelection()) {
return true;
}
break;
}
-
case KeyEvent.KEYCODE_DEL: {
Object o = getListView().getSelectedItem();
if (o != null) {
@@ -1533,6 +1566,7 @@
cache.dataView = (TextView) view.findViewById(R.id.data);
cache.presenceView = (ImageView) view.findViewById(R.id.presence);
cache.photoView = (ImageView) view.findViewById(R.id.photo);
+ cache.photoView.setOnClickListener(ContactsListActivity.this);
view.setTag(cache);
return view;
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 156f487..9bc8f74 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -17,6 +17,8 @@
package com.android.contacts;
+import com.android.contacts.ui.FastTrackWindow;
+
import java.io.ByteArrayInputStream;
import android.provider.ContactsContract.Data;
import java.io.InputStream;
diff --git a/src/com/android/contacts/FastTrackWindow.java b/src/com/android/contacts/FastTrackWindow.java
deleted file mode 100644
index d763cf7..0000000
--- a/src/com/android/contacts/FastTrackWindow.java
+++ /dev/null
@@ -1,850 +0,0 @@
-/*
- * 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;
-
-import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
-import com.android.contacts.SocialStreamActivity.Mapping;
-import com.android.contacts.SocialStreamActivity.MappingCache;
-import com.android.internal.policy.PolicyManager;
-
-import android.content.ActivityNotFoundException;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.provider.ContactsContract;
-import android.provider.SocialContract;
-import android.provider.Contacts.Phones;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.CommonDataKinds;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.Presence;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.Im.PresenceColumns;
-import android.provider.SocialContract.Activities;
-import android.text.SpannableStringBuilder;
-import android.text.format.DateUtils;
-import android.text.style.CharacterStyle;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
-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.Window;
-import android.view.WindowManager;
-import android.view.View.OnClickListener;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.AbsListView;
-import android.widget.AdapterView;
-import android.widget.BaseAdapter;
-import android.widget.HorizontalScrollView;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.RemoteViews;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.Set;
-
-/**
- * Window that shows fast-track contact details for a specific
- * {@link Contacts#_ID}.
- */
-public class FastTrackWindow implements Window.Callback, QueryCompleteListener, OnClickListener,
- AbsListView.OnItemClickListener {
- private static final String TAG = "FastTrackWindow";
-
- /**
- * Interface used to allow the person showing a {@link FastTrackWindow} to
- * know when the window has been dismissed.
- */
- interface OnDismissListener {
- public void onDismiss(FastTrackWindow dialog);
- }
-
- final Context mContext;
- final LayoutInflater mInflater;
- final WindowManager mWindowManager;
- Window mWindow;
- View mDecor;
-
- private boolean mQuerying = false;
- private boolean mShowing = false;
-
- /** Mapping cache from mime-type to icons and actions */
- private MappingCache mMappingCache;
-
- private NotifyingAsyncQueryHandler mHandler;
- private OnDismissListener mDismissListener;
-
- private long mAggId;
- private Rect mAnchor;
-
- private boolean mHasSummary = false;
- private boolean mHasSocial = false;
- private boolean mHasActions = false;
-
- private View mArrowUp;
- private View mArrowDown;
-
- private ImageView mPhoto;
- private ImageView mPresence;
- private TextView mContent;
- private TextView mPublished;
- private HorizontalScrollView mTrackScroll;
- private ViewGroup mTrack;
- private ListView mResolveList;
-
- private String mDisplayName = null;
- private String mSocialTitle = null;
-
- private SpannableStringBuilder mBuilder = new SpannableStringBuilder();
- private CharacterStyle mStyleBold = new StyleSpan(android.graphics.Typeface.BOLD);
- private CharacterStyle mStyleBlack = new ForegroundColorSpan(Color.BLACK);
-
- /**
- * Set of {@link ActionInfo} that are associated with the aggregate
- * currently displayed by this fast-track window, represented as a map from
- * {@link String} mimetype to {@link ActionList}.
- */
- private ActionMap mActions = new ActionMap();
-
- // TODO We should move this to someplace more general as it is needed in a few places in the app
- // code.
- /**
- * Specific mime-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
- * distinguishes actions that should initiate a text message.
- */
- public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
-
- /**
- * Specific mime-types that should be bumped to the front of the fast-track.
- * Other mime-types not appearing in this list follow in alphabetic order.
- */
- private static final String[] ORDERED_MIMETYPES = new String[] {
- Phones.CONTENT_ITEM_TYPE,
- Contacts.CONTENT_ITEM_TYPE,
- MIME_SMS_ADDRESS,
- Email.CONTENT_ITEM_TYPE,
- };
-
- private static final boolean INCLUDE_PROFILE_ACTION = true;
-
- private static final int TOKEN_SUMMARY = 1;
- private static final int TOKEN_SOCIAL = 2;
- private static final int TOKEN_DATA = 3;
-
- /** Message to show when no activity is found to perform an action */
- // TODO: move this value into a resources string
- private static final String NOT_FOUND = "Couldn't find an app to handle this action";
-
- /**
- * Prepare a fast-track window to show in the given {@link Context}.
- */
- public FastTrackWindow(Context context) {
- mContext = new ContextThemeWrapper(context, R.style.FastTrack);
- 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.setContentView(R.layout.fasttrack);
-
- mArrowUp = mWindow.findViewById(R.id.arrow_up);
- mArrowDown = mWindow.findViewById(R.id.arrow_down);
-
- mPhoto = (ImageView)mWindow.findViewById(R.id.photo);
- mPresence = (ImageView)mWindow.findViewById(R.id.presence);
- mContent = (TextView)mWindow.findViewById(R.id.content);
- mPublished = (TextView)mWindow.findViewById(R.id.published);
- mTrack = (ViewGroup)mWindow.findViewById(R.id.fasttrack);
- mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
- mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
-
- // TODO: move generation of mime-type cache to more-efficient place
- generateMappingCache();
-
- }
-
- /**
- * Prepare a fast-track window to show in the given {@link Context}, and
- * notify the given {@link OnDismissListener} each time this dialog is
- * dismissed.
- */
- public FastTrackWindow(Context context, OnDismissListener dismissListener) {
- this(context);
- mDismissListener = dismissListener;
- }
-
- /**
- * Generate {@link MappingCache} specifically for fast-track windows. This
- * cache knows how to display {@link CommonDataKinds#PACKAGE_COMMON} data
- * types using generic icons.
- */
- private void generateMappingCache() {
- mMappingCache = MappingCache.createAndFill(mContext);
-
- Resources res = mContext.getResources();
- Mapping mapping;
-
- mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Contacts.CONTENT_ITEM_TYPE);
- mapping.icon = BitmapFactory.decodeResource(res, R.drawable.ic_contacts_details);
- mMappingCache.addMapping(mapping);
-
- mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Phone.CONTENT_ITEM_TYPE);
- mapping.summaryColumn = Phone.TYPE;
- mapping.detailColumn = Phone.NUMBER;
- mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_call);
- mMappingCache.addMapping(mapping);
-
- mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, MIME_SMS_ADDRESS);
- mapping.summaryColumn = Phone.TYPE;
- mapping.detailColumn = Phone.NUMBER;
- mapping.icon = BitmapFactory.decodeResource(res, R.drawable.sym_action_sms);
- mMappingCache.addMapping(mapping);
-
- mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Email.CONTENT_ITEM_TYPE);
- mapping.summaryColumn = Email.TYPE;
- mapping.detailColumn = Email.DATA;
- mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_email);
- mMappingCache.addMapping(mapping);
-
- }
-
- /**
- * Start showing a fast-track window for the given {@link Aggregate#_ID}
- * pointing towards the given location.
- */
- public void show(Uri aggUri, Rect anchor) {
- if (mShowing || mQuerying) {
- Log.w(TAG, "already in process of showing");
- return;
- }
-
- mAggId = ContentUris.parseId(aggUri);
- mAnchor = new Rect(anchor);
- mQuerying = true;
-
- Uri aggSummary = ContentUris.withAppendedId(
- ContactsContract.Contacts.CONTENT_SUMMARY_URI, mAggId);
- Uri aggSocial = ContentUris.withAppendedId(
- SocialContract.Activities.CONTENT_CONTACT_STATUS_URI, mAggId);
- Uri aggData = Uri.withAppendedPath(aggUri,
- ContactsContract.Contacts.Data.CONTENT_DIRECTORY);
-
- // Start data query in background
- mHandler = new NotifyingAsyncQueryHandler(mContext, this);
- mHandler.startQuery(TOKEN_SUMMARY, null, aggSummary, null, null, null, null);
- mHandler.startQuery(TOKEN_SOCIAL, null, aggSocial, null, null, null, null);
- mHandler.startQuery(TOKEN_DATA, null, aggData, null, null, null, null);
-
- }
-
- /**
- * Show the correct callout 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);
- LinearLayout.LayoutParams param = (LinearLayout.LayoutParams)showArrow.getLayoutParams();
- param.leftMargin = requestedX - arrowWidth / 2;
-
- hideArrow.setVisibility(View.INVISIBLE);
- }
-
- /**
- * Actual internal method to show this fast-track window. Called only by
- * {@link #considerShowing()} when all data requirements have been met.
- */
- private void showInternal() {
- mDecor = mWindow.getDecorView();
- WindowManager.LayoutParams l = mWindow.getAttributes();
-
- l.width = WindowManager.LayoutParams.FILL_PARENT;
- 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();
- final int arrowHeight = mArrowUp.getHeight();
-
- l.gravity = Gravity.TOP | Gravity.LEFT;
- l.x = 0;
-
- 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 + arrowHeight;
-
- } 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 - arrowHeight;
-
- }
-
- l.dimAmount = 0.6f;
- l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND
- | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
- | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
- | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
-
- mWindowManager.addView(mDecor, l);
- mShowing = true;
- mQuerying = false;
- }
-
- /**
- * Dismiss this fast-track window if showing.
- */
- public void dismiss() {
- if (!mQuerying && !mShowing) {
- Log.d(TAG, "not visible, ignore");
- return;
- }
-
- // Cancel any pending queries
- mHandler.cancelOperation(TOKEN_SUMMARY);
- mHandler.cancelOperation(TOKEN_SOCIAL);
- mHandler.cancelOperation(TOKEN_DATA);
-
- // Reset all views to prepare for possible recycling
- mPhoto.setImageResource(R.drawable.ic_contact_list_picture);
- mPresence.setImageDrawable(null);
- mPublished.setText(null);
- mContent.setText(null);
-
- mDisplayName = null;
- mSocialTitle = null;
-
- // Clear track actions and scroll to hard left
- mActions.clear();
- mTrack.removeAllViews();
- mTrackScroll.fullScroll(View.FOCUS_LEFT);
- mWasDownArrow = false;
-
- showResolveList(View.GONE);
-
- mHasSocial = false;
- mHasActions = false;
-
- if (mDecor == null || !mShowing) {
- Log.d(TAG, "not showing, ignore");
- return;
- }
-
- mWindowManager.removeView(mDecor);
- mDecor = null;
- mWindow.closeAllPanels();
- mShowing = false;
-
- // Notify any listeners that we've been dismissed
- if (mDismissListener != null) {
- mDismissListener.onDismiss(this);
- }
- }
-
- /**
- * Returns true if this fast-track window is showing or querying.
- */
- public boolean isShowing() {
- return mShowing || mQuerying;
- }
-
- /**
- * Consider showing this window, which will only call through to
- * {@link #showInternal()} when all data items are present.
- */
- private synchronized void considerShowing() {
- if (mHasSummary && mHasSocial && mHasActions && !mShowing) {
- // Now that all queries have been finished, build summary string.
- mBuilder.clear();
- mBuilder.append(mDisplayName);
- mBuilder.append(" ");
- mBuilder.append(mSocialTitle);
- mBuilder.setSpan(mStyleBold, 0, mDisplayName.length(), 0);
- mBuilder.setSpan(mStyleBlack, 0, mDisplayName.length(), 0);
- mContent.setText(mBuilder);
-
- showInternal();
- }
- }
-
- /** {@inheritDoc} */
- public void onQueryComplete(int token, Object cookie, Cursor cursor) {
- if (cursor == null) {
- return;
- } else if (token == TOKEN_SUMMARY) {
- handleSummary(cursor);
- } else if (token == TOKEN_SOCIAL) {
- handleSocial(cursor);
- } else if (token == TOKEN_DATA) {
- handleData(cursor);
- }
- }
-
- /**
- * Handle the result from the {@link TOKEN_SUMMARY} query.
- */
- private void handleSummary(Cursor cursor) {
- final int colDisplayName = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
- final int colStatus = cursor.getColumnIndex(PresenceColumns.PRESENCE_STATUS);
-
- if (cursor.moveToNext()) {
- mDisplayName = cursor.getString(colDisplayName);
-
- int status = cursor.getInt(colStatus);
- int statusRes = Presence.getPresenceIconResourceId(status);
- mPresence.setImageResource(statusRes);
- }
-
- mHasSummary = true;
- considerShowing();
- }
-
- /**
- * Handle the result from the {@link TOKEN_SOCIAL} query.
- */
- private void handleSocial(Cursor cursor) {
- final int colTitle = cursor.getColumnIndex(Activities.TITLE);
- final int colPublished = cursor.getColumnIndex(Activities.PUBLISHED);
-
- if (cursor.moveToNext()) {
- mSocialTitle = cursor.getString(colTitle);
-
- long published = cursor.getLong(colPublished);
- CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
- System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
- mPublished.setText(relativePublished);
-
- }
-
- mHasSocial = true;
- considerShowing();
- }
-
- /**
- * Description of a specific, actionable {@link Data#_ID} item. May have a
- * {@link Mapping} associated with it to find {@link RemoteViews} or icon,
- * and may have built a summary of itself for UI display.
- */
- private class ActionInfo {
- long dataId;
- String packageName;
- String mimeType;
-
- Mapping mapping;
- CharSequence summaryValue;
- String detailValue;
-
- /**
- * Create an action from common {@link Data} elements.
- */
- public ActionInfo(long dataId, String packageName, String mimeType) {
- this.dataId = dataId;
- this.packageName = packageName;
- this.mimeType = mimeType;
- }
-
- /**
- * Attempt to find a {@link Mapping} for the package and mime-type
- * defined by this action. Returns true if one was found.
- */
- public boolean findMapping(MappingCache cache) {
- mapping = cache.findMapping(packageName, mimeType);
- return (mapping != null);
- }
-
- /**
- * Given a {@link Cursor} pointed at the {@link Data} row associated
- * with this action, use the {@link Mapping} to build a text summary.
- */
- public void buildDetails(Cursor cursor) {
- if (mapping == null) return;
-
- // Try finding common display label for this item, otherwise fall
- // back to reading from defined summary column.
- summaryValue = ContactsUtils.getDisplayLabel(mContext, mimeType, cursor);
- if (summaryValue == null && mapping.summaryColumn != null) {
- summaryValue = cursor.getString(cursor.getColumnIndex(mapping.summaryColumn));
- }
-
- // Read detailed value, if mapping was defined
- if (mapping.detailColumn != null) {
- detailValue = cursor.getString(cursor.getColumnIndex(mapping.detailColumn));
- }
- }
-
- /**
- * Build an {@link Intent} that will perform this action.
- */
- public Intent buildIntent() {
- // Handle well-known mime-types with special care
- if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
- Uri callUri = Uri.parse("tel:" + Uri.encode(detailValue));
- return new Intent(Intent.ACTION_DIAL, callUri);
-
- } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
- Uri smsUri = Uri.fromParts("smsto", detailValue, null);
- return new Intent(Intent.ACTION_SENDTO, smsUri);
-
- } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
- Uri mailUri = Uri.fromParts("mailto", detailValue, null);
- return new Intent(Intent.ACTION_SENDTO, mailUri);
-
- }
-
- // Otherwise fall back to default VIEW action
- Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
-
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setData(dataUri);
-
- return intent;
- }
- }
-
- /**
- * Provide a strongly-typed {@link LinkedList} that holds a list of
- * {@link ActionInfo} objects.
- */
- private class ActionList extends LinkedList<ActionInfo> {
- }
-
- /**
- * Provide a simple way of collecting one or more {@link ActionInfo} objects
- * under a mime-type key.
- */
- private class ActionMap extends HashMap<String, ActionList> {
- private void collect(String mimeType, ActionInfo info) {
- // Create list for this mimetype when needed
- ActionList collectList = get(mimeType);
- if (collectList == null) {
- collectList = new ActionList();
- put(mimeType, collectList);
- }
- collectList.add(info);
- }
- }
-
- /**
- * Handle the result from the {@link TOKEN_DATA} query.
- */
- private void handleData(Cursor cursor) {
- final int colId = cursor.getColumnIndex(Data._ID);
- final int colPackage = cursor.getColumnIndex(Data.RES_PACKAGE);
- final int colMimeType = cursor.getColumnIndex(Data.MIMETYPE);
- final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
-
- ActionInfo info;
-
- // Add the profile shortcut action if requested
- if (INCLUDE_PROFILE_ACTION) {
- final String mimeType = Contacts.CONTENT_ITEM_TYPE;
- info = new ActionInfo(mAggId, CommonDataKinds.PACKAGE_COMMON, mimeType);
- if (info.findMapping(mMappingCache)) {
- mActions.collect(mimeType, info);
- }
- }
-
- while (cursor.moveToNext()) {
- final long dataId = cursor.getLong(colId);
- final String packageName = cursor.getString(colPackage);
- final String mimeType = cursor.getString(colMimeType);
-
- // Handle when a photo appears in the various data items
- // TODO: accept a photo only if its marked as primary
- if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
- byte[] photoBlob = cursor.getBlob(colPhoto);
- Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
- mPhoto.setImageBitmap(photoBitmap);
- continue;
- }
-
- // 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.
- info = new ActionInfo(dataId, packageName, mimeType);
- if (info.findMapping(mMappingCache)) {
- info.buildDetails(cursor);
- mActions.collect(info.mimeType, info);
- }
-
- // If phone number, also insert as text message action
- if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) {
- info = new ActionInfo(dataId, packageName, MIME_SMS_ADDRESS);
- if (info.findMapping(mMappingCache)) {
- info.buildDetails(cursor);
- mActions.collect(info.mimeType, info);
- }
- }
- }
-
- cursor.close();
-
- // Turn our list of actions into UI elements, starting with common types
- Set<String> containedTypes = mActions.keySet();
- for (String mimeType : ORDERED_MIMETYPES) {
- if (containedTypes.contains(mimeType)) {
- mTrack.addView(inflateAction(mimeType));
- containedTypes.remove(mimeType);
- }
- }
-
- // Then continue with remaining mime-types in alphabetical order
- String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
- Arrays.sort(remainingTypes);
- for (String mimeType : remainingTypes) {
- mTrack.addView(inflateAction(mimeType));
- }
-
- mHasActions = true;
- considerShowing();
- }
-
- /**
- * Inflate the in-track view for the action of the given mime-type. Will use
- * the icon provided by the {@link Mapping}.
- */
- private View inflateAction(String mimeType) {
- ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false);
-
- // Add direct intent if single child, otherwise flag for multiple
- ActionList children = mActions.get(mimeType);
- ActionInfo firstInfo = children.get(0);
- if (children.size() == 1) {
- view.setTag(firstInfo.buildIntent());
- } else {
- view.setTag(children);
- }
-
- // Set icon and listen for clicks
- view.setImageBitmap(firstInfo.mapping.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 #showResolveList(int)}. Used to decide during a later call if
- * the arrow should be restored.
- */
- private boolean mWasDownArrow = false;
-
- /**
- * Helper for showing and hiding {@link #mResolveList}, which will correctly
- * manage {@link #mArrowDown} as needed.
- */
- private void showResolveList(int visibility) {
- // Show or hide the resolve list if needed
- if (mResolveList.getVisibility() != visibility) {
- mResolveList.setVisibility(visibility);
- }
-
- if (visibility == View.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 v) {
- final Object tag = v.getTag();
- if (tag instanceof Intent) {
- // Hide the resolution list, if present
- showResolveList(View.GONE);
-
- // Incoming tag is concrete intent, so launch
- try {
- mContext.startActivity((Intent)tag);
- } catch (ActivityNotFoundException e) {
- Log.w(TAG, NOT_FOUND);
- Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
- }
- } 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
- showResolveList(View.VISIBLE);
-
- 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.fasttrack_resolve_item, parent, false);
- }
-
- // Set action title based on summary value
- ActionInfo info = (ActionInfo)getItem(position);
-
- ImageView icon1 = (ImageView)convertView.findViewById(android.R.id.icon1);
- TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
- TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
-
- icon1.setImageBitmap(info.mapping.icon);
- text1.setText(info.summaryValue);
- text2.setText(info.detailValue);
-
- convertView.setTag(info.buildIntent());
- return convertView;
- }
- });
-
- // Make sure we resize to make room for ListView
- onWindowAttributesChanged(mWindow.getAttributes());
-
- }
- }
-
- /** {@inheritDoc} */
- public boolean dispatchKeyEvent(KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_DOWN
- && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
- // Back key will first dismiss any expanded resolve list, otherwise
- // it will close the entire dialog.
- if (mResolveList.getVisibility() == View.VISIBLE) {
- showResolveList(View.GONE);
- } else {
- dismiss();
- }
- return true;
- }
- return mWindow.superDispatchKeyEvent(event);
- }
-
- /** {@inheritDoc} */
- public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
- // TODO: make this window accessible
- return false;
- }
-
- /** {@inheritDoc} */
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
- dismiss();
- return true;
- }
- 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) {
- }
-}
diff --git a/src/com/android/contacts/ShowOrCreateActivity.java b/src/com/android/contacts/ShowOrCreateActivity.java
index 3be2b33..fb6f60b 100755
--- a/src/com/android/contacts/ShowOrCreateActivity.java
+++ b/src/com/android/contacts/ShowOrCreateActivity.java
@@ -17,6 +17,7 @@
package com.android.contacts;
import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+import com.android.contacts.ui.FastTrackWindow;
import android.app.Activity;
import android.app.AlertDialog;
@@ -30,7 +31,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
-import android.provider.Contacts.Intents;
+import android.provider.ContactsContract.Intents;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.PhoneLookup;
@@ -70,14 +71,6 @@
static final int AGGREGATE_ID_INDEX = 0;
- /**
- * Extra used to request a specific {@link FastTrackWindow} position.
- * Normally the fast-track window will try pointing an arrow towards this
- * location, but if the left and right edges are crossed, the arrow may be
- * hidden.
- */
- private static final String EXTRA_RECT = "target_rect";
-
static final int QUERY_TOKEN = 42;
private NotifyingAsyncQueryHandler mQueryHandler;
@@ -163,18 +156,21 @@
*/
private void showFastTrack(Uri aggUri) {
// Use our local window token for now
- Bundle extras = getIntent().getExtras();
+ final Bundle extras = getIntent().getExtras();
Rect targetRect;
- if (extras.containsKey(EXTRA_RECT)) {
- targetRect = (Rect)extras.getParcelable(EXTRA_RECT);
+ if (extras.containsKey(Intents.EXTRA_TARGET_RECT)) {
+ targetRect = (Rect)extras.getParcelable(Intents.EXTRA_TARGET_RECT);
} else {
// TODO: this default rect matches gmail messages, and should move over there
targetRect = new Rect(15, 110, 15+18, 110+18);
}
+ // Use requested display mode, defaulting to medium
+ final int mode = extras.getInt(Intents.EXTRA_MODE, Intents.MODE_MEDIUM);
+
mFastTrack = new FastTrackWindow(this, this);
- mFastTrack.show(aggUri, targetRect);
+ mFastTrack.show(aggUri, targetRect, mode);
}
/** {@inheritDoc} */
diff --git a/src/com/android/contacts/SocialStreamActivity.java b/src/com/android/contacts/SocialStreamActivity.java
index ae22adf..a3adf76 100644
--- a/src/com/android/contacts/SocialStreamActivity.java
+++ b/src/com/android/contacts/SocialStreamActivity.java
@@ -17,6 +17,7 @@
package com.android.contacts;
import com.android.contacts.EdgeTriggerView.EdgeTriggerListener;
+import com.android.contacts.ui.FastTrackWindow;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -44,6 +45,7 @@
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Intents;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.SocialContract.Activities;
@@ -175,7 +177,7 @@
mRect.bottom = mRect.top + anchor.getHeight();
mFastTrack.dismiss();
- mFastTrack.show(aggUri, mRect);
+ mFastTrack.show(aggUri, mRect, Intents.MODE_MEDIUM);
}
/** {@inheritDoc} */
diff --git a/src/com/android/contacts/TypePrecedence.java b/src/com/android/contacts/TypePrecedence.java
index d348fef..5b51ba6 100644
--- a/src/com/android/contacts/TypePrecedence.java
+++ b/src/com/android/contacts/TypePrecedence.java
@@ -16,6 +16,8 @@
package com.android.contacts;
+import com.android.contacts.ui.FastTrackWindow;
+
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Organization;
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index f678632..d00df4a 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -32,6 +32,7 @@
import com.android.contacts.Collapser.Collapsible;
import com.android.contacts.SplitAggregateView.OnContactSelectedListener;
+import com.android.contacts.ui.FastTrackWindow;
import com.android.internal.telephony.ITelephony;
import android.app.AlertDialog;
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index b778f2a..d975d64 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -16,7 +16,9 @@
package com.android.contacts.model;
+import android.accounts.Account;
import android.content.ContentValues;
+import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
@@ -64,9 +66,9 @@
*/
/**
- * Internal structure that represents constraints for a specific data source,
- * such as the various data types they support, including details on how those
- * types should be rendered and edited.
+ * Internal structure that represents constraints and styles for a specific data
+ * source, such as the various data types they support, including details on how
+ * those types should be rendered and edited.
* <p>
* In the future this may be inflated from XML defined by a data source.
*/
@@ -77,6 +79,12 @@
public String accountType;
/**
+ * Package that resources should be loaded from, either defined through an
+ * {@link Account} or for matching against {@link Data#RES_PACKAGE}.
+ */
+ public String resPackageName;
+
+ /**
* Set of {@link DataKind} supported by this source.
*/
private ArrayList<DataKind> mKinds = new ArrayList<DataKind>();
@@ -163,6 +171,7 @@
public static class EditType {
public int rawValue;
public int labelRes;
+ public int actionRes;
public boolean secondary;
public int specificMax;
public String customColumn;
@@ -170,6 +179,7 @@
public EditType(int rawValue, int labelRes) {
this.rawValue = rawValue;
this.labelRes = labelRes;
+ this.actionRes = actionRes;
this.specificMax = -1;
}
@@ -238,7 +248,7 @@
* before presenting to the user.
*/
public interface StringInflater {
- public CharSequence inflateUsing(Cursor cursor);
+ public CharSequence inflateUsing(Context context, Cursor cursor);
}
}
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index f0d0017..de925cb 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -21,6 +21,8 @@
import com.android.contacts.model.EntityDelta.ValuesDelta;
import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Intents;
@@ -183,6 +185,16 @@
}
/**
+ * Find the {@link EditType} that describes the given {@link Cursor} row,
+ * assuming the given {@link DataKind} dictates the possible types.
+ */
+ public static EditType getCurrentType(Cursor cursor, DataKind kind) {
+ final int index = cursor.getColumnIndex(kind.typeColumn);
+ final int rawValue = cursor.getInt(index);
+ return getType(kind, rawValue);
+ }
+
+ /**
* Find the {@link EditType} with the given {@link EditType#rawValue}.
*/
public static EditType getType(DataKind kind, int rawValue) {
@@ -285,9 +297,9 @@
* Parse the given {@link Bundle} into the given {@link EntityDelta} state,
* assuming the extras defined through {@link Intents}.
*/
- public static void parseExtras(EntityDelta state, Bundle extras) {
+ public static void parseExtras(Context context, EntityDelta state, Bundle extras) {
final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
- final ContactsSource source = Sources.getInstance().getSourceForType(accountType);
+ final ContactsSource source = Sources.getInstance(context).getSourceForType(accountType);
{
// StructuredName
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index f0a2840..303d15d 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -19,6 +19,7 @@
import com.android.contacts.R;
import android.content.ContentValues;
+import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.provider.ContactsContract.Contacts;
@@ -55,9 +56,9 @@
private static Sources sInstance;
- public static synchronized Sources getInstance() {
+ public static synchronized Sources getInstance(Context context) {
if (sInstance == null) {
- sInstance = new Sources();
+ sInstance = new Sources(context);
}
return sInstance;
}
@@ -67,9 +68,9 @@
private HashMap<String, ContactsSource> mSources = new HashMap<String, ContactsSource>();
- private Sources() {
- mSources.put(ACCOUNT_TYPE_GOOGLE, buildGoogle());
- mSources.put(ACCOUNT_TYPE_EXCHANGE, buildExchange());
+ private Sources(Context context) {
+ mSources.put(ACCOUNT_TYPE_GOOGLE, buildGoogle(context));
+ mSources.put(ACCOUNT_TYPE_EXCHANGE, buildExchange(context));
}
/**
@@ -100,8 +101,10 @@
/**
* Hard-coded instance of {@link ContactsSource} for Google Contacts.
*/
- private ContactsSource buildGoogle() {
+ private ContactsSource buildGoogle(Context context) {
final ContactsSource list = new ContactsSource();
+ list.accountType = ACCOUNT_TYPE_GOOGLE;
+ list.resPackageName = context.getPackageName();
{
// GOOGLE: STRUCTUREDNAME
@@ -121,8 +124,8 @@
DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
- kind.actionHeader = new ActionLabelInflater(R.string.actionCall, kind);
- kind.actionBody = new ColumnInflater(Phone.NUMBER);
+ kind.actionHeader = new ActionLabelInflater(list.resPackageName, kind);
+ kind.actionBody = new SimpleInflater(Phone.NUMBER);
kind.typeColumn = Phone.TYPE;
kind.typeList = new ArrayList<EditType>();
@@ -147,8 +150,8 @@
DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
- kind.actionHeader = new ActionLabelInflater(R.string.actionEmail, kind);
- kind.actionBody = new ColumnInflater(Email.DATA);
+ kind.actionHeader = new ActionLabelInflater(list.resPackageName, kind);
+ kind.actionBody = new SimpleInflater(Email.DATA);
kind.typeColumn = Email.TYPE;
kind.typeList = new ArrayList<EditType>();
@@ -169,8 +172,8 @@
DataKind kind = new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup,
android.R.drawable.sym_action_chat, 20, true);
- kind.actionHeader = new ActionLabelInflater(R.string.actionChat, kind);
- kind.actionBody = new ColumnInflater(Im.DATA);
+ kind.actionHeader = new ActionLabelInflater(list.resPackageName, kind);
+ kind.actionBody = new SimpleInflater(Im.DATA);
// NOTE: even though a traditional "type" exists, for editing
// purposes we're using the network to pick labels
@@ -202,9 +205,9 @@
DataKind kind = new DataKind(StructuredPostal.CONTENT_ITEM_TYPE,
R.string.postalLabelsGroup, R.drawable.sym_action_map, 25, true);
- kind.actionHeader = new ActionLabelInflater(R.string.actionMap, kind);
+ kind.actionHeader = new ActionLabelInflater(list.resPackageName, kind);
// TODO: build body from various structured fields
- kind.actionBody = new ColumnInflater(StructuredPostal.FORMATTED_ADDRESS);
+ kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
kind.typeColumn = StructuredPostal.TYPE;
kind.typeList = new ArrayList<EditType>();
@@ -234,9 +237,9 @@
DataKind kind = new DataKind(Organization.CONTENT_ITEM_TYPE,
R.string.organizationLabelsGroup, R.drawable.sym_action_organization, 30, true);
- kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup);
+ kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.organizationLabelsGroup);
// TODO: build body from multiple fields
- kind.actionBody = new ColumnInflater(Organization.TITLE);
+ kind.actionBody = new SimpleInflater(Organization.TITLE);
kind.typeColumn = Organization.TYPE;
kind.typeList = new ArrayList<EditType>();
@@ -260,8 +263,8 @@
R.string.label_notes, R.drawable.sym_note, 110, true);
kind.secondary = true;
- kind.actionHeader = new SimpleInflater(R.string.label_notes);
- kind.actionBody = new ColumnInflater(Note.NOTE);
+ kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.label_notes);
+ kind.actionBody = new SimpleInflater(Note.NOTE);
kind.fieldList = new ArrayList<EditField>();
kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
@@ -275,8 +278,8 @@
R.string.nicknameLabelsGroup, -1, 115, true);
kind.secondary = true;
- kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup);
- kind.actionBody = new ColumnInflater(Nickname.NAME);
+ kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.nicknameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
kind.fieldList = new ArrayList<EditField>();
kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
@@ -313,8 +316,10 @@
/**
* Hard-coded instance of {@link ContactsSource} for Exchange.
*/
- private ContactsSource buildExchange() {
+ private ContactsSource buildExchange(Context context) {
final ContactsSource list = new ContactsSource();
+ list.accountType = ACCOUNT_TYPE_EXCHANGE;
+ list.resPackageName = context.getPackageName();
{
// EXCHANGE: STRUCTUREDNAME
@@ -336,8 +341,8 @@
DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
- kind.actionHeader = new ActionLabelInflater(R.string.actionCall, kind);
- kind.actionBody = new ColumnInflater(Phone.NUMBER);
+ kind.actionHeader = new ActionLabelInflater(list.resPackageName, kind);
+ kind.actionBody = new SimpleInflater(Phone.NUMBER);
kind.typeColumn = Phone.TYPE;
kind.typeList = new ArrayList<EditType>();
@@ -367,8 +372,8 @@
DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
- kind.actionHeader = new ActionLabelInflater(R.string.actionEmail, kind);
- kind.actionBody = new ColumnInflater(Email.DATA);
+ kind.actionHeader = new ActionLabelInflater(list.resPackageName, kind);
+ kind.actionBody = new SimpleInflater(Email.DATA);
kind.typeColumn = Email.TYPE;
kind.typeList = new ArrayList<EditType>();
@@ -387,8 +392,8 @@
DataKind kind = new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup,
android.R.drawable.sym_action_chat, 20, true);
- kind.actionHeader = new ActionLabelInflater(R.string.actionChat, kind);
- kind.actionBody = new ColumnInflater(Im.DATA);
+ kind.actionHeader = new ActionLabelInflater(list.resPackageName, kind);
+ kind.actionBody = new SimpleInflater(Im.DATA);
kind.typeColumn = Im.TYPE;
kind.typeList = new ArrayList<EditType>();
@@ -409,8 +414,8 @@
kind.secondary = true;
kind.typeOverallMax = 1;
- kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup);
- kind.actionBody = new ColumnInflater(Nickname.NAME);
+ kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.nicknameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
kind.fieldList = new ArrayList<EditField>();
kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
@@ -426,8 +431,8 @@
kind.secondary = true;
kind.typeOverallMax = 1;
- kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup);
- kind.actionBody = new ColumnInflater(Website.URL);
+ kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.websiteLabelsGroup);
+ kind.actionBody = new SimpleInflater(Website.URL);
kind.fieldList = new ArrayList<EditField>();
kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
@@ -443,16 +448,42 @@
* filled from the given column.
*/
public static class SimpleInflater implements StringInflater {
- // TODO: implement this
+ private final String mPackageName;
+ private final int mStringRes;
+ private final String mColumnName;
- public SimpleInflater(int stringRes) {
+ public SimpleInflater(String packageName, int stringRes) {
+ this(packageName, stringRes, null);
}
- public SimpleInflater(int stringRes, String columnName) {
+ public SimpleInflater(String columnName) {
+ this(null, -1, columnName);
}
- public CharSequence inflateUsing(Cursor cursor) {
- return null;
+ public SimpleInflater(String packageName, int stringRes, String columnName) {
+ mPackageName = packageName;
+ mStringRes = stringRes;
+ mColumnName = columnName;
+ }
+
+ public CharSequence inflateUsing(Context context, Cursor cursor) {
+ final int index = mColumnName != null ? cursor.getColumnIndex(mColumnName) : -1;
+ final boolean validString = mStringRes > 0;
+ final boolean validColumn = index != -1;
+
+ final CharSequence stringValue = validString ? context.getPackageManager().getText(
+ mPackageName, mStringRes, null) : null;
+ final CharSequence columnValue = validColumn ? cursor.getString(index) : null;
+
+ if (validString && validColumn) {
+ return String.format(stringValue.toString(), columnValue);
+ } else if (validString) {
+ return stringValue;
+ } else if (validColumn) {
+ return columnValue;
+ } else {
+ return null;
+ }
}
}
@@ -462,30 +493,19 @@
* {@link EditType#labelRes}.
*/
public static class ActionLabelInflater implements StringInflater {
- // TODO: implement this
+ private String mPackageName;
+ private DataKind mKind;
- public ActionLabelInflater(int actionRes, DataKind labelProvider) {
+ public ActionLabelInflater(String packageName, DataKind labelProvider) {
+ mPackageName = packageName;
+ mKind = labelProvider;
}
- public CharSequence inflateUsing(Cursor cursor) {
- // use the given action string along with localized label name
- return null;
+ public CharSequence inflateUsing(Context context, Cursor cursor) {
+ final EditType type = EntityModifier.getCurrentType(cursor, mKind);
+ final boolean validString = type.actionRes > 0;
+ return validString ? context.getPackageManager().getText(mPackageName, type.actionRes,
+ null) : null;
}
}
-
- /**
- * Simple inflater that uses the raw value from the given column.
- */
- public static class ColumnInflater implements StringInflater {
- // TODO: implement this
-
- public ColumnInflater(String columnName) {
- }
-
- public CharSequence inflateUsing(Cursor cursor) {
- // return the cursor value for column name
- return null;
- }
- }
-
}
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index f173172..d56e771 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -99,7 +99,7 @@
final Bundle extras = intent.getExtras();
mUri = intent.getData();
- mSources = Sources.getInstance();
+ mSources = Sources.getInstance(this);
if (Intent.ACTION_EDIT.equals(action) && icicle == null) {
// Read initial state from database
diff --git a/src/com/android/contacts/ui/FastTrackWindow.java b/src/com/android/contacts/ui/FastTrackWindow.java
new file mode 100644
index 0000000..9862147
--- /dev/null
+++ b/src/com/android/contacts/ui/FastTrackWindow.java
@@ -0,0 +1,1002 @@
+/*
+ *
+ * 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.NotifyingAsyncQueryHandler;
+import com.android.contacts.R;
+import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.internal.policy.PolicyManager;
+
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.SocialContract;
+import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.Im.PresenceColumns;
+import android.provider.SocialContract.Activities;
+import android.text.SpannableStringBuilder;
+import android.text.format.DateUtils;
+import android.text.style.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+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.View.OnClickListener;
+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.HorizontalScrollView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Window that shows fast-track contact details for a specific
+ * {@link Contacts#_ID}.
+ */
+public class FastTrackWindow implements Window.Callback, QueryCompleteListener, OnClickListener,
+ AbsListView.OnItemClickListener {
+ private static final String TAG = "FastTrackWindow";
+
+ /**
+ * Interface used to allow the person showing a {@link FastTrackWindow} to
+ * know when the window has been dismissed.
+ */
+ public interface OnDismissListener {
+ public void onDismiss(FastTrackWindow dialog);
+ }
+
+ private final Context mContext;
+ private final PackageManager mPackageManager;
+ private final LayoutInflater mInflater;
+ private final WindowManager mWindowManager;
+ private Window mWindow;
+ private View mDecor;
+
+ private boolean mQuerying = false;
+ private boolean mShowing = false;
+
+ private NotifyingAsyncQueryHandler mHandler;
+ private OnDismissListener mDismissListener;
+
+ private long mAggId;
+ private Rect mAnchor;
+
+ private boolean mHasSummary = false;
+ private boolean mHasSocial = false;
+ private boolean mHasActions = false;
+
+ private ImageView mArrowUp;
+ private ImageView mArrowDown;
+
+ private View mHeader;
+ private HorizontalScrollView mTrackScroll;
+ private ViewGroup mTrack;
+ private Animation mTrackAnim;
+ private ListView mResolveList;
+
+ /**
+ * Set of {@link Action} that are associated with the aggregate currently
+ * displayed by this fast-track window, represented as a map from
+ * {@link String} MIME-type to {@link ActionList}.
+ */
+ private ActionMap mActions = new ActionMap();
+
+ /**
+ * Specific MIME-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
+ * distinguishes actions that should initiate a text message.
+ */
+ // TODO: We should move this to someplace more general as it is needed in a
+ // few places in the app code.
+ public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
+
+ private static final String SCHEME_TEL = "tel";
+ private static final String SCHEME_SMSTO = "smsto";
+ private static final String SCHEME_MAILTO = "mailto";
+
+ /**
+ * Specific mime-types that should be bumped to the front of the fast-track.
+ * Other mime-types not appearing in this list follow in alphabetic order.
+ */
+ private static final String[] ORDERED_MIMETYPES = new String[] {
+ Phones.CONTENT_ITEM_TYPE,
+ Contacts.CONTENT_ITEM_TYPE,
+ MIME_SMS_ADDRESS,
+ Email.CONTENT_ITEM_TYPE,
+ };
+
+ private static final int TOKEN_SUMMARY = 1;
+ private static final int TOKEN_SOCIAL = 2;
+ private static final int TOKEN_DATA = 3;
+
+ /**
+ * Prepare a fast-track window to show in the given {@link Context}.
+ */
+ public FastTrackWindow(Context context) {
+ mContext = new ContextThemeWrapper(context, R.style.FastTrack);
+ mPackageManager = context.getPackageManager();
+ 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.setContentView(R.layout.fasttrack);
+
+ mArrowUp = (ImageView)mWindow.findViewById(R.id.arrow_up);
+ mArrowDown = (ImageView)mWindow.findViewById(R.id.arrow_down);
+
+ mTrack = (ViewGroup)mWindow.findViewById(R.id.fasttrack);
+ mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
+ mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
+
+ // Prepare track entrance animation
+ mTrackAnim = AnimationUtils.loadAnimation(mContext, R.anim.fasttrack);
+ 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;
+ }
+ });
+ }
+
+ /**
+ * Prepare a fast-track window to show in the given {@link Context}, and
+ * notify the given {@link OnDismissListener} each time this dialog is
+ * dismissed.
+ */
+ public FastTrackWindow(Context context, OnDismissListener dismissListener) {
+ this(context);
+ mDismissListener = dismissListener;
+ }
+
+ private View getHeaderView(int mode) {
+ View header = null;
+ switch (mode) {
+ case Intents.MODE_SMALL:
+ header = mWindow.findViewById(R.id.header_small);
+ break;
+ case Intents.MODE_MEDIUM:
+ header = mWindow.findViewById(R.id.header_medium);
+ break;
+ case Intents.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 {
+ header.setVisibility(View.VISIBLE);
+ }
+
+ return header;
+ }
+
+ /**
+ * Start showing a fast-track window for the given {@link Contacts#_ID}
+ * pointing towards the given location.
+ */
+ public void show(Uri aggUri, Rect anchor, int mode) {
+ if (mShowing || mQuerying) {
+ Log.w(TAG, "already in process of showing");
+ return;
+ }
+
+ // Prepare header view for requested mode
+ mHeader = getHeaderView(mode);
+
+ setHeaderText(R.id.name, R.string.fasttrack_missing_name);
+ setHeaderText(R.id.status, R.string.fasttrack_missing_status);
+ setHeaderText(R.id.published, null);
+ setHeaderImage(R.id.presence, null);
+
+ mAggId = ContentUris.parseId(aggUri);
+ mAnchor = new Rect(anchor);
+ mQuerying = true;
+
+ Uri aggSummary = ContentUris.withAppendedId(
+ ContactsContract.Contacts.CONTENT_SUMMARY_URI, mAggId);
+ Uri aggSocial = ContentUris.withAppendedId(
+ SocialContract.Activities.CONTENT_CONTACT_STATUS_URI, mAggId);
+ Uri aggData = Uri.withAppendedPath(aggUri,
+ ContactsContract.Contacts.Data.CONTENT_DIRECTORY);
+
+ // Start data query in background
+ mHandler = new NotifyingAsyncQueryHandler(mContext, this);
+ mHandler.startQuery(TOKEN_SUMMARY, null, aggSummary, null, null, null, null);
+ mHandler.startQuery(TOKEN_SOCIAL, null, aggSocial, null, null, null, null);
+ mHandler.startQuery(TOKEN_DATA, null, aggData, null, null, null, null);
+ }
+
+ /**
+ * 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 fast-track window. Called only by
+ * {@link #considerShowing()} when all data requirements have been met.
+ */
+ private void showInternal() {
+ mDecor = mWindow.getDecorView();
+ WindowManager.LayoutParams l = mWindow.getAttributes();
+
+ l.width = WindowManager.LayoutParams.FILL_PARENT;
+ 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();
+ final int arrowHeight = mArrowUp.getDrawable().getIntrinsicHeight();
+
+ l.gravity = Gravity.TOP | Gravity.LEFT;
+ l.x = 0;
+
+ 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 + arrowHeight;
+
+ } 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 - arrowHeight;
+
+ }
+
+ l.dimAmount = 0.0f;
+ l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND
+ | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+ | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+
+ mWindowManager.addView(mDecor, l);
+ mShowing = true;
+ mQuerying = false;
+
+ mTrack.startAnimation(mTrackAnim);
+ }
+
+ /**
+ * Dismiss this fast-track window if showing.
+ */
+ public void dismiss() {
+ if (!isShowing()) {
+ Log.d(TAG, "not visible, ignore");
+ return;
+ }
+
+ // Completely hide header from current mode
+ mHeader.setVisibility(View.GONE);
+
+ // Cancel any pending queries
+ mHandler.cancelOperation(TOKEN_SUMMARY);
+ mHandler.cancelOperation(TOKEN_SOCIAL);
+ mHandler.cancelOperation(TOKEN_DATA);
+
+ // Clear track actions and scroll to hard left
+ mActions.clear();
+ mTrack.removeViews(1, mTrack.getChildCount() - 2);
+ mTrackScroll.fullScroll(View.FOCUS_LEFT);
+ mWasDownArrow = false;
+
+ showResolveList(View.GONE);
+
+ mQuerying = false;
+ mHasSummary = false;
+ mHasSocial = false;
+ mHasActions = false;
+
+ if (mDecor == null || !mShowing) {
+ Log.d(TAG, "not showing, ignore");
+ return;
+ }
+
+ mWindowManager.removeView(mDecor);
+ mDecor = null;
+ mWindow.closeAllPanels();
+ mShowing = false;
+
+ // Notify any listeners that we've been dismissed
+ if (mDismissListener != null) {
+ mDismissListener.onDismiss(this);
+ }
+ }
+
+ /**
+ * Returns true if this fast-track window is showing or querying.
+ */
+ public boolean isShowing() {
+ return mShowing || mQuerying;
+ }
+
+ /**
+ * Consider showing this window, which will only call through to
+ * {@link #showInternal()} when all data items are present.
+ */
+ private synchronized void considerShowing() {
+ if (mHasSummary && mHasSocial && mHasActions && !mShowing) {
+ showInternal();
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (cursor == null) {
+ // Problem while running query, so bail without showing
+ Log.w(TAG, "Missing cursor for token=" + token);
+ this.dismiss();
+ return;
+ }
+
+ switch (token) {
+ case TOKEN_SUMMARY:
+ handleSummary(cursor);
+ mHasSummary = true;
+ break;
+ case TOKEN_SOCIAL:
+ handleSocial(cursor);
+ mHasSocial = true;
+ break;
+ case TOKEN_DATA:
+ handleData(cursor);
+ mHasActions = true;
+ break;
+ }
+
+ 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);
+ }
+ }
+
+ /** 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) {
+ ((ImageView)view).setImageDrawable(drawable);
+ }
+ }
+
+ /**
+ * Handle the result from the {@link #TOKEN_SUMMARY} query.
+ */
+ private void handleSummary(Cursor cursor) {
+ if (cursor == null || !cursor.moveToNext()) return;
+
+ final String name = getAsString(cursor, Contacts.DISPLAY_NAME);
+ final int status = getAsInteger(cursor, Contacts.PRESENCE_STATUS);
+ final int statusIcon = Presence.getPresenceIconResourceId(status);
+
+ setHeaderText(R.id.name, name);
+ setHeaderImage(R.id.presence, statusIcon);
+ }
+
+ /**
+ * Handle the result from the {@link #TOKEN_SOCIAL} query.
+ */
+ private void handleSocial(Cursor cursor) {
+ if (cursor == null || !cursor.moveToNext()) return;
+
+ final String status = getAsString(cursor, Activities.TITLE);
+ final long published = getAsLong(cursor, Activities.PUBLISHED);
+ final CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
+ System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
+
+ setHeaderText(R.id.status, status);
+ setHeaderText(R.id.published, relativePublished);
+ }
+
+ /**
+ * Find the Fast-Track-specific presence icon for showing in chiclets.
+ */
+ private Drawable getTrackPresenceIcon(int status) {
+ int resId = -1;
+ switch (status) {
+ case Presence.AVAILABLE:
+ resId = R.drawable.fasttrack_slider_presence_active;
+ break;
+ case Presence.IDLE:
+ case Presence.AWAY:
+ resId = R.drawable.fasttrack_slider_presence_away;
+ break;
+ case Presence.DO_NOT_DISTURB:
+ resId = R.drawable.fasttrack_slider_presence_busy;
+ break;
+ case Presence.INVISIBLE:
+ resId = R.drawable.fasttrack_slider_presence_inactive;
+ break;
+ case Presence.OFFLINE:
+ default:
+ resId = R.drawable.fasttrack_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 int from the given {@link Cursor}. */
+ private static int getAsInteger(Cursor cursor, String columnName) {
+ final int index = cursor.getColumnIndex(columnName);
+ return cursor.getInt(index);
+ }
+
+ /** Read long from the given {@link Cursor}. */
+ private static long getAsLong(Cursor cursor, String columnName) {
+ final int index = cursor.getColumnIndex(columnName);
+ return cursor.getLong(index);
+ }
+
+ /**
+ * Abstract definition of an action that could be performed, along with
+ * string description and icon.
+ */
+ private interface Action {
+ public CharSequence getHeader();
+ public CharSequence getBody();
+ public Drawable getIcon();
+
+ /**
+ * Build an {@link Intent} that will perform this action.
+ */
+ public Intent getIntent();
+ }
+
+ /**
+ * 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 ContactsSource mSource;
+ private final DataKind mKind;
+
+ private CharSequence mHeader;
+ private CharSequence mBody;
+ private Intent mIntent;
+
+ /**
+ * Create an action from common {@link Data} elements.
+ */
+ public DataAction(Context context, ContactsSource source, DataKind kind, Cursor cursor) {
+ mContext = context;
+ mSource = source;
+ mKind = kind;
+
+ // Inflate strings from cursor
+ if (mKind.actionHeader != null) {
+ mHeader = mKind.actionHeader.inflateUsing(context, cursor);
+ }
+ if (mKind.actionBody != null) {
+ mBody = mKind.actionBody.inflateUsing(context, cursor);
+ }
+
+ // Handle well-known MIME-types with special care
+ final String mimeType = mKind.mimeType;
+ if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final String number = getAsString(cursor, Phone.NUMBER);
+ final Uri callUri = Uri.fromParts(SCHEME_TEL, number, null);
+ mIntent = new Intent(Intent.ACTION_DIAL, callUri);
+
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final String address = getAsString(cursor, Email.DATA);
+ final Uri mailUri = Uri.fromParts(SCHEME_MAILTO, address, null);
+ mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
+
+ } else {
+ // Otherwise fall back to default VIEW action
+ final long dataId = getAsLong(cursor, Data._ID);
+ final Uri dataUri = ContentUris.withAppendedId(Data.CONTENT_URI, dataId);
+ mIntent = new Intent(Intent.ACTION_VIEW, dataUri);
+ }
+ }
+
+ /** {@inheritDoc} */
+ public CharSequence getHeader() {
+ return mHeader;
+ }
+
+ /** {@inheritDoc} */
+ public CharSequence getBody() {
+ return mBody;
+ }
+
+ /** {@inheritDoc} */
+ public Drawable getIcon() {
+ Drawable icon = null;
+ if (mSource.resPackageName == null || mKind.iconRes != -1) {
+ icon = mContext.getPackageManager().getDrawable(mSource.resPackageName,
+ mKind.iconRes, null);
+ }
+ return icon;
+ }
+
+ /** {@inheritDoc} */
+ public Intent getIntent() {
+ return mIntent;
+ }
+ }
+
+ private static class SmsAction implements Action {
+ private final Context mContext;
+ private final Intent mIntent;
+
+ public SmsAction(Context context, Cursor cursor) {
+ mContext = context;
+
+ final String number = getAsString(cursor, Phone.NUMBER);
+ final Uri smsUri = Uri.fromParts(SCHEME_SMSTO, number, null);
+ mIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
+ }
+
+ /** {@inheritDoc} */
+ public CharSequence getHeader() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public CharSequence getBody() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public Drawable getIcon() {
+ return mContext.getResources().getDrawable(R.drawable.sym_action_sms);
+ }
+
+ /** {@inheritDoc} */
+ public Intent getIntent() {
+ return mIntent;
+ }
+ }
+
+ private static class ProfileAction implements Action {
+ private final Context mContext;
+ private final long mId;
+
+ public ProfileAction(Context context, long contactId) {
+ mContext = context;
+ mId = contactId;
+ }
+
+ /** {@inheritDoc} */
+ public CharSequence getHeader() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public CharSequence getBody() {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ public Drawable getIcon() {
+ return mContext.getResources().getDrawable(R.drawable.ic_contacts_details);
+ }
+
+ /** {@inheritDoc} */
+ public Intent getIntent() {
+ final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, mId);
+ return new Intent(Intent.ACTION_VIEW, contactUri);
+ }
+ }
+
+ /**
+ * Provide a strongly-typed {@link LinkedList} that holds a list of
+ * {@link Action} objects.
+ */
+ private class ActionList extends LinkedList<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);
+ }
+ }
+
+ /**
+ * Handle the result from the {@link #TOKEN_DATA} query.
+ */
+ private void handleData(Cursor cursor) {
+ if (cursor == null) return;
+
+ final ContactsSource defaultSource = Sources.getInstance(mContext).getSourceForType(
+ Sources.ACCOUNT_TYPE_GOOGLE);
+
+ {
+ // Add the profile shortcut action
+ final Action action = new ProfileAction(mContext, mAggId);
+ mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
+ }
+
+ final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
+
+ while (cursor.moveToNext()) {
+ final String accountType = getAsString(cursor, RawContacts.ACCOUNT_TYPE);
+ final String resPackage = getAsString(cursor, Data.RES_PACKAGE);
+ final String mimeType = getAsString(cursor, Data.MIMETYPE);
+
+ // Handle when a photo appears in the various data items
+ // TODO: accept a photo only if its marked as primary
+ // TODO: move to using photo thumbnail columns, when they exist
+ if (photoView != null && Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
+ final byte[] photoBlob = cursor.getBlob(colPhoto);
+ final Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
+ photoView.setImageBitmap(photoBitmap);
+ continue;
+ }
+
+ // TODO: find the ContactsSource for this, either from accountType,
+ // or through lazy-loading when resPackage is set, or default.
+ final ContactsSource source = defaultSource;
+ final DataKind kind = source.getKindForMimetype(mimeType);
+
+ 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, source, kind, cursor);
+ considerAdd(action, mimeType);
+ }
+
+ // If phone number, also insert as text message action
+ if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ final Action action = new SmsAction(mContext, cursor);
+ considerAdd(action, MIME_SMS_ADDRESS);
+ }
+ }
+
+ // Turn our list of actions into UI elements, starting with common types
+ final Set<String> containedTypes = mActions.keySet();
+ for (String mimeType : ORDERED_MIMETYPES) {
+ if (containedTypes.contains(mimeType)) {
+ final int index = mTrack.getChildCount() - 1;
+ mTrack.addView(inflateAction(mimeType), index);
+ containedTypes.remove(mimeType);
+ }
+ }
+
+ // Then continue with remaining MIME-types in alphabetical order
+ 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);
+ }
+ }
+
+ /**
+ * 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) {
+ final Intent intent = action.getIntent();
+ final boolean intentHandled = mPackageManager.queryIntentActivities(intent, 0).size() > 0;
+ if (intentHandled) {
+ mActions.collect(mimeType, action);
+ }
+ }
+
+ /**
+ * Inflate the in-track view for the action of the given MIME-type. Will use
+ * the icon provided by the {@link DataKind}.
+ */
+ private View inflateAction(String mimeType) {
+ ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false);
+
+ // Add direct intent if single child, otherwise flag for multiple
+ ActionList children = mActions.get(mimeType);
+ Action firstInfo = children.get(0);
+ if (children.size() == 1) {
+ view.setTag(firstInfo.getIntent());
+ } else {
+ view.setTag(children);
+ }
+
+ // Set icon and listen for clicks
+ view.setImageDrawable(firstInfo.getIcon());
+ 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 #showResolveList(int)}. Used to decide during a later call if
+ * the arrow should be restored.
+ */
+ private boolean mWasDownArrow = false;
+
+ /**
+ * Helper for showing and hiding {@link #mResolveList}, which will correctly
+ * manage {@link #mArrowDown} as needed.
+ */
+ private void showResolveList(int visibility) {
+ // Show or hide the resolve list if needed
+ if (mResolveList.getVisibility() != visibility) {
+ mResolveList.setVisibility(visibility);
+ }
+
+ if (visibility == View.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 v) {
+ final Object tag = v.getTag();
+ if (tag instanceof Intent) {
+ // Hide the resolution list, if present
+ showResolveList(View.GONE);
+
+ // Dismiss track entirely if switching to dialer
+ final Intent intent = (Intent)tag;
+ final String action = intent.getAction();
+ if (Intent.ACTION_DIAL.equals(action)) {
+ this.dismiss();
+ }
+
+ try {
+ // Incoming tag is concrete intent, so try launching
+ mContext.startActivity((Intent)tag);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(mContext, R.string.fasttrack_missing_app, Toast.LENGTH_SHORT).show();
+ }
+ } 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
+ showResolveList(View.VISIBLE);
+
+ 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.fasttrack_resolve_item, parent, false);
+ }
+
+ // Set action title based on summary value
+ final Action action = (Action)getItem(position);
+
+ ImageView icon1 = (ImageView)convertView.findViewById(android.R.id.icon1);
+ TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+ TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+
+ icon1.setImageDrawable(action.getIcon());
+ text1.setText(action.getHeader());
+ text2.setText(action.getBody());
+
+ convertView.setTag(action.getIntent());
+ return convertView;
+ }
+ });
+
+ // Make sure we resize to make room for ListView
+ onWindowAttributesChanged(mWindow.getAttributes());
+
+ }
+ }
+
+ /** {@inheritDoc} */
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ // Back key will first dismiss any expanded resolve list, otherwise
+ // it will close the entire dialog.
+ if (mResolveList.getVisibility() == View.VISIBLE) {
+ showResolveList(View.GONE);
+ } else {
+ dismiss();
+ }
+ return true;
+ }
+ return mWindow.superDispatchKeyEvent(event);
+ }
+
+ /** {@inheritDoc} */
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ // TODO: make this window accessible
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
+ dismiss();
+ return true;
+ }
+ 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) {
+ }
+}