blob: 1252086248359097ebf9017f3bd03f9a2e7fe4d7 [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 Sharkey9bd858b2009-05-21 01:33:30 -0700162// public static final int ICON_SIZE = 42;
163// public static final int ICON_PADDING = 3;
164
Jeff Sharkey549aa162009-05-21 01:33:30 -0700165 // TODO: read this status from actual query
166 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";
167
168 private static final boolean INCLUDE_PROFILE_ACTION = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700169
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700170 private static final int TOKEN_DISPLAY_NAME = 1;
171 private static final int TOKEN_DATA = 2;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700172
Jeff Sharkey3f177592009-05-18 15:23:12 -0700173 /** Message to show when no activity is found to perform an action */
174 // TODO: move this value into a resources string
175 private static final String NOT_FOUND = "Couldn't find an app to handle this action";
176
Jeff Sharkey549aa162009-05-21 01:33:30 -0700177 /**
178 * Prepare a fast-track window to show in the given {@link Context}.
179 */
180 public FastTrackWindow(Context context) {
181 mContext = new ContextThemeWrapper(context, R.style.FastTrack);
182 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
183 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700184
Jeff Sharkey549aa162009-05-21 01:33:30 -0700185 mWindow = PolicyManager.makeNewWindow(mContext);
186 mWindow.setCallback(this);
187 mWindow.setWindowManager(mWindowManager, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700188
Jeff Sharkey549aa162009-05-21 01:33:30 -0700189 mWindow.setContentView(R.layout.fasttrack);
190
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700191 mArrowUp = (View)mWindow.findViewById(R.id.arrow_up);
192 mArrowDown = (View)mWindow.findViewById(R.id.arrow_down);
193
Jeff Sharkey549aa162009-05-21 01:33:30 -0700194 mPhoto = (ImageView)mWindow.findViewById(R.id.photo);
195 mPresence = (ImageView)mWindow.findViewById(R.id.presence);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700196 mContent = (TextView)mWindow.findViewById(R.id.content);
197 mPublished = (TextView)mWindow.findViewById(R.id.published);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700198 mTrack = (ViewGroup)mWindow.findViewById(R.id.fasttrack);
199
200 // TODO: move generation of mime-type cache to more-efficient place
201 generateMappingCache();
202
Jeff Sharkey3f177592009-05-18 15:23:12 -0700203 }
204
205 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700206 * Prepare a fast-track window to show in the given {@link Context}, and
207 * notify the given {@link OnDismissListener} each time this dialog is
208 * dismissed.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700209 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700210 public FastTrackWindow(Context context, OnDismissListener dismissListener) {
211 this(context);
212 mDismissListener = dismissListener;
213 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700214
Jeff Sharkey549aa162009-05-21 01:33:30 -0700215 /**
216 * Generate {@link MappingCache} specifically for fast-track windows. This
217 * cache knows how to display {@link CommonDataKinds#PACKAGE_COMMON} data
218 * types using generic icons.
219 */
220 private void generateMappingCache() {
221 mMappingCache = MappingCache.createAndFill(mContext);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700222
Jeff Sharkey549aa162009-05-21 01:33:30 -0700223 Resources res = mContext.getResources();
224 Mapping mapping;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700225
Jeff Sharkey549aa162009-05-21 01:33:30 -0700226 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Aggregates.CONTENT_ITEM_TYPE);
227 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.ic_contacts_details);
228 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700229
Jeff Sharkey549aa162009-05-21 01:33:30 -0700230 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Phone.CONTENT_ITEM_TYPE);
231 mapping.summaryColumn = Phone.NUMBER;
232 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_call);
233 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700234
Jeff Sharkey549aa162009-05-21 01:33:30 -0700235 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, MIME_SMS_ADDRESS);
236 mapping.summaryColumn = Phone.NUMBER;
237 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.sym_action_sms);
238 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700239
Jeff Sharkey549aa162009-05-21 01:33:30 -0700240 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Email.CONTENT_ITEM_TYPE);
241 mapping.summaryColumn = Email.DATA;
242 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_email);
243 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700244
Jeff Sharkey549aa162009-05-21 01:33:30 -0700245 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700246
Jeff Sharkey549aa162009-05-21 01:33:30 -0700247 /**
248 * Start showing a fast-track window for the given {@link Aggregate#_ID}
249 * pointing towards the given location.
250 */
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700251 public void show(Uri aggUri, int x, int y, int height) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700252 if (mShowing || mQuerying) {
253 Log.w(TAG, "already in process of showing");
254 return;
255 }
256
257 mAggId = ContentUris.parseId(aggUri);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700258 mAnchorX = x;
259 mAnchorY = y;
260 mAnchorHeight = height;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700261 mQuerying = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700262
263 // Start data query in background
Jeff Sharkey549aa162009-05-21 01:33:30 -0700264 Uri dataUri = Uri.withAppendedPath(aggUri,
265 ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700266
Jeff Sharkey549aa162009-05-21 01:33:30 -0700267 // TODO: also query for latest status message
268 mHandler = new NotifyingAsyncQueryHandler(mContext, this);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700269 mHandler.startQuery(TOKEN_DISPLAY_NAME, null, aggUri, null, null, null, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700270 mHandler.startQuery(TOKEN_DATA, null, dataUri, null, null, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700271
272 }
273
274 /**
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700275 * Show the correct callout arrow based on a {@link R.id} reference.
276 */
277 private void showArrow(int whichArrow, int requestedX) {
278 final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
279 final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
280
281 showArrow.setVisibility(View.VISIBLE);
282 LinearLayout.LayoutParams param = (LinearLayout.LayoutParams)showArrow.getLayoutParams();
283 param.leftMargin = requestedX - mArrowWidth / 2;
284
285 hideArrow.setVisibility(View.INVISIBLE);
286 }
287
288 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700289 * Actual internal method to show this fast-track window. Called only by
290 * {@link #considerShowing()} when all data requirements have been met.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700291 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700292 private void showInternal() {
293 mDecor = mWindow.getDecorView();
294 WindowManager.LayoutParams l = mWindow.getAttributes();
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700295
Jeff Sharkey549aa162009-05-21 01:33:30 -0700296 l.gravity = Gravity.TOP | Gravity.LEFT;
297 l.x = 0;
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700298
299 if (mAnchorY > mHeight) {
300 // Show downwards callout when enough room
301 showArrow(R.id.arrow_down, mAnchorX);
302 l.y = mAnchorY - (mHeight - (mArrowHeight * 2) - 5);
303
304 } else {
305 // Otherwise show upwards callout
306 showArrow(R.id.arrow_up, mAnchorX);
307 l.y = mAnchorY + mAnchorHeight - 10;
308
309 }
310
Jeff Sharkey549aa162009-05-21 01:33:30 -0700311 l.width = WindowManager.LayoutParams.FILL_PARENT;
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700312 l.height = mHeight;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700313
314 l.dimAmount = 0.6f;
315 l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND
316 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
317 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
318 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
319 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
320
321 mWindowManager.addView(mDecor, l);
322 mShowing = true;
323 mQuerying = false;
324 }
325
326 /**
327 * Dismiss this fast-track window if showing.
328 */
329 public void dismiss() {
330 if (!mQuerying && !mShowing) {
331 Log.d(TAG, "not visible, ignore");
332 return;
333 }
334
335 // Cancel any pending queries
336 mHandler.cancelOperation(TOKEN_DISPLAY_NAME);
337 mHandler.cancelOperation(TOKEN_DATA);
338
339 // Reset all views to prepare for possible recycling
340 mPhoto.setImageResource(R.drawable.ic_contact_list_picture);
341 mPresence.setImageDrawable(null);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700342 mContent.setText(null);
343 mPublished.setText(null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700344
345 mActions.clear();
346 mTrack.removeAllViews();
347
348 mHasProfile = false;
349 mHasActions = false;
350
351 if (mDecor == null || !mShowing) {
352 Log.d(TAG, "not showing, ignore");
353 return;
354 }
355
356 mWindowManager.removeView(mDecor);
357 mDecor = null;
358 mWindow.closeAllPanels();
359 mShowing = false;
360
361 // Notify any listeners that we've been dismissed
362 if (mDismissListener != null) {
363 mDismissListener.onDismiss(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700364 }
365 }
366
Jeff Sharkey549aa162009-05-21 01:33:30 -0700367 /**
368 * Returns true if this fast-track window is showing or querying.
369 */
370 public boolean isShowing() {
371 return mShowing || mQuerying;
372 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700373
Jeff Sharkey549aa162009-05-21 01:33:30 -0700374 /**
375 * Consider showing this window, which will only call through to
376 * {@link #showInternal()} when all data items are present.
377 */
378 private synchronized void considerShowing() {
379 if (mHasActions && mHasProfile && !mShowing) {
380 showInternal();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700381 }
382 }
383
384 /** {@inheritDoc} */
385 public void onQueryComplete(int token, Object cookie, Cursor cursor) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700386 if (cursor == null) {
387 return;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700388 } else if (token == TOKEN_DISPLAY_NAME) {
389 handleDisplayName(cursor);
390 } else if (token == TOKEN_DATA) {
391 handleData(cursor);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700392 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700393 }
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700394
395 private SpannableStringBuilder mBuilder = new SpannableStringBuilder();
396 private CharacterStyle mStyleBold = new StyleSpan(android.graphics.Typeface.BOLD);
397 private CharacterStyle mStyleBlack = new ForegroundColorSpan(Color.BLACK);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700398
399 /**
400 * Handle the result from the {@link TOKEN_DISPLAY_NAME} query.
401 */
402 private void handleDisplayName(Cursor cursor) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700403 final int COL_DISPLAY_NAME = cursor.getColumnIndex(Aggregates.DISPLAY_NAME);
404
405 if (cursor.moveToNext()) {
406 String foundName = cursor.getString(COL_DISPLAY_NAME);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700407
408 mBuilder.clear();
409 mBuilder.append(foundName);
410 mBuilder.append(" ");
411 mBuilder.append(STUB_STATUS);
412 mBuilder.setSpan(mStyleBold, 0, foundName.length(), 0);
413 mBuilder.setSpan(mStyleBlack, 0, foundName.length(), 0);
414 mContent.setText(mBuilder);
415
416 mPublished.setText("4 hours ago");
417
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700418 }
419
Jeff Sharkey549aa162009-05-21 01:33:30 -0700420 mHasProfile = true;
421 considerShowing();
422 }
423
424 /**
425 * Description of a specific, actionable {@link Data#_ID} item. May have a
426 * {@link Mapping} associated with it to find {@link RemoteViews} or icon,
427 * and may have built a summary of itself for UI display.
428 */
429 private class ActionInfo {
430 long dataId;
431 String packageName;
432 String mimeType;
433
434 Mapping mapping;
435 String summaryValue;
436
437 /**
438 * Create an action from common {@link Data} elements.
439 */
440 public ActionInfo(long dataId, String packageName, String mimeType) {
441 this.dataId = dataId;
442 this.packageName = packageName;
443 this.mimeType = mimeType;
444 }
445
446 /**
447 * Attempt to find a {@link Mapping} for the package and mime-type
448 * defined by this action. Returns true if one was found.
449 */
450 public boolean findMapping(MappingCache cache) {
451 mapping = cache.findMapping(packageName, mimeType);
452 return (mapping != null);
453 }
454
455 /**
456 * Given a {@link Cursor} pointed at the {@link Data} row associated
457 * with this action, use the {@link Mapping} to build a text summary.
458 */
459 public void buildSummary(Cursor cursor) {
460 if (mapping == null || mapping.summaryColumn == null) return;
461 int index = cursor.getColumnIndex(mapping.summaryColumn);
462 if (index != -1) {
463 summaryValue = cursor.getString(index);
464 }
465 }
466
467 /**
468 * Build an {@link Intent} that will perform this action.
469 */
470 public Intent buildIntent() {
471 // Handle well-known mime-types with special care
472 if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
473 Uri callUri = Uri.parse("tel:" + Uri.encode(summaryValue));
474 return new Intent(Intent.ACTION_DIAL, callUri);
475
476 } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
477 Uri smsUri = Uri.fromParts("smsto", summaryValue, null);
478 return new Intent(Intent.ACTION_SENDTO, smsUri);
479
480 } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
481 Uri mailUri = Uri.fromParts("mailto", summaryValue, null);
482 return new Intent(Intent.ACTION_SENDTO, mailUri);
483
484 }
485
486 // Otherwise fall back to default VIEW action
487 Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
488
489 Intent intent = new Intent(Intent.ACTION_VIEW);
490 intent.setData(dataUri);
491
492 return intent;
493 }
494 }
495
496 /**
497 * Provide a simple way of collecting one or more {@link ActionInfo} objects
498 * under a mime-type key.
499 */
500 private class ActionSet extends HashMap<String, LinkedList<ActionInfo>> {
501 private void collect(String mimeType, ActionInfo info) {
502 // Create mime-type set if needed
503 if (!containsKey(mimeType)) {
504 put(mimeType, new LinkedList<ActionInfo>());
505 }
506 LinkedList<ActionInfo> collectList = get(mimeType);
507 collectList.add(info);
508 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700509 }
510
511 /**
512 * Handle the result from the {@link TOKEN_DATA} query.
513 */
514 private void handleData(Cursor cursor) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700515 final int COL_ID = cursor.getColumnIndex(Data._ID);
516 final int COL_PACKAGE = cursor.getColumnIndex(Data.PACKAGE);
517 final int COL_MIMETYPE = cursor.getColumnIndex(Data.MIMETYPE);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700518 final int COL_PHOTO = cursor.getColumnIndex(Photo.PHOTO);
519
Jeff Sharkey549aa162009-05-21 01:33:30 -0700520 ActionInfo info;
521
522 // Add the profile shortcut action if requested
523 if (INCLUDE_PROFILE_ACTION) {
524 final String mimeType = Aggregates.CONTENT_ITEM_TYPE;
525 info = new ActionInfo(mAggId, CommonDataKinds.PACKAGE_COMMON, mimeType);
526 if (info.findMapping(mMappingCache)) {
527 mActions.collect(mimeType, info);
528 }
529 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700530
531 while (cursor.moveToNext()) {
532 final long dataId = cursor.getLong(COL_ID);
533 final String packageName = cursor.getString(COL_PACKAGE);
534 final String mimeType = cursor.getString(COL_MIMETYPE);
535
Jeff Sharkey549aa162009-05-21 01:33:30 -0700536 // Handle when a photo appears in the various data items
537 // TODO: accept a photo only if its marked as primary
538 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700539 byte[] photoBlob = cursor.getBlob(COL_PHOTO);
540 Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700541 mPhoto.setImageBitmap(photoBitmap);
Jeff Sharkey69126682009-05-18 21:33:41 -0700542 continue;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700543 }
544
Jeff Sharkey549aa162009-05-21 01:33:30 -0700545 // Build an action for this data entry, find a mapping to a UI
546 // element, build its summary from the cursor, and collect it along
547 // with all others of this mime-type.
548 info = new ActionInfo(dataId, packageName, mimeType);
549 if (info.findMapping(mMappingCache)) {
550 info.buildSummary(cursor);
551 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700552 }
553
Jeff Sharkey549aa162009-05-21 01:33:30 -0700554 // If phone number, also insert as text message action
555 if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) {
556 info = new ActionInfo(dataId, packageName, MIME_SMS_ADDRESS);
557 if (info.findMapping(mMappingCache)) {
558 info.buildSummary(cursor);
559 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700560 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700561 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700562 }
563
564 cursor.close();
565
Jeff Sharkey549aa162009-05-21 01:33:30 -0700566 // Turn our list of actions into UI elements, starting with common types
567 Set<String> containedTypes = mActions.keySet();
568 for (String mimeType : ORDERED_MIMETYPES) {
569 if (containedTypes.contains(mimeType)) {
570 mTrack.addView(inflateAction(mimeType));
571 containedTypes.remove(mimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700572 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700573 }
574
Jeff Sharkey549aa162009-05-21 01:33:30 -0700575 // Then continue with remaining mime-types in alphabetical order
576 String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
577 Arrays.sort(remainingTypes);
578 for (String mimeType : remainingTypes) {
579 mTrack.addView(inflateAction(mimeType));
580 }
581
582 mHasActions = true;
583 considerShowing();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700584 }
585
586 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700587 * Inflate the in-track view for the action of the given mime-type. Will use
588 * the icon provided by the {@link Mapping}.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700589 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700590 private View inflateAction(String mimeType) {
591 ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700592
Jeff Sharkey549aa162009-05-21 01:33:30 -0700593 // Add direct intent if single child, otherwise flag for multiple
594 LinkedList<ActionInfo> children = mActions.get(mimeType);
595 ActionInfo firstInfo = children.get(0);
596 if (children.size() == 1) {
597 view.setTag(firstInfo.buildIntent());
598 } else {
599 view.setTag(mimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700600 }
601
Jeff Sharkey549aa162009-05-21 01:33:30 -0700602 // Set icon and listen for clicks
603 view.setImageBitmap(firstInfo.mapping.icon);
604 view.setOnClickListener(this);
605 return view;
606 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700607
Jeff Sharkey549aa162009-05-21 01:33:30 -0700608 /** {@inheritDoc} */
609 public void onClick(View v) {
610 final Object tag = v.getTag();
611 if (tag instanceof Intent) {
612 // Incoming tag is concrete intent, so launch
613 try {
614 mContext.startActivity((Intent)tag);
615 } catch (ActivityNotFoundException e) {
616 Log.w(TAG, NOT_FOUND);
617 Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
618 }
619 } else if (tag instanceof String) {
620 // Incoming tag is a mime-type, so show resolution list
621 LinkedList<ActionInfo> children = mActions.get(tag);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700622
Jeff Sharkey549aa162009-05-21 01:33:30 -0700623 // TODO: show drop-down resolution list
624 Log.d(TAG, "would show list between several options here");
625
626 }
627 }
628
629 /** {@inheritDoc} */
630 public boolean dispatchKeyEvent(KeyEvent event) {
631 return mWindow.superDispatchKeyEvent(event);
632 }
633
634 /** {@inheritDoc} */
635 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
636 // TODO: make this window accessible
637 return false;
638 }
639
640 /** {@inheritDoc} */
641 public boolean dispatchTouchEvent(MotionEvent event) {
642 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
643 dismiss();
644 return true;
645 }
646 return mWindow.superDispatchTouchEvent(event);
647 }
648
649 /** {@inheritDoc} */
650 public boolean dispatchTrackballEvent(MotionEvent event) {
651 return mWindow.superDispatchTrackballEvent(event);
652 }
653
654 /** {@inheritDoc} */
655 public void onContentChanged() {
656 }
657
658 /** {@inheritDoc} */
659 public boolean onCreatePanelMenu(int featureId, Menu menu) {
660 return false;
661 }
662
663 /** {@inheritDoc} */
664 public View onCreatePanelView(int featureId) {
665 return null;
666 }
667
668 /** {@inheritDoc} */
669 public boolean onMenuItemSelected(int featureId, MenuItem item) {
670 return false;
671 }
672
673 /** {@inheritDoc} */
674 public boolean onMenuOpened(int featureId, Menu menu) {
675 return false;
676 }
677
678 /** {@inheritDoc} */
679 public void onPanelClosed(int featureId, Menu menu) {
680 }
681
682 /** {@inheritDoc} */
683 public boolean onPreparePanel(int featureId, View view, Menu menu) {
684 return false;
685 }
686
687 /** {@inheritDoc} */
688 public boolean onSearchRequested() {
689 return false;
690 }
691
692 /** {@inheritDoc} */
693 public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
694 if (mDecor != null) {
695 mWindowManager.updateViewLayout(mDecor, attrs);
696 }
697 }
698
699 /** {@inheritDoc} */
700 public void onWindowFocusChanged(boolean hasFocus) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700701 }
702}