blob: a97c7f76c50578ee842c2bc81af8fe1d6512cecf [file] [log] [blame]
Jeff Sharkey3f177592009-05-18 15:23:12 -07001/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.contacts;
18
19import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
Jeff Sharkey3f177592009-05-18 15:23:12 -070020import com.android.contacts.SocialStreamActivity.MappingCache;
Jeff Sharkey549aa162009-05-21 01:33:30 -070021import com.android.contacts.SocialStreamActivity.Mapping;
22import com.android.internal.policy.PolicyManager;
Jeff Sharkey3f177592009-05-18 15:23:12 -070023import com.android.providers.contacts2.ContactsContract;
Jeff Sharkeyecedf752009-05-18 22:07:40 -070024import com.android.providers.contacts2.ContactsContract.Aggregates;
Jeff Sharkey3f177592009-05-18 15:23:12 -070025import com.android.providers.contacts2.ContactsContract.CommonDataKinds;
26import com.android.providers.contacts2.ContactsContract.Data;
27import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Email;
28import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Im;
29import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Phone;
Jeff Sharkeyecedf752009-05-18 22:07:40 -070030import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Photo;
Jeff Sharkey3f177592009-05-18 15:23:12 -070031import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Postal;
32
Jeff Sharkey549aa162009-05-21 01:33:30 -070033import android.app.Activity;
Jeff Sharkey3f177592009-05-18 15:23:12 -070034import android.content.ActivityNotFoundException;
35import android.content.ContentUris;
36import android.content.Context;
37import android.content.Intent;
38import android.content.res.Resources;
39import android.database.Cursor;
Jeff Sharkeyecedf752009-05-18 22:07:40 -070040import android.graphics.Bitmap;
41import android.graphics.BitmapFactory;
Jeff Sharkey80a193a2009-05-21 14:18:18 -070042import android.graphics.Color;
Jeff Sharkey3f177592009-05-18 15:23:12 -070043import android.net.Uri;
Jeff Sharkey549aa162009-05-21 01:33:30 -070044import android.provider.Contacts.Phones;
Jeff Sharkey80a193a2009-05-21 14:18:18 -070045import android.text.SpannableStringBuilder;
46import android.text.style.CharacterStyle;
47import android.text.style.ForegroundColorSpan;
48import android.text.style.StyleSpan;
Jeff Sharkey3f177592009-05-18 15:23:12 -070049import android.util.Log;
Jeff Sharkey549aa162009-05-21 01:33:30 -070050import android.view.ContextThemeWrapper;
Jeff Sharkey3f177592009-05-18 15:23:12 -070051import android.view.Gravity;
Jeff Sharkey549aa162009-05-21 01:33:30 -070052import android.view.KeyEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070053import android.view.LayoutInflater;
Jeff Sharkey549aa162009-05-21 01:33:30 -070054import android.view.Menu;
55import android.view.MenuItem;
56import android.view.MotionEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070057import android.view.View;
Jeff Sharkey549aa162009-05-21 01:33:30 -070058import android.view.ViewConfiguration;
Jeff Sharkey3f177592009-05-18 15:23:12 -070059import android.view.ViewGroup;
60import android.view.ViewTreeObserver;
Jeff Sharkey549aa162009-05-21 01:33:30 -070061import android.view.Window;
62import android.view.WindowManager;
Jeff Sharkey3f177592009-05-18 15:23:12 -070063import android.view.View.OnClickListener;
64import android.view.ViewTreeObserver.OnScrollChangedListener;
Jeff Sharkey549aa162009-05-21 01:33:30 -070065import android.view.accessibility.AccessibilityEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070066import android.widget.AbsListView;
67import android.widget.ImageView;
68import android.widget.LinearLayout;
69import android.widget.ListView;
70import android.widget.PopupWindow;
71import android.widget.TextView;
72import android.widget.Toast;
73import android.widget.AbsListView.OnScrollListener;
74import android.widget.Gallery.LayoutParams;
75
76import java.lang.ref.WeakReference;
77import java.util.ArrayList;
Jeff Sharkey549aa162009-05-21 01:33:30 -070078import java.util.Arrays;
Jeff Sharkey3f177592009-05-18 15:23:12 -070079import java.util.Collections;
80import java.util.Comparator;
81import java.util.HashMap;
82import java.util.Iterator;
Jeff Sharkey549aa162009-05-21 01:33:30 -070083import java.util.LinkedList;
Jeff Sharkey3f177592009-05-18 15:23:12 -070084import java.util.PriorityQueue;
Jeff Sharkey549aa162009-05-21 01:33:30 -070085import java.util.Set;
Jeff Sharkey3f177592009-05-18 15:23:12 -070086
87/**
Jeff Sharkey549aa162009-05-21 01:33:30 -070088 * Window that shows fast-track contact details for a specific
89 * {@link Aggregate#_ID}.
Jeff Sharkey3f177592009-05-18 15:23:12 -070090 */
Jeff Sharkey549aa162009-05-21 01:33:30 -070091public class FastTrackWindow implements Window.Callback, QueryCompleteListener, OnClickListener {
Jeff Sharkey3f177592009-05-18 15:23:12 -070092 private static final String TAG = "FastTrackWindow";
93
Jeff Sharkey549aa162009-05-21 01:33:30 -070094 /**
95 * Interface used to allow the person showing a {@link FastTrackWindow} to
96 * know when the window has been dismissed.
97 */
98 interface OnDismissListener {
99 public void onDismiss(FastTrackWindow dialog);
100 }
101
102 final Context mContext;
103 final LayoutInflater mInflater;
104 final WindowManager mWindowManager;
105 Window mWindow;
106 View mDecor;
107
108 private boolean mQuerying = false;
109 private boolean mShowing = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700110
111 /** Mapping cache from mime-type to icons and actions */
112 private MappingCache mMappingCache;
113
Jeff Sharkey3f177592009-05-18 15:23:12 -0700114 private NotifyingAsyncQueryHandler mHandler;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700115 private OnDismissListener mDismissListener;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700116
Jeff Sharkey549aa162009-05-21 01:33:30 -0700117 private long mAggId;
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700118 private int mAnchorX;
119 private int mAnchorY;
120 private int mAnchorHeight;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700121
Jeff Sharkey549aa162009-05-21 01:33:30 -0700122 private boolean mHasProfile = false;
123 private boolean mHasActions = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700124
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700125 private View mArrowUp;
126 private View mArrowDown;
127
Jeff Sharkey549aa162009-05-21 01:33:30 -0700128 private ImageView mPhoto;
129 private ImageView mPresence;
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700130 private TextView mContent;
131 private TextView mPublished;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700132 private ViewGroup mTrack;
133
134 // TODO: read from a resource somewhere
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700135 private static final int mHeight = 138;
136 private static final int mArrowHeight = 10;
137 private static final int mArrowWidth = 24;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700138
139 /**
140 * Set of {@link ActionInfo} that are associated with the aggregate
141 * currently displayed by this fast-track window.
142 */
143 private ActionSet mActions = new ActionSet();
144
145 /**
146 * Specific mime-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
147 * distinguishes actions that should initiate a text message.
148 */
149 public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
150
151 /**
152 * Specific mime-types that should be bumped to the front of the fast-track.
153 * Other mime-types not appearing in this list follow in alphabetic order.
154 */
155 private static final String[] ORDERED_MIMETYPES = new String[] {
156 Aggregates.CONTENT_ITEM_TYPE,
157 Phones.CONTENT_ITEM_TYPE,
158 MIME_SMS_ADDRESS,
159 Email.CONTENT_ITEM_TYPE,
160 };
161
Jeff Sharkey549aa162009-05-21 01:33:30 -0700162 // TODO: read this status from actual query
163 private static final String STUB_STATUS = "has a really long random status message that would be far too long to read on a single device without the need for tiny reading glasses";
164
165 private static final boolean INCLUDE_PROFILE_ACTION = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700166
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700167 private static final int TOKEN_DISPLAY_NAME = 1;
168 private static final int TOKEN_DATA = 2;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700169
Jeff Sharkey3f177592009-05-18 15:23:12 -0700170 /** Message to show when no activity is found to perform an action */
171 // TODO: move this value into a resources string
172 private static final String NOT_FOUND = "Couldn't find an app to handle this action";
173
Jeff Sharkey549aa162009-05-21 01:33:30 -0700174 /**
175 * Prepare a fast-track window to show in the given {@link Context}.
176 */
177 public FastTrackWindow(Context context) {
178 mContext = new ContextThemeWrapper(context, R.style.FastTrack);
179 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
180 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700181
Jeff Sharkey549aa162009-05-21 01:33:30 -0700182 mWindow = PolicyManager.makeNewWindow(mContext);
183 mWindow.setCallback(this);
184 mWindow.setWindowManager(mWindowManager, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700185
Jeff Sharkey549aa162009-05-21 01:33:30 -0700186 mWindow.setContentView(R.layout.fasttrack);
187
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700188 mArrowUp = (View)mWindow.findViewById(R.id.arrow_up);
189 mArrowDown = (View)mWindow.findViewById(R.id.arrow_down);
190
Jeff Sharkey549aa162009-05-21 01:33:30 -0700191 mPhoto = (ImageView)mWindow.findViewById(R.id.photo);
192 mPresence = (ImageView)mWindow.findViewById(R.id.presence);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700193 mContent = (TextView)mWindow.findViewById(R.id.content);
194 mPublished = (TextView)mWindow.findViewById(R.id.published);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700195 mTrack = (ViewGroup)mWindow.findViewById(R.id.fasttrack);
196
197 // TODO: move generation of mime-type cache to more-efficient place
198 generateMappingCache();
199
Jeff Sharkey3f177592009-05-18 15:23:12 -0700200 }
201
202 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700203 * Prepare a fast-track window to show in the given {@link Context}, and
204 * notify the given {@link OnDismissListener} each time this dialog is
205 * dismissed.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700206 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700207 public FastTrackWindow(Context context, OnDismissListener dismissListener) {
208 this(context);
209 mDismissListener = dismissListener;
210 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700211
Jeff Sharkey549aa162009-05-21 01:33:30 -0700212 /**
213 * Generate {@link MappingCache} specifically for fast-track windows. This
214 * cache knows how to display {@link CommonDataKinds#PACKAGE_COMMON} data
215 * types using generic icons.
216 */
217 private void generateMappingCache() {
218 mMappingCache = MappingCache.createAndFill(mContext);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700219
Jeff Sharkey549aa162009-05-21 01:33:30 -0700220 Resources res = mContext.getResources();
221 Mapping mapping;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700222
Jeff Sharkey549aa162009-05-21 01:33:30 -0700223 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Aggregates.CONTENT_ITEM_TYPE);
224 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.ic_contacts_details);
225 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700226
Jeff Sharkey549aa162009-05-21 01:33:30 -0700227 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Phone.CONTENT_ITEM_TYPE);
228 mapping.summaryColumn = Phone.NUMBER;
229 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_call);
230 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700231
Jeff Sharkey549aa162009-05-21 01:33:30 -0700232 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, MIME_SMS_ADDRESS);
233 mapping.summaryColumn = Phone.NUMBER;
234 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.sym_action_sms);
235 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700236
Jeff Sharkey549aa162009-05-21 01:33:30 -0700237 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Email.CONTENT_ITEM_TYPE);
238 mapping.summaryColumn = Email.DATA;
239 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_email);
240 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700241
Jeff Sharkey549aa162009-05-21 01:33:30 -0700242 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700243
Jeff Sharkey549aa162009-05-21 01:33:30 -0700244 /**
245 * Start showing a fast-track window for the given {@link Aggregate#_ID}
246 * pointing towards the given location.
247 */
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700248 public void show(Uri aggUri, int x, int y, int height) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700249 if (mShowing || mQuerying) {
250 Log.w(TAG, "already in process of showing");
251 return;
252 }
253
254 mAggId = ContentUris.parseId(aggUri);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700255 mAnchorX = x;
256 mAnchorY = y;
257 mAnchorHeight = height;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700258 mQuerying = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700259
260 // Start data query in background
Jeff Sharkey549aa162009-05-21 01:33:30 -0700261 Uri dataUri = Uri.withAppendedPath(aggUri,
262 ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700263
Jeff Sharkey549aa162009-05-21 01:33:30 -0700264 // TODO: also query for latest status message
265 mHandler = new NotifyingAsyncQueryHandler(mContext, this);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700266 mHandler.startQuery(TOKEN_DISPLAY_NAME, null, aggUri, null, null, null, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700267 mHandler.startQuery(TOKEN_DATA, null, dataUri, null, null, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700268
269 }
270
271 /**
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700272 * Show the correct callout arrow based on a {@link R.id} reference.
273 */
274 private void showArrow(int whichArrow, int requestedX) {
275 final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
276 final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
277
278 showArrow.setVisibility(View.VISIBLE);
279 LinearLayout.LayoutParams param = (LinearLayout.LayoutParams)showArrow.getLayoutParams();
280 param.leftMargin = requestedX - mArrowWidth / 2;
281
282 hideArrow.setVisibility(View.INVISIBLE);
283 }
284
285 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700286 * Actual internal method to show this fast-track window. Called only by
287 * {@link #considerShowing()} when all data requirements have been met.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700288 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700289 private void showInternal() {
290 mDecor = mWindow.getDecorView();
291 WindowManager.LayoutParams l = mWindow.getAttributes();
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700292
Jeff Sharkey549aa162009-05-21 01:33:30 -0700293 l.gravity = Gravity.TOP | Gravity.LEFT;
294 l.x = 0;
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700295
296 if (mAnchorY > mHeight) {
297 // Show downwards callout when enough room
298 showArrow(R.id.arrow_down, mAnchorX);
299 l.y = mAnchorY - (mHeight - (mArrowHeight * 2) - 5);
300
301 } else {
302 // Otherwise show upwards callout
303 showArrow(R.id.arrow_up, mAnchorX);
304 l.y = mAnchorY + mAnchorHeight - 10;
305
306 }
307
Jeff Sharkey549aa162009-05-21 01:33:30 -0700308 l.width = WindowManager.LayoutParams.FILL_PARENT;
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700309 l.height = mHeight;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700310
311 l.dimAmount = 0.6f;
312 l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND
313 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
314 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
315 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
316 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
317
318 mWindowManager.addView(mDecor, l);
319 mShowing = true;
320 mQuerying = false;
321 }
322
323 /**
324 * Dismiss this fast-track window if showing.
325 */
326 public void dismiss() {
327 if (!mQuerying && !mShowing) {
328 Log.d(TAG, "not visible, ignore");
329 return;
330 }
331
332 // Cancel any pending queries
333 mHandler.cancelOperation(TOKEN_DISPLAY_NAME);
334 mHandler.cancelOperation(TOKEN_DATA);
335
336 // Reset all views to prepare for possible recycling
337 mPhoto.setImageResource(R.drawable.ic_contact_list_picture);
338 mPresence.setImageDrawable(null);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700339 mContent.setText(null);
340 mPublished.setText(null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700341
342 mActions.clear();
343 mTrack.removeAllViews();
344
345 mHasProfile = false;
346 mHasActions = false;
347
348 if (mDecor == null || !mShowing) {
349 Log.d(TAG, "not showing, ignore");
350 return;
351 }
352
353 mWindowManager.removeView(mDecor);
354 mDecor = null;
355 mWindow.closeAllPanels();
356 mShowing = false;
357
358 // Notify any listeners that we've been dismissed
359 if (mDismissListener != null) {
360 mDismissListener.onDismiss(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700361 }
362 }
363
Jeff Sharkey549aa162009-05-21 01:33:30 -0700364 /**
365 * Returns true if this fast-track window is showing or querying.
366 */
367 public boolean isShowing() {
368 return mShowing || mQuerying;
369 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700370
Jeff Sharkey549aa162009-05-21 01:33:30 -0700371 /**
372 * Consider showing this window, which will only call through to
373 * {@link #showInternal()} when all data items are present.
374 */
375 private synchronized void considerShowing() {
376 if (mHasActions && mHasProfile && !mShowing) {
377 showInternal();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700378 }
379 }
380
381 /** {@inheritDoc} */
382 public void onQueryComplete(int token, Object cookie, Cursor cursor) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700383 if (cursor == null) {
384 return;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700385 } else if (token == TOKEN_DISPLAY_NAME) {
386 handleDisplayName(cursor);
387 } else if (token == TOKEN_DATA) {
388 handleData(cursor);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700389 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700390 }
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700391
392 private SpannableStringBuilder mBuilder = new SpannableStringBuilder();
393 private CharacterStyle mStyleBold = new StyleSpan(android.graphics.Typeface.BOLD);
394 private CharacterStyle mStyleBlack = new ForegroundColorSpan(Color.BLACK);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700395
396 /**
397 * Handle the result from the {@link TOKEN_DISPLAY_NAME} query.
398 */
399 private void handleDisplayName(Cursor cursor) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700400 final int COL_DISPLAY_NAME = cursor.getColumnIndex(Aggregates.DISPLAY_NAME);
401
402 if (cursor.moveToNext()) {
403 String foundName = cursor.getString(COL_DISPLAY_NAME);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700404
405 mBuilder.clear();
406 mBuilder.append(foundName);
407 mBuilder.append(" ");
408 mBuilder.append(STUB_STATUS);
409 mBuilder.setSpan(mStyleBold, 0, foundName.length(), 0);
410 mBuilder.setSpan(mStyleBlack, 0, foundName.length(), 0);
411 mContent.setText(mBuilder);
412
413 mPublished.setText("4 hours ago");
414
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700415 }
416
Jeff Sharkey549aa162009-05-21 01:33:30 -0700417 mHasProfile = true;
418 considerShowing();
419 }
420
421 /**
422 * Description of a specific, actionable {@link Data#_ID} item. May have a
423 * {@link Mapping} associated with it to find {@link RemoteViews} or icon,
424 * and may have built a summary of itself for UI display.
425 */
426 private class ActionInfo {
427 long dataId;
428 String packageName;
429 String mimeType;
430
431 Mapping mapping;
432 String summaryValue;
433
434 /**
435 * Create an action from common {@link Data} elements.
436 */
437 public ActionInfo(long dataId, String packageName, String mimeType) {
438 this.dataId = dataId;
439 this.packageName = packageName;
440 this.mimeType = mimeType;
441 }
442
443 /**
444 * Attempt to find a {@link Mapping} for the package and mime-type
445 * defined by this action. Returns true if one was found.
446 */
447 public boolean findMapping(MappingCache cache) {
448 mapping = cache.findMapping(packageName, mimeType);
449 return (mapping != null);
450 }
451
452 /**
453 * Given a {@link Cursor} pointed at the {@link Data} row associated
454 * with this action, use the {@link Mapping} to build a text summary.
455 */
456 public void buildSummary(Cursor cursor) {
457 if (mapping == null || mapping.summaryColumn == null) return;
458 int index = cursor.getColumnIndex(mapping.summaryColumn);
459 if (index != -1) {
460 summaryValue = cursor.getString(index);
461 }
462 }
463
464 /**
465 * Build an {@link Intent} that will perform this action.
466 */
467 public Intent buildIntent() {
468 // Handle well-known mime-types with special care
469 if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
470 Uri callUri = Uri.parse("tel:" + Uri.encode(summaryValue));
471 return new Intent(Intent.ACTION_DIAL, callUri);
472
473 } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
474 Uri smsUri = Uri.fromParts("smsto", summaryValue, null);
475 return new Intent(Intent.ACTION_SENDTO, smsUri);
476
477 } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
478 Uri mailUri = Uri.fromParts("mailto", summaryValue, null);
479 return new Intent(Intent.ACTION_SENDTO, mailUri);
480
481 }
482
483 // Otherwise fall back to default VIEW action
484 Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
485
486 Intent intent = new Intent(Intent.ACTION_VIEW);
487 intent.setData(dataUri);
488
489 return intent;
490 }
491 }
492
493 /**
494 * Provide a simple way of collecting one or more {@link ActionInfo} objects
495 * under a mime-type key.
496 */
497 private class ActionSet extends HashMap<String, LinkedList<ActionInfo>> {
498 private void collect(String mimeType, ActionInfo info) {
499 // Create mime-type set if needed
500 if (!containsKey(mimeType)) {
501 put(mimeType, new LinkedList<ActionInfo>());
502 }
503 LinkedList<ActionInfo> collectList = get(mimeType);
504 collectList.add(info);
505 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700506 }
507
508 /**
509 * Handle the result from the {@link TOKEN_DATA} query.
510 */
511 private void handleData(Cursor cursor) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700512 final int COL_ID = cursor.getColumnIndex(Data._ID);
513 final int COL_PACKAGE = cursor.getColumnIndex(Data.PACKAGE);
514 final int COL_MIMETYPE = cursor.getColumnIndex(Data.MIMETYPE);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700515 final int COL_PHOTO = cursor.getColumnIndex(Photo.PHOTO);
516
Jeff Sharkey549aa162009-05-21 01:33:30 -0700517 ActionInfo info;
518
519 // Add the profile shortcut action if requested
520 if (INCLUDE_PROFILE_ACTION) {
521 final String mimeType = Aggregates.CONTENT_ITEM_TYPE;
522 info = new ActionInfo(mAggId, CommonDataKinds.PACKAGE_COMMON, mimeType);
523 if (info.findMapping(mMappingCache)) {
524 mActions.collect(mimeType, info);
525 }
526 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700527
528 while (cursor.moveToNext()) {
529 final long dataId = cursor.getLong(COL_ID);
530 final String packageName = cursor.getString(COL_PACKAGE);
531 final String mimeType = cursor.getString(COL_MIMETYPE);
532
Jeff Sharkey549aa162009-05-21 01:33:30 -0700533 // Handle when a photo appears in the various data items
534 // TODO: accept a photo only if its marked as primary
535 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700536 byte[] photoBlob = cursor.getBlob(COL_PHOTO);
537 Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700538 mPhoto.setImageBitmap(photoBitmap);
Jeff Sharkey69126682009-05-18 21:33:41 -0700539 continue;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700540 }
541
Jeff Sharkey549aa162009-05-21 01:33:30 -0700542 // Build an action for this data entry, find a mapping to a UI
543 // element, build its summary from the cursor, and collect it along
544 // with all others of this mime-type.
545 info = new ActionInfo(dataId, packageName, mimeType);
546 if (info.findMapping(mMappingCache)) {
547 info.buildSummary(cursor);
548 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700549 }
550
Jeff Sharkey549aa162009-05-21 01:33:30 -0700551 // If phone number, also insert as text message action
552 if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) {
553 info = new ActionInfo(dataId, packageName, MIME_SMS_ADDRESS);
554 if (info.findMapping(mMappingCache)) {
555 info.buildSummary(cursor);
556 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700557 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700558 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700559 }
560
561 cursor.close();
562
Jeff Sharkey549aa162009-05-21 01:33:30 -0700563 // Turn our list of actions into UI elements, starting with common types
564 Set<String> containedTypes = mActions.keySet();
565 for (String mimeType : ORDERED_MIMETYPES) {
566 if (containedTypes.contains(mimeType)) {
567 mTrack.addView(inflateAction(mimeType));
568 containedTypes.remove(mimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700569 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700570 }
571
Jeff Sharkey549aa162009-05-21 01:33:30 -0700572 // Then continue with remaining mime-types in alphabetical order
573 String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
574 Arrays.sort(remainingTypes);
575 for (String mimeType : remainingTypes) {
576 mTrack.addView(inflateAction(mimeType));
577 }
578
579 mHasActions = true;
580 considerShowing();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700581 }
582
583 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700584 * Inflate the in-track view for the action of the given mime-type. Will use
585 * the icon provided by the {@link Mapping}.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700586 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700587 private View inflateAction(String mimeType) {
588 ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700589
Jeff Sharkey549aa162009-05-21 01:33:30 -0700590 // Add direct intent if single child, otherwise flag for multiple
591 LinkedList<ActionInfo> children = mActions.get(mimeType);
592 ActionInfo firstInfo = children.get(0);
593 if (children.size() == 1) {
594 view.setTag(firstInfo.buildIntent());
595 } else {
596 view.setTag(mimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700597 }
598
Jeff Sharkey549aa162009-05-21 01:33:30 -0700599 // Set icon and listen for clicks
600 view.setImageBitmap(firstInfo.mapping.icon);
601 view.setOnClickListener(this);
602 return view;
603 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700604
Jeff Sharkey549aa162009-05-21 01:33:30 -0700605 /** {@inheritDoc} */
606 public void onClick(View v) {
607 final Object tag = v.getTag();
608 if (tag instanceof Intent) {
609 // Incoming tag is concrete intent, so launch
610 try {
611 mContext.startActivity((Intent)tag);
612 } catch (ActivityNotFoundException e) {
613 Log.w(TAG, NOT_FOUND);
614 Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
615 }
616 } else if (tag instanceof String) {
617 // Incoming tag is a mime-type, so show resolution list
618 LinkedList<ActionInfo> children = mActions.get(tag);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700619
Jeff Sharkey549aa162009-05-21 01:33:30 -0700620 // TODO: show drop-down resolution list
621 Log.d(TAG, "would show list between several options here");
622
623 }
624 }
625
626 /** {@inheritDoc} */
627 public boolean dispatchKeyEvent(KeyEvent event) {
628 return mWindow.superDispatchKeyEvent(event);
629 }
630
631 /** {@inheritDoc} */
632 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
633 // TODO: make this window accessible
634 return false;
635 }
636
637 /** {@inheritDoc} */
638 public boolean dispatchTouchEvent(MotionEvent event) {
639 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
640 dismiss();
641 return true;
642 }
643 return mWindow.superDispatchTouchEvent(event);
644 }
645
646 /** {@inheritDoc} */
647 public boolean dispatchTrackballEvent(MotionEvent event) {
648 return mWindow.superDispatchTrackballEvent(event);
649 }
650
651 /** {@inheritDoc} */
652 public void onContentChanged() {
653 }
654
655 /** {@inheritDoc} */
656 public boolean onCreatePanelMenu(int featureId, Menu menu) {
657 return false;
658 }
659
660 /** {@inheritDoc} */
661 public View onCreatePanelView(int featureId) {
662 return null;
663 }
664
665 /** {@inheritDoc} */
666 public boolean onMenuItemSelected(int featureId, MenuItem item) {
667 return false;
668 }
669
670 /** {@inheritDoc} */
671 public boolean onMenuOpened(int featureId, Menu menu) {
672 return false;
673 }
674
675 /** {@inheritDoc} */
676 public void onPanelClosed(int featureId, Menu menu) {
677 }
678
679 /** {@inheritDoc} */
680 public boolean onPreparePanel(int featureId, View view, Menu menu) {
681 return false;
682 }
683
684 /** {@inheritDoc} */
685 public boolean onSearchRequested() {
686 return false;
687 }
688
689 /** {@inheritDoc} */
690 public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
691 if (mDecor != null) {
692 mWindowManager.updateViewLayout(mDecor, attrs);
693 }
694 }
695
696 /** {@inheritDoc} */
697 public void onWindowFocusChanged(boolean hasFocus) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700698 }
699}