blob: 63ba476c341322a3df5c7e9b24e53a5f9b740c9c [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 Sharkey549aa162009-05-21 01:33:30 -070020import com.android.contacts.SocialStreamActivity.Mapping;
Jeff Sharkey39261272009-06-03 19:15:09 -070021import com.android.contacts.SocialStreamActivity.MappingCache;
Jeff Sharkey549aa162009-05-21 01:33:30 -070022import com.android.internal.policy.PolicyManager;
Jeff Sharkey3f177592009-05-18 15:23:12 -070023
24import android.content.ActivityNotFoundException;
25import android.content.ContentUris;
26import android.content.Context;
27import android.content.Intent;
28import android.content.res.Resources;
29import android.database.Cursor;
Jeff Sharkeyecedf752009-05-18 22:07:40 -070030import android.graphics.Bitmap;
31import android.graphics.BitmapFactory;
Jeff Sharkey80a193a2009-05-21 14:18:18 -070032import android.graphics.Color;
Jeff Sharkey39261272009-06-03 19:15:09 -070033import android.graphics.Rect;
Jeff Sharkey3f177592009-05-18 15:23:12 -070034import android.net.Uri;
Evan Millar66388be2009-05-28 15:41:07 -070035import android.provider.ContactsContract;
Jeff Sharkey39261272009-06-03 19:15:09 -070036import android.provider.SocialContract;
37import android.provider.Contacts.Phones;
Evan Millar66388be2009-05-28 15:41:07 -070038import android.provider.ContactsContract.Aggregates;
39import android.provider.ContactsContract.CommonDataKinds;
40import android.provider.ContactsContract.Data;
Jeff Sharkey39261272009-06-03 19:15:09 -070041import android.provider.ContactsContract.Presence;
Evan Millar66388be2009-05-28 15:41:07 -070042import android.provider.ContactsContract.CommonDataKinds.Email;
Evan Millar66388be2009-05-28 15:41:07 -070043import android.provider.ContactsContract.CommonDataKinds.Phone;
44import android.provider.ContactsContract.CommonDataKinds.Photo;
Jeff Sharkey39261272009-06-03 19:15:09 -070045import android.provider.Im.PresenceColumns;
46import android.provider.SocialContract.Activities;
Jeff Sharkey80a193a2009-05-21 14:18:18 -070047import android.text.SpannableStringBuilder;
Jeff Sharkey39261272009-06-03 19:15:09 -070048import android.text.format.DateUtils;
Jeff Sharkey80a193a2009-05-21 14:18:18 -070049import android.text.style.CharacterStyle;
50import android.text.style.ForegroundColorSpan;
51import android.text.style.StyleSpan;
Jeff Sharkey3f177592009-05-18 15:23:12 -070052import android.util.Log;
Jeff Sharkey549aa162009-05-21 01:33:30 -070053import android.view.ContextThemeWrapper;
Jeff Sharkey3f177592009-05-18 15:23:12 -070054import android.view.Gravity;
Jeff Sharkey549aa162009-05-21 01:33:30 -070055import android.view.KeyEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070056import android.view.LayoutInflater;
Jeff Sharkey549aa162009-05-21 01:33:30 -070057import android.view.Menu;
58import android.view.MenuItem;
59import android.view.MotionEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070060import android.view.View;
61import android.view.ViewGroup;
Jeff Sharkey549aa162009-05-21 01:33:30 -070062import android.view.Window;
63import android.view.WindowManager;
Jeff Sharkey3f177592009-05-18 15:23:12 -070064import android.view.View.OnClickListener;
Jeff Sharkey549aa162009-05-21 01:33:30 -070065import android.view.accessibility.AccessibilityEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070066import android.widget.AbsListView;
Jeff Sharkey51ca5442009-05-21 16:12:18 -070067import android.widget.AdapterView;
68import android.widget.BaseAdapter;
Jeff Sharkey39261272009-06-03 19:15:09 -070069import android.widget.HorizontalScrollView;
Jeff Sharkey3f177592009-05-18 15:23:12 -070070import android.widget.ImageView;
71import android.widget.LinearLayout;
72import android.widget.ListView;
Jeff Sharkey39261272009-06-03 19:15:09 -070073import android.widget.RemoteViews;
Jeff Sharkey3f177592009-05-18 15:23:12 -070074import android.widget.TextView;
75import android.widget.Toast;
Jeff Sharkey3f177592009-05-18 15:23:12 -070076
Jeff Sharkey549aa162009-05-21 01:33:30 -070077import java.util.Arrays;
Jeff Sharkey3f177592009-05-18 15:23:12 -070078import java.util.HashMap;
Jeff Sharkey549aa162009-05-21 01:33:30 -070079import java.util.LinkedList;
Jeff Sharkey549aa162009-05-21 01:33:30 -070080import java.util.Set;
Jeff Sharkey3f177592009-05-18 15:23:12 -070081
82/**
Jeff Sharkey549aa162009-05-21 01:33:30 -070083 * Window that shows fast-track contact details for a specific
Jeff Sharkey39261272009-06-03 19:15:09 -070084 * {@link Aggregates#_ID}.
Jeff Sharkey3f177592009-05-18 15:23:12 -070085 */
Jeff Sharkey39261272009-06-03 19:15:09 -070086public class FastTrackWindow implements Window.Callback, QueryCompleteListener, OnClickListener,
87 AbsListView.OnItemClickListener {
Jeff Sharkey3f177592009-05-18 15:23:12 -070088 private static final String TAG = "FastTrackWindow";
89
Jeff Sharkey549aa162009-05-21 01:33:30 -070090 /**
91 * Interface used to allow the person showing a {@link FastTrackWindow} to
92 * know when the window has been dismissed.
93 */
94 interface OnDismissListener {
95 public void onDismiss(FastTrackWindow dialog);
96 }
97
98 final Context mContext;
99 final LayoutInflater mInflater;
100 final WindowManager mWindowManager;
101 Window mWindow;
102 View mDecor;
103
104 private boolean mQuerying = false;
105 private boolean mShowing = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700106
107 /** Mapping cache from mime-type to icons and actions */
108 private MappingCache mMappingCache;
109
Jeff Sharkey3f177592009-05-18 15:23:12 -0700110 private NotifyingAsyncQueryHandler mHandler;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700111 private OnDismissListener mDismissListener;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700112
Jeff Sharkey549aa162009-05-21 01:33:30 -0700113 private long mAggId;
Jeff Sharkey39261272009-06-03 19:15:09 -0700114 private Rect mAnchor;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700115
Jeff Sharkey39261272009-06-03 19:15:09 -0700116 private boolean mHasSummary = false;
117 private boolean mHasSocial = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700118 private boolean mHasActions = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700119
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700120 private View mArrowUp;
121 private View mArrowDown;
122
Jeff Sharkey549aa162009-05-21 01:33:30 -0700123 private ImageView mPhoto;
124 private ImageView mPresence;
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700125 private TextView mContent;
126 private TextView mPublished;
Jeff Sharkey39261272009-06-03 19:15:09 -0700127 private HorizontalScrollView mTrackScroll;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700128 private ViewGroup mTrack;
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700129 private ListView mResolveList;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700130
Jeff Sharkey39261272009-06-03 19:15:09 -0700131 private String mDisplayName = null;
132 private String mSocialTitle = null;
133
134 private SpannableStringBuilder mBuilder = new SpannableStringBuilder();
135 private CharacterStyle mStyleBold = new StyleSpan(android.graphics.Typeface.BOLD);
136 private CharacterStyle mStyleBlack = new ForegroundColorSpan(Color.BLACK);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700137
138 /**
139 * Set of {@link ActionInfo} that are associated with the aggregate
Jeff Sharkey39261272009-06-03 19:15:09 -0700140 * currently displayed by this fast-track window, represented as a map from
141 * {@link String} mimetype to {@link ActionList}.
Jeff Sharkey549aa162009-05-21 01:33:30 -0700142 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700143 private ActionMap mActions = new ActionMap();
Jeff Sharkey549aa162009-05-21 01:33:30 -0700144
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[] {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700156 Phones.CONTENT_ITEM_TYPE,
Jeff Sharkey39261272009-06-03 19:15:09 -0700157 Aggregates.CONTENT_ITEM_TYPE,
Jeff Sharkey549aa162009-05-21 01:33:30 -0700158 MIME_SMS_ADDRESS,
159 Email.CONTENT_ITEM_TYPE,
160 };
161
Jeff Sharkey549aa162009-05-21 01:33:30 -0700162 private static final boolean INCLUDE_PROFILE_ACTION = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700163
Jeff Sharkey39261272009-06-03 19:15:09 -0700164 private static final int TOKEN_SUMMARY = 1;
165 private static final int TOKEN_SOCIAL = 2;
166 private static final int TOKEN_DATA = 3;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700167
Jeff Sharkey3f177592009-05-18 15:23:12 -0700168 /** Message to show when no activity is found to perform an action */
169 // TODO: move this value into a resources string
170 private static final String NOT_FOUND = "Couldn't find an app to handle this action";
171
Jeff Sharkey549aa162009-05-21 01:33:30 -0700172 /**
173 * Prepare a fast-track window to show in the given {@link Context}.
174 */
175 public FastTrackWindow(Context context) {
176 mContext = new ContextThemeWrapper(context, R.style.FastTrack);
177 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
178 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700179
Jeff Sharkey549aa162009-05-21 01:33:30 -0700180 mWindow = PolicyManager.makeNewWindow(mContext);
181 mWindow.setCallback(this);
182 mWindow.setWindowManager(mWindowManager, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700183
Jeff Sharkey549aa162009-05-21 01:33:30 -0700184 mWindow.setContentView(R.layout.fasttrack);
185
Jeff Sharkey39261272009-06-03 19:15:09 -0700186 mArrowUp = mWindow.findViewById(R.id.arrow_up);
187 mArrowDown = mWindow.findViewById(R.id.arrow_down);
188
Jeff Sharkey549aa162009-05-21 01:33:30 -0700189 mPhoto = (ImageView)mWindow.findViewById(R.id.photo);
190 mPresence = (ImageView)mWindow.findViewById(R.id.presence);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700191 mContent = (TextView)mWindow.findViewById(R.id.content);
192 mPublished = (TextView)mWindow.findViewById(R.id.published);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700193 mTrack = (ViewGroup)mWindow.findViewById(R.id.fasttrack);
Jeff Sharkey39261272009-06-03 19:15:09 -0700194 mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700195 mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700196
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);
Jeff Sharkey39261272009-06-03 19:15:09 -0700228 mapping.summaryColumn = Phone.TYPE;
229 mapping.detailColumn = Phone.NUMBER;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700230 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_call);
231 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700232
Jeff Sharkey549aa162009-05-21 01:33:30 -0700233 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, MIME_SMS_ADDRESS);
Jeff Sharkey39261272009-06-03 19:15:09 -0700234 mapping.summaryColumn = Phone.TYPE;
235 mapping.detailColumn = Phone.NUMBER;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700236 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.sym_action_sms);
237 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700238
Jeff Sharkey549aa162009-05-21 01:33:30 -0700239 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Email.CONTENT_ITEM_TYPE);
Jeff Sharkey39261272009-06-03 19:15:09 -0700240 mapping.summaryColumn = Email.TYPE;
241 mapping.detailColumn = Email.DATA;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700242 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 Sharkey39261272009-06-03 19:15:09 -0700251 public void show(Uri aggUri, Rect anchor) {
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 Sharkey39261272009-06-03 19:15:09 -0700258 mAnchor = new Rect(anchor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700259 mQuerying = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700260
Jeff Sharkey39261272009-06-03 19:15:09 -0700261 Uri aggSummary = ContentUris.withAppendedId(
262 ContactsContract.Aggregates.CONTENT_SUMMARY_URI, mAggId);
263 Uri aggSocial = ContentUris.withAppendedId(
264 SocialContract.Activities.CONTENT_AGGREGATE_STATUS_URI, mAggId);
265 Uri aggData = Uri.withAppendedPath(aggUri,
Jeff Sharkey549aa162009-05-21 01:33:30 -0700266 ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700267
Jeff Sharkey39261272009-06-03 19:15:09 -0700268 // Start data query in background
Jeff Sharkey549aa162009-05-21 01:33:30 -0700269 mHandler = new NotifyingAsyncQueryHandler(mContext, this);
Jeff Sharkey39261272009-06-03 19:15:09 -0700270 mHandler.startQuery(TOKEN_SUMMARY, null, aggSummary, null, null, null, null);
271 mHandler.startQuery(TOKEN_SOCIAL, null, aggSocial, null, null, null, null);
272 mHandler.startQuery(TOKEN_DATA, null, aggData, null, null, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700273
274 }
275
276 /**
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700277 * Show the correct callout arrow based on a {@link R.id} reference.
278 */
279 private void showArrow(int whichArrow, int requestedX) {
280 final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
281 final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
Jeff Sharkey39261272009-06-03 19:15:09 -0700282
283 final int arrowWidth = mArrowUp.getMeasuredWidth();
284
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700285 showArrow.setVisibility(View.VISIBLE);
286 LinearLayout.LayoutParams param = (LinearLayout.LayoutParams)showArrow.getLayoutParams();
Jeff Sharkey39261272009-06-03 19:15:09 -0700287 param.leftMargin = requestedX - arrowWidth / 2;
288
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700289 hideArrow.setVisibility(View.INVISIBLE);
290 }
291
292 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700293 * Actual internal method to show this fast-track window. Called only by
294 * {@link #considerShowing()} when all data requirements have been met.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700295 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700296 private void showInternal() {
297 mDecor = mWindow.getDecorView();
298 WindowManager.LayoutParams l = mWindow.getAttributes();
Jeff Sharkey39261272009-06-03 19:15:09 -0700299
300 l.width = WindowManager.LayoutParams.FILL_PARENT;
301 l.height = WindowManager.LayoutParams.WRAP_CONTENT;
302
303 // Force layout measuring pass so we have baseline numbers
304 mDecor.measure(l.width, l.height);
305
306 final int blockHeight = mDecor.getMeasuredHeight();
307 final int arrowHeight = mArrowUp.getHeight();
308
Jeff Sharkey549aa162009-05-21 01:33:30 -0700309 l.gravity = Gravity.TOP | Gravity.LEFT;
310 l.x = 0;
Jeff Sharkey39261272009-06-03 19:15:09 -0700311
312 if (mAnchor.top > blockHeight) {
313 // Show downwards callout when enough room, aligning bottom block
314 // edge with top of anchor area, and adjusting to inset arrow.
315 showArrow(R.id.arrow_down, mAnchor.centerX());
316 l.y = mAnchor.top - blockHeight + arrowHeight;
317
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700318 } else {
Jeff Sharkey39261272009-06-03 19:15:09 -0700319 // Otherwise show upwards callout, aligning block top with bottom of
320 // anchor area, and adjusting to inset arrow.
321 showArrow(R.id.arrow_up, mAnchor.centerX());
322 l.y = mAnchor.bottom - arrowHeight;
323
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700324 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700325
326 l.dimAmount = 0.6f;
327 l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND
328 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
Jeff Sharkey39261272009-06-03 19:15:09 -0700329 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
Jeff Sharkey549aa162009-05-21 01:33:30 -0700330 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
331 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
332
333 mWindowManager.addView(mDecor, l);
334 mShowing = true;
335 mQuerying = false;
336 }
337
338 /**
339 * Dismiss this fast-track window if showing.
340 */
341 public void dismiss() {
342 if (!mQuerying && !mShowing) {
343 Log.d(TAG, "not visible, ignore");
344 return;
345 }
346
347 // Cancel any pending queries
Jeff Sharkey39261272009-06-03 19:15:09 -0700348 mHandler.cancelOperation(TOKEN_SUMMARY);
349 mHandler.cancelOperation(TOKEN_SOCIAL);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700350 mHandler.cancelOperation(TOKEN_DATA);
351
352 // Reset all views to prepare for possible recycling
353 mPhoto.setImageResource(R.drawable.ic_contact_list_picture);
Jeff Sharkey39261272009-06-03 19:15:09 -0700354 mPresence.setImageDrawable(null);
355 mPublished.setText(null);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700356 mContent.setText(null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700357
Jeff Sharkey39261272009-06-03 19:15:09 -0700358 mDisplayName = null;
359 mSocialTitle = null;
360
361 // Clear track actions and scroll to hard left
Jeff Sharkey549aa162009-05-21 01:33:30 -0700362 mActions.clear();
363 mTrack.removeAllViews();
Jeff Sharkey39261272009-06-03 19:15:09 -0700364 mTrackScroll.fullScroll(View.FOCUS_LEFT);
365 mWasDownArrow = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700366
Jeff Sharkey39261272009-06-03 19:15:09 -0700367 showResolveList(View.GONE);
368
369 mHasSocial = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700370 mHasActions = false;
371
372 if (mDecor == null || !mShowing) {
373 Log.d(TAG, "not showing, ignore");
374 return;
375 }
376
377 mWindowManager.removeView(mDecor);
378 mDecor = null;
379 mWindow.closeAllPanels();
380 mShowing = false;
381
382 // Notify any listeners that we've been dismissed
383 if (mDismissListener != null) {
384 mDismissListener.onDismiss(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700385 }
386 }
387
Jeff Sharkey549aa162009-05-21 01:33:30 -0700388 /**
389 * Returns true if this fast-track window is showing or querying.
390 */
391 public boolean isShowing() {
392 return mShowing || mQuerying;
393 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700394
Jeff Sharkey549aa162009-05-21 01:33:30 -0700395 /**
396 * Consider showing this window, which will only call through to
397 * {@link #showInternal()} when all data items are present.
398 */
399 private synchronized void considerShowing() {
Jeff Sharkey39261272009-06-03 19:15:09 -0700400 if (mHasSummary && mHasSocial && mHasActions && !mShowing) {
401 // Now that all queries have been finished, build summary string.
402 mBuilder.clear();
403 mBuilder.append(mDisplayName);
404 mBuilder.append(" ");
405 mBuilder.append(mSocialTitle);
406 mBuilder.setSpan(mStyleBold, 0, mDisplayName.length(), 0);
407 mBuilder.setSpan(mStyleBlack, 0, mDisplayName.length(), 0);
408 mContent.setText(mBuilder);
409
Jeff Sharkey549aa162009-05-21 01:33:30 -0700410 showInternal();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700411 }
412 }
413
414 /** {@inheritDoc} */
415 public void onQueryComplete(int token, Object cookie, Cursor cursor) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700416 if (cursor == null) {
417 return;
Jeff Sharkey39261272009-06-03 19:15:09 -0700418 } else if (token == TOKEN_SUMMARY) {
419 handleSummary(cursor);
420 } else if (token == TOKEN_SOCIAL) {
421 handleSocial(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700422 } else if (token == TOKEN_DATA) {
423 handleData(cursor);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700424 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700425 }
426
427 /**
Jeff Sharkey39261272009-06-03 19:15:09 -0700428 * Handle the result from the {@link TOKEN_SUMMARY} query.
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700429 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700430 private void handleSummary(Cursor cursor) {
431 final int colDisplayName = cursor.getColumnIndex(Aggregates.DISPLAY_NAME);
432 final int colStatus = cursor.getColumnIndex(PresenceColumns.PRESENCE_STATUS);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700433
434 if (cursor.moveToNext()) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700435 mDisplayName = cursor.getString(colDisplayName);
436
437 int status = cursor.getInt(colStatus);
438 int statusRes = Presence.getPresenceIconResourceId(status);
439 mPresence.setImageResource(statusRes);
440 }
441
442 mHasSummary = true;
443 considerShowing();
444 }
445
446 /**
447 * Handle the result from the {@link TOKEN_SOCIAL} query.
448 */
449 private void handleSocial(Cursor cursor) {
450 final int colTitle = cursor.getColumnIndex(Activities.TITLE);
451 final int colPublished = cursor.getColumnIndex(Activities.PUBLISHED);
452
453 if (cursor.moveToNext()) {
454 mSocialTitle = cursor.getString(colTitle);
455
456 long published = cursor.getLong(colPublished);
457 CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
458 System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
459 mPublished.setText(relativePublished);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700460
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700461 }
462
Jeff Sharkey39261272009-06-03 19:15:09 -0700463 mHasSocial = true;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700464 considerShowing();
465 }
466
467 /**
468 * Description of a specific, actionable {@link Data#_ID} item. May have a
469 * {@link Mapping} associated with it to find {@link RemoteViews} or icon,
470 * and may have built a summary of itself for UI display.
471 */
472 private class ActionInfo {
473 long dataId;
474 String packageName;
475 String mimeType;
476
477 Mapping mapping;
Jeff Sharkey39261272009-06-03 19:15:09 -0700478 CharSequence summaryValue;
479 String detailValue;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700480
481 /**
482 * Create an action from common {@link Data} elements.
483 */
484 public ActionInfo(long dataId, String packageName, String mimeType) {
485 this.dataId = dataId;
486 this.packageName = packageName;
487 this.mimeType = mimeType;
488 }
489
490 /**
491 * Attempt to find a {@link Mapping} for the package and mime-type
492 * defined by this action. Returns true if one was found.
493 */
494 public boolean findMapping(MappingCache cache) {
495 mapping = cache.findMapping(packageName, mimeType);
496 return (mapping != null);
497 }
498
499 /**
500 * Given a {@link Cursor} pointed at the {@link Data} row associated
501 * with this action, use the {@link Mapping} to build a text summary.
502 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700503 public void buildDetails(Cursor cursor) {
504 if (mapping == null) return;
505
506 // Try finding common display label for this item, otherwise fall
507 // back to reading from defined summary column.
508 summaryValue = ContactsUtils.getDisplayLabel(mContext, mimeType, cursor);
509 if (summaryValue == null && mapping.summaryColumn != null) {
510 summaryValue = cursor.getString(cursor.getColumnIndex(mapping.summaryColumn));
511 }
512
513 // Read detailed value, if mapping was defined
514 if (mapping.detailColumn != null) {
515 detailValue = cursor.getString(cursor.getColumnIndex(mapping.detailColumn));
Jeff Sharkey549aa162009-05-21 01:33:30 -0700516 }
517 }
518
519 /**
520 * Build an {@link Intent} that will perform this action.
521 */
522 public Intent buildIntent() {
523 // Handle well-known mime-types with special care
524 if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700525 Uri callUri = Uri.parse("tel:" + Uri.encode(detailValue));
Jeff Sharkey549aa162009-05-21 01:33:30 -0700526 return new Intent(Intent.ACTION_DIAL, callUri);
527
528 } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700529 Uri smsUri = Uri.fromParts("smsto", detailValue, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700530 return new Intent(Intent.ACTION_SENDTO, smsUri);
531
532 } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700533 Uri mailUri = Uri.fromParts("mailto", detailValue, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700534 return new Intent(Intent.ACTION_SENDTO, mailUri);
535
536 }
537
538 // Otherwise fall back to default VIEW action
539 Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
540
541 Intent intent = new Intent(Intent.ACTION_VIEW);
542 intent.setData(dataUri);
543
544 return intent;
545 }
546 }
547
548 /**
Jeff Sharkey39261272009-06-03 19:15:09 -0700549 * Provide a strongly-typed {@link LinkedList} that holds a list of
550 * {@link ActionInfo} objects.
551 */
552 private class ActionList extends LinkedList<ActionInfo> {
553 }
554
555 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700556 * Provide a simple way of collecting one or more {@link ActionInfo} objects
557 * under a mime-type key.
558 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700559 private class ActionMap extends HashMap<String, ActionList> {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700560 private void collect(String mimeType, ActionInfo info) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700561 // Create list for this mimetype when needed
562 ActionList collectList = get(mimeType);
563 if (collectList == null) {
564 collectList = new ActionList();
565 put(mimeType, collectList);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700566 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700567 collectList.add(info);
568 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700569 }
570
571 /**
572 * Handle the result from the {@link TOKEN_DATA} query.
573 */
574 private void handleData(Cursor cursor) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700575 final int colId = cursor.getColumnIndex(Data._ID);
576 final int colPackage = cursor.getColumnIndex(Data.PACKAGE);
577 final int colMimeType = cursor.getColumnIndex(Data.MIMETYPE);
578 final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700579
Jeff Sharkey549aa162009-05-21 01:33:30 -0700580 ActionInfo info;
581
582 // Add the profile shortcut action if requested
583 if (INCLUDE_PROFILE_ACTION) {
584 final String mimeType = Aggregates.CONTENT_ITEM_TYPE;
585 info = new ActionInfo(mAggId, CommonDataKinds.PACKAGE_COMMON, mimeType);
586 if (info.findMapping(mMappingCache)) {
587 mActions.collect(mimeType, info);
588 }
589 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700590
591 while (cursor.moveToNext()) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700592 final long dataId = cursor.getLong(colId);
593 final String packageName = cursor.getString(colPackage);
594 final String mimeType = cursor.getString(colMimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700595
Jeff Sharkey549aa162009-05-21 01:33:30 -0700596 // Handle when a photo appears in the various data items
597 // TODO: accept a photo only if its marked as primary
598 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700599 byte[] photoBlob = cursor.getBlob(colPhoto);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700600 Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700601 mPhoto.setImageBitmap(photoBitmap);
Jeff Sharkey69126682009-05-18 21:33:41 -0700602 continue;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700603 }
604
Jeff Sharkey549aa162009-05-21 01:33:30 -0700605 // Build an action for this data entry, find a mapping to a UI
606 // element, build its summary from the cursor, and collect it along
607 // with all others of this mime-type.
608 info = new ActionInfo(dataId, packageName, mimeType);
609 if (info.findMapping(mMappingCache)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700610 info.buildDetails(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700611 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700612 }
613
Jeff Sharkey549aa162009-05-21 01:33:30 -0700614 // If phone number, also insert as text message action
615 if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) {
616 info = new ActionInfo(dataId, packageName, MIME_SMS_ADDRESS);
617 if (info.findMapping(mMappingCache)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700618 info.buildDetails(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700619 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700620 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700621 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700622 }
623
624 cursor.close();
625
Jeff Sharkey549aa162009-05-21 01:33:30 -0700626 // Turn our list of actions into UI elements, starting with common types
627 Set<String> containedTypes = mActions.keySet();
628 for (String mimeType : ORDERED_MIMETYPES) {
629 if (containedTypes.contains(mimeType)) {
630 mTrack.addView(inflateAction(mimeType));
631 containedTypes.remove(mimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700632 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700633 }
634
Jeff Sharkey549aa162009-05-21 01:33:30 -0700635 // Then continue with remaining mime-types in alphabetical order
636 String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
637 Arrays.sort(remainingTypes);
638 for (String mimeType : remainingTypes) {
639 mTrack.addView(inflateAction(mimeType));
640 }
641
642 mHasActions = true;
643 considerShowing();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700644 }
645
646 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700647 * Inflate the in-track view for the action of the given mime-type. Will use
648 * the icon provided by the {@link Mapping}.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700649 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700650 private View inflateAction(String mimeType) {
651 ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700652
Jeff Sharkey549aa162009-05-21 01:33:30 -0700653 // Add direct intent if single child, otherwise flag for multiple
Jeff Sharkey39261272009-06-03 19:15:09 -0700654 ActionList children = mActions.get(mimeType);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700655 ActionInfo firstInfo = children.get(0);
Jeff Sharkey39261272009-06-03 19:15:09 -0700656 if (children.size() == 1) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700657 view.setTag(firstInfo.buildIntent());
Jeff Sharkey39261272009-06-03 19:15:09 -0700658 } else {
659 view.setTag(children);
660 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700661
Jeff Sharkey549aa162009-05-21 01:33:30 -0700662 // Set icon and listen for clicks
663 view.setImageBitmap(firstInfo.mapping.icon);
664 view.setOnClickListener(this);
665 return view;
666 }
Jeff Sharkey39261272009-06-03 19:15:09 -0700667
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700668 /** {@inheritDoc} */
669 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
670 // Pass list item clicks along so that Intents are handled uniformly
671 onClick(view);
672 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700673
Jeff Sharkey39261272009-06-03 19:15:09 -0700674 /**
675 * Flag indicating if {@link #mArrowDown} was visible during the last call
676 * to {@link #showResolveList(int)}. Used to decide during a later call if
677 * the arrow should be restored.
678 */
679 private boolean mWasDownArrow = false;
680
681 /**
682 * Helper for showing and hiding {@link #mResolveList}, which will correctly
683 * manage {@link #mArrowDown} as needed.
684 */
685 private void showResolveList(int visibility) {
686 // Show or hide the resolve list if needed
687 if (mResolveList.getVisibility() != visibility) {
688 mResolveList.setVisibility(visibility);
689 }
690
691 if (visibility == View.VISIBLE) {
692 // If showing list, then hide and save state of down arrow
693 mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
694 mArrowDown.setVisibility(View.INVISIBLE);
695 } else {
696 // If hiding list, restore any down arrow state
697 mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
698 }
699
700 }
701
Jeff Sharkey549aa162009-05-21 01:33:30 -0700702 /** {@inheritDoc} */
703 public void onClick(View v) {
704 final Object tag = v.getTag();
705 if (tag instanceof Intent) {
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700706 // Hide the resolution list, if present
Jeff Sharkey39261272009-06-03 19:15:09 -0700707 showResolveList(View.GONE);
708
Jeff Sharkey549aa162009-05-21 01:33:30 -0700709 // Incoming tag is concrete intent, so launch
710 try {
711 mContext.startActivity((Intent)tag);
712 } catch (ActivityNotFoundException e) {
713 Log.w(TAG, NOT_FOUND);
714 Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
715 }
Jeff Sharkey39261272009-06-03 19:15:09 -0700716 } else if (tag instanceof ActionList) {
717 // Incoming tag is a mime-type, so show resolution list
718 final ActionList children = (ActionList)tag;
719
720 // Show resolution list and set adapter
721 showResolveList(View.VISIBLE);
722
723 mResolveList.setOnItemClickListener(this);
724 mResolveList.setAdapter(new BaseAdapter() {
725 public int getCount() {
726 return children.size();
727 }
728
729 public Object getItem(int position) {
730 return children.get(position);
731 }
732
733 public long getItemId(int position) {
734 return position;
735 }
736
737 public View getView(int position, View convertView, ViewGroup parent) {
738 if (convertView == null) {
739 convertView = mInflater.inflate(R.layout.fasttrack_resolve_item, parent, false);
740 }
741
742 // Set action title based on summary value
743 ActionInfo info = (ActionInfo)getItem(position);
744
745 ImageView icon1 = (ImageView)convertView.findViewById(android.R.id.icon1);
746 TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
747 TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
748
749 icon1.setImageBitmap(info.mapping.icon);
750 text1.setText(info.summaryValue);
751 text2.setText(info.detailValue);
752
753 convertView.setTag(info.buildIntent());
754 return convertView;
755 }
756 });
757
758 // Make sure we resize to make room for ListView
759 onWindowAttributesChanged(mWindow.getAttributes());
Jeff Sharkey549aa162009-05-21 01:33:30 -0700760
761 }
762 }
763
764 /** {@inheritDoc} */
765 public boolean dispatchKeyEvent(KeyEvent event) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700766 if (event.getAction() == KeyEvent.ACTION_DOWN
767 && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
768 // Back key will first dismiss any expanded resolve list, otherwise
769 // it will close the entire dialog.
770 if (mResolveList.getVisibility() == View.VISIBLE) {
771 showResolveList(View.GONE);
772 } else {
773 dismiss();
774 }
775 return true;
776 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700777 return mWindow.superDispatchKeyEvent(event);
778 }
779
780 /** {@inheritDoc} */
781 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
782 // TODO: make this window accessible
783 return false;
784 }
785
786 /** {@inheritDoc} */
787 public boolean dispatchTouchEvent(MotionEvent event) {
788 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
789 dismiss();
790 return true;
791 }
792 return mWindow.superDispatchTouchEvent(event);
793 }
794
795 /** {@inheritDoc} */
796 public boolean dispatchTrackballEvent(MotionEvent event) {
797 return mWindow.superDispatchTrackballEvent(event);
798 }
799
800 /** {@inheritDoc} */
801 public void onContentChanged() {
802 }
803
804 /** {@inheritDoc} */
805 public boolean onCreatePanelMenu(int featureId, Menu menu) {
806 return false;
807 }
808
809 /** {@inheritDoc} */
810 public View onCreatePanelView(int featureId) {
811 return null;
812 }
813
814 /** {@inheritDoc} */
815 public boolean onMenuItemSelected(int featureId, MenuItem item) {
816 return false;
817 }
818
819 /** {@inheritDoc} */
820 public boolean onMenuOpened(int featureId, Menu menu) {
821 return false;
822 }
823
824 /** {@inheritDoc} */
825 public void onPanelClosed(int featureId, Menu menu) {
826 }
827
828 /** {@inheritDoc} */
829 public boolean onPreparePanel(int featureId, View view, Menu menu) {
830 return false;
831 }
832
833 /** {@inheritDoc} */
834 public boolean onSearchRequested() {
835 return false;
836 }
837
838 /** {@inheritDoc} */
839 public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
840 if (mDecor != null) {
841 mWindowManager.updateViewLayout(mDecor, attrs);
842 }
843 }
844
845 /** {@inheritDoc} */
846 public void onWindowFocusChanged(boolean hasFocus) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700847 }
848}