blob: f206dd1c87002851e271b2377a0609cfb8a45d69 [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
Evan Millar54a5c9f2009-06-23 17:41:09 -0700145 // TODO We should move this to someplace more general as it is needed in a few places in the app
146 // code.
Jeff Sharkey549aa162009-05-21 01:33:30 -0700147 /**
148 * Specific mime-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
149 * distinguishes actions that should initiate a text message.
150 */
151 public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
152
153 /**
154 * Specific mime-types that should be bumped to the front of the fast-track.
155 * Other mime-types not appearing in this list follow in alphabetic order.
156 */
157 private static final String[] ORDERED_MIMETYPES = new String[] {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700158 Phones.CONTENT_ITEM_TYPE,
Jeff Sharkey39261272009-06-03 19:15:09 -0700159 Aggregates.CONTENT_ITEM_TYPE,
Jeff Sharkey549aa162009-05-21 01:33:30 -0700160 MIME_SMS_ADDRESS,
161 Email.CONTENT_ITEM_TYPE,
162 };
163
Jeff Sharkey549aa162009-05-21 01:33:30 -0700164 private static final boolean INCLUDE_PROFILE_ACTION = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700165
Jeff Sharkey39261272009-06-03 19:15:09 -0700166 private static final int TOKEN_SUMMARY = 1;
167 private static final int TOKEN_SOCIAL = 2;
168 private static final int TOKEN_DATA = 3;
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 Sharkey39261272009-06-03 19:15:09 -0700188 mArrowUp = mWindow.findViewById(R.id.arrow_up);
189 mArrowDown = 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);
Jeff Sharkey39261272009-06-03 19:15:09 -0700196 mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700197 mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700198
199 // TODO: move generation of mime-type cache to more-efficient place
200 generateMappingCache();
201
Jeff Sharkey3f177592009-05-18 15:23:12 -0700202 }
203
204 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700205 * Prepare a fast-track window to show in the given {@link Context}, and
206 * notify the given {@link OnDismissListener} each time this dialog is
207 * dismissed.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700208 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700209 public FastTrackWindow(Context context, OnDismissListener dismissListener) {
210 this(context);
211 mDismissListener = dismissListener;
212 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700213
Jeff Sharkey549aa162009-05-21 01:33:30 -0700214 /**
215 * Generate {@link MappingCache} specifically for fast-track windows. This
216 * cache knows how to display {@link CommonDataKinds#PACKAGE_COMMON} data
217 * types using generic icons.
218 */
219 private void generateMappingCache() {
220 mMappingCache = MappingCache.createAndFill(mContext);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700221
Jeff Sharkey549aa162009-05-21 01:33:30 -0700222 Resources res = mContext.getResources();
223 Mapping mapping;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700224
Jeff Sharkey549aa162009-05-21 01:33:30 -0700225 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Aggregates.CONTENT_ITEM_TYPE);
226 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.ic_contacts_details);
227 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700228
Jeff Sharkey549aa162009-05-21 01:33:30 -0700229 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Phone.CONTENT_ITEM_TYPE);
Jeff Sharkey39261272009-06-03 19:15:09 -0700230 mapping.summaryColumn = Phone.TYPE;
231 mapping.detailColumn = Phone.NUMBER;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700232 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);
Jeff Sharkey39261272009-06-03 19:15:09 -0700236 mapping.summaryColumn = Phone.TYPE;
237 mapping.detailColumn = Phone.NUMBER;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700238 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.sym_action_sms);
239 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700240
Jeff Sharkey549aa162009-05-21 01:33:30 -0700241 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Email.CONTENT_ITEM_TYPE);
Jeff Sharkey39261272009-06-03 19:15:09 -0700242 mapping.summaryColumn = Email.TYPE;
243 mapping.detailColumn = Email.DATA;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700244 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_email);
245 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700246
Jeff Sharkey549aa162009-05-21 01:33:30 -0700247 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700248
Jeff Sharkey549aa162009-05-21 01:33:30 -0700249 /**
250 * Start showing a fast-track window for the given {@link Aggregate#_ID}
251 * pointing towards the given location.
252 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700253 public void show(Uri aggUri, Rect anchor) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700254 if (mShowing || mQuerying) {
255 Log.w(TAG, "already in process of showing");
256 return;
257 }
258
259 mAggId = ContentUris.parseId(aggUri);
Jeff Sharkey39261272009-06-03 19:15:09 -0700260 mAnchor = new Rect(anchor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700261 mQuerying = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700262
Jeff Sharkey39261272009-06-03 19:15:09 -0700263 Uri aggSummary = ContentUris.withAppendedId(
264 ContactsContract.Aggregates.CONTENT_SUMMARY_URI, mAggId);
265 Uri aggSocial = ContentUris.withAppendedId(
266 SocialContract.Activities.CONTENT_AGGREGATE_STATUS_URI, mAggId);
267 Uri aggData = Uri.withAppendedPath(aggUri,
Jeff Sharkey549aa162009-05-21 01:33:30 -0700268 ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700269
Jeff Sharkey39261272009-06-03 19:15:09 -0700270 // Start data query in background
Jeff Sharkey549aa162009-05-21 01:33:30 -0700271 mHandler = new NotifyingAsyncQueryHandler(mContext, this);
Jeff Sharkey39261272009-06-03 19:15:09 -0700272 mHandler.startQuery(TOKEN_SUMMARY, null, aggSummary, null, null, null, null);
273 mHandler.startQuery(TOKEN_SOCIAL, null, aggSocial, null, null, null, null);
274 mHandler.startQuery(TOKEN_DATA, null, aggData, null, null, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700275
276 }
277
278 /**
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700279 * Show the correct callout arrow based on a {@link R.id} reference.
280 */
281 private void showArrow(int whichArrow, int requestedX) {
282 final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
283 final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
Jeff Sharkey39261272009-06-03 19:15:09 -0700284
285 final int arrowWidth = mArrowUp.getMeasuredWidth();
286
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700287 showArrow.setVisibility(View.VISIBLE);
288 LinearLayout.LayoutParams param = (LinearLayout.LayoutParams)showArrow.getLayoutParams();
Jeff Sharkey39261272009-06-03 19:15:09 -0700289 param.leftMargin = requestedX - arrowWidth / 2;
290
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700291 hideArrow.setVisibility(View.INVISIBLE);
292 }
293
294 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700295 * Actual internal method to show this fast-track window. Called only by
296 * {@link #considerShowing()} when all data requirements have been met.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700297 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700298 private void showInternal() {
299 mDecor = mWindow.getDecorView();
300 WindowManager.LayoutParams l = mWindow.getAttributes();
Jeff Sharkey39261272009-06-03 19:15:09 -0700301
302 l.width = WindowManager.LayoutParams.FILL_PARENT;
303 l.height = WindowManager.LayoutParams.WRAP_CONTENT;
304
305 // Force layout measuring pass so we have baseline numbers
306 mDecor.measure(l.width, l.height);
307
308 final int blockHeight = mDecor.getMeasuredHeight();
309 final int arrowHeight = mArrowUp.getHeight();
310
Jeff Sharkey549aa162009-05-21 01:33:30 -0700311 l.gravity = Gravity.TOP | Gravity.LEFT;
312 l.x = 0;
Jeff Sharkey39261272009-06-03 19:15:09 -0700313
314 if (mAnchor.top > blockHeight) {
315 // Show downwards callout when enough room, aligning bottom block
316 // edge with top of anchor area, and adjusting to inset arrow.
317 showArrow(R.id.arrow_down, mAnchor.centerX());
318 l.y = mAnchor.top - blockHeight + arrowHeight;
319
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700320 } else {
Jeff Sharkey39261272009-06-03 19:15:09 -0700321 // Otherwise show upwards callout, aligning block top with bottom of
322 // anchor area, and adjusting to inset arrow.
323 showArrow(R.id.arrow_up, mAnchor.centerX());
324 l.y = mAnchor.bottom - arrowHeight;
325
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700326 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700327
328 l.dimAmount = 0.6f;
329 l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND
330 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
Jeff Sharkey39261272009-06-03 19:15:09 -0700331 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
Jeff Sharkey549aa162009-05-21 01:33:30 -0700332 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
333 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
334
335 mWindowManager.addView(mDecor, l);
336 mShowing = true;
337 mQuerying = false;
338 }
339
340 /**
341 * Dismiss this fast-track window if showing.
342 */
343 public void dismiss() {
344 if (!mQuerying && !mShowing) {
345 Log.d(TAG, "not visible, ignore");
346 return;
347 }
348
349 // Cancel any pending queries
Jeff Sharkey39261272009-06-03 19:15:09 -0700350 mHandler.cancelOperation(TOKEN_SUMMARY);
351 mHandler.cancelOperation(TOKEN_SOCIAL);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700352 mHandler.cancelOperation(TOKEN_DATA);
353
354 // Reset all views to prepare for possible recycling
355 mPhoto.setImageResource(R.drawable.ic_contact_list_picture);
Jeff Sharkey39261272009-06-03 19:15:09 -0700356 mPresence.setImageDrawable(null);
357 mPublished.setText(null);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700358 mContent.setText(null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700359
Jeff Sharkey39261272009-06-03 19:15:09 -0700360 mDisplayName = null;
361 mSocialTitle = null;
362
363 // Clear track actions and scroll to hard left
Jeff Sharkey549aa162009-05-21 01:33:30 -0700364 mActions.clear();
365 mTrack.removeAllViews();
Jeff Sharkey39261272009-06-03 19:15:09 -0700366 mTrackScroll.fullScroll(View.FOCUS_LEFT);
367 mWasDownArrow = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700368
Jeff Sharkey39261272009-06-03 19:15:09 -0700369 showResolveList(View.GONE);
370
371 mHasSocial = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700372 mHasActions = false;
373
374 if (mDecor == null || !mShowing) {
375 Log.d(TAG, "not showing, ignore");
376 return;
377 }
378
379 mWindowManager.removeView(mDecor);
380 mDecor = null;
381 mWindow.closeAllPanels();
382 mShowing = false;
383
384 // Notify any listeners that we've been dismissed
385 if (mDismissListener != null) {
386 mDismissListener.onDismiss(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700387 }
388 }
389
Jeff Sharkey549aa162009-05-21 01:33:30 -0700390 /**
391 * Returns true if this fast-track window is showing or querying.
392 */
393 public boolean isShowing() {
394 return mShowing || mQuerying;
395 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700396
Jeff Sharkey549aa162009-05-21 01:33:30 -0700397 /**
398 * Consider showing this window, which will only call through to
399 * {@link #showInternal()} when all data items are present.
400 */
401 private synchronized void considerShowing() {
Jeff Sharkey39261272009-06-03 19:15:09 -0700402 if (mHasSummary && mHasSocial && mHasActions && !mShowing) {
403 // Now that all queries have been finished, build summary string.
404 mBuilder.clear();
405 mBuilder.append(mDisplayName);
406 mBuilder.append(" ");
407 mBuilder.append(mSocialTitle);
408 mBuilder.setSpan(mStyleBold, 0, mDisplayName.length(), 0);
409 mBuilder.setSpan(mStyleBlack, 0, mDisplayName.length(), 0);
410 mContent.setText(mBuilder);
411
Jeff Sharkey549aa162009-05-21 01:33:30 -0700412 showInternal();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700413 }
414 }
415
416 /** {@inheritDoc} */
417 public void onQueryComplete(int token, Object cookie, Cursor cursor) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700418 if (cursor == null) {
419 return;
Jeff Sharkey39261272009-06-03 19:15:09 -0700420 } else if (token == TOKEN_SUMMARY) {
421 handleSummary(cursor);
422 } else if (token == TOKEN_SOCIAL) {
423 handleSocial(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700424 } else if (token == TOKEN_DATA) {
425 handleData(cursor);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700426 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700427 }
428
429 /**
Jeff Sharkey39261272009-06-03 19:15:09 -0700430 * Handle the result from the {@link TOKEN_SUMMARY} query.
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700431 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700432 private void handleSummary(Cursor cursor) {
433 final int colDisplayName = cursor.getColumnIndex(Aggregates.DISPLAY_NAME);
434 final int colStatus = cursor.getColumnIndex(PresenceColumns.PRESENCE_STATUS);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700435
436 if (cursor.moveToNext()) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700437 mDisplayName = cursor.getString(colDisplayName);
438
439 int status = cursor.getInt(colStatus);
440 int statusRes = Presence.getPresenceIconResourceId(status);
441 mPresence.setImageResource(statusRes);
442 }
443
444 mHasSummary = true;
445 considerShowing();
446 }
447
448 /**
449 * Handle the result from the {@link TOKEN_SOCIAL} query.
450 */
451 private void handleSocial(Cursor cursor) {
452 final int colTitle = cursor.getColumnIndex(Activities.TITLE);
453 final int colPublished = cursor.getColumnIndex(Activities.PUBLISHED);
454
455 if (cursor.moveToNext()) {
456 mSocialTitle = cursor.getString(colTitle);
457
458 long published = cursor.getLong(colPublished);
459 CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
460 System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
461 mPublished.setText(relativePublished);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700462
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700463 }
464
Jeff Sharkey39261272009-06-03 19:15:09 -0700465 mHasSocial = true;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700466 considerShowing();
467 }
468
469 /**
470 * Description of a specific, actionable {@link Data#_ID} item. May have a
471 * {@link Mapping} associated with it to find {@link RemoteViews} or icon,
472 * and may have built a summary of itself for UI display.
473 */
474 private class ActionInfo {
475 long dataId;
476 String packageName;
477 String mimeType;
478
479 Mapping mapping;
Jeff Sharkey39261272009-06-03 19:15:09 -0700480 CharSequence summaryValue;
481 String detailValue;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700482
483 /**
484 * Create an action from common {@link Data} elements.
485 */
486 public ActionInfo(long dataId, String packageName, String mimeType) {
487 this.dataId = dataId;
488 this.packageName = packageName;
489 this.mimeType = mimeType;
490 }
491
492 /**
493 * Attempt to find a {@link Mapping} for the package and mime-type
494 * defined by this action. Returns true if one was found.
495 */
496 public boolean findMapping(MappingCache cache) {
497 mapping = cache.findMapping(packageName, mimeType);
498 return (mapping != null);
499 }
500
501 /**
502 * Given a {@link Cursor} pointed at the {@link Data} row associated
503 * with this action, use the {@link Mapping} to build a text summary.
504 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700505 public void buildDetails(Cursor cursor) {
506 if (mapping == null) return;
507
508 // Try finding common display label for this item, otherwise fall
509 // back to reading from defined summary column.
510 summaryValue = ContactsUtils.getDisplayLabel(mContext, mimeType, cursor);
511 if (summaryValue == null && mapping.summaryColumn != null) {
512 summaryValue = cursor.getString(cursor.getColumnIndex(mapping.summaryColumn));
513 }
514
515 // Read detailed value, if mapping was defined
516 if (mapping.detailColumn != null) {
517 detailValue = cursor.getString(cursor.getColumnIndex(mapping.detailColumn));
Jeff Sharkey549aa162009-05-21 01:33:30 -0700518 }
519 }
520
521 /**
522 * Build an {@link Intent} that will perform this action.
523 */
524 public Intent buildIntent() {
525 // Handle well-known mime-types with special care
526 if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700527 Uri callUri = Uri.parse("tel:" + Uri.encode(detailValue));
Jeff Sharkey549aa162009-05-21 01:33:30 -0700528 return new Intent(Intent.ACTION_DIAL, callUri);
529
530 } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700531 Uri smsUri = Uri.fromParts("smsto", detailValue, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700532 return new Intent(Intent.ACTION_SENDTO, smsUri);
533
534 } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700535 Uri mailUri = Uri.fromParts("mailto", detailValue, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700536 return new Intent(Intent.ACTION_SENDTO, mailUri);
537
538 }
539
540 // Otherwise fall back to default VIEW action
541 Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
542
543 Intent intent = new Intent(Intent.ACTION_VIEW);
544 intent.setData(dataUri);
545
546 return intent;
547 }
548 }
549
550 /**
Jeff Sharkey39261272009-06-03 19:15:09 -0700551 * Provide a strongly-typed {@link LinkedList} that holds a list of
552 * {@link ActionInfo} objects.
553 */
554 private class ActionList extends LinkedList<ActionInfo> {
555 }
556
557 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700558 * Provide a simple way of collecting one or more {@link ActionInfo} objects
559 * under a mime-type key.
560 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700561 private class ActionMap extends HashMap<String, ActionList> {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700562 private void collect(String mimeType, ActionInfo info) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700563 // Create list for this mimetype when needed
564 ActionList collectList = get(mimeType);
565 if (collectList == null) {
566 collectList = new ActionList();
567 put(mimeType, collectList);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700568 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700569 collectList.add(info);
570 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700571 }
572
573 /**
574 * Handle the result from the {@link TOKEN_DATA} query.
575 */
576 private void handleData(Cursor cursor) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700577 final int colId = cursor.getColumnIndex(Data._ID);
Jeff Sharkeyc6ad3ab2009-07-21 19:30:15 -0700578 final int colPackage = cursor.getColumnIndex(Data.RES_PACKAGE);
Jeff Sharkey39261272009-06-03 19:15:09 -0700579 final int colMimeType = cursor.getColumnIndex(Data.MIMETYPE);
580 final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700581
Jeff Sharkey549aa162009-05-21 01:33:30 -0700582 ActionInfo info;
583
584 // Add the profile shortcut action if requested
585 if (INCLUDE_PROFILE_ACTION) {
586 final String mimeType = Aggregates.CONTENT_ITEM_TYPE;
587 info = new ActionInfo(mAggId, CommonDataKinds.PACKAGE_COMMON, mimeType);
588 if (info.findMapping(mMappingCache)) {
589 mActions.collect(mimeType, info);
590 }
591 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700592
593 while (cursor.moveToNext()) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700594 final long dataId = cursor.getLong(colId);
595 final String packageName = cursor.getString(colPackage);
596 final String mimeType = cursor.getString(colMimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700597
Jeff Sharkey549aa162009-05-21 01:33:30 -0700598 // Handle when a photo appears in the various data items
599 // TODO: accept a photo only if its marked as primary
600 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700601 byte[] photoBlob = cursor.getBlob(colPhoto);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700602 Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700603 mPhoto.setImageBitmap(photoBitmap);
Jeff Sharkey69126682009-05-18 21:33:41 -0700604 continue;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700605 }
606
Jeff Sharkey549aa162009-05-21 01:33:30 -0700607 // Build an action for this data entry, find a mapping to a UI
608 // element, build its summary from the cursor, and collect it along
609 // with all others of this mime-type.
610 info = new ActionInfo(dataId, packageName, mimeType);
611 if (info.findMapping(mMappingCache)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700612 info.buildDetails(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700613 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700614 }
615
Jeff Sharkey549aa162009-05-21 01:33:30 -0700616 // If phone number, also insert as text message action
617 if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) {
618 info = new ActionInfo(dataId, packageName, MIME_SMS_ADDRESS);
619 if (info.findMapping(mMappingCache)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700620 info.buildDetails(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700621 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700622 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700623 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700624 }
625
626 cursor.close();
627
Jeff Sharkey549aa162009-05-21 01:33:30 -0700628 // Turn our list of actions into UI elements, starting with common types
629 Set<String> containedTypes = mActions.keySet();
630 for (String mimeType : ORDERED_MIMETYPES) {
631 if (containedTypes.contains(mimeType)) {
632 mTrack.addView(inflateAction(mimeType));
633 containedTypes.remove(mimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700634 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700635 }
636
Jeff Sharkey549aa162009-05-21 01:33:30 -0700637 // Then continue with remaining mime-types in alphabetical order
638 String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
639 Arrays.sort(remainingTypes);
640 for (String mimeType : remainingTypes) {
641 mTrack.addView(inflateAction(mimeType));
642 }
643
644 mHasActions = true;
645 considerShowing();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700646 }
647
648 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700649 * Inflate the in-track view for the action of the given mime-type. Will use
650 * the icon provided by the {@link Mapping}.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700651 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700652 private View inflateAction(String mimeType) {
653 ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700654
Jeff Sharkey549aa162009-05-21 01:33:30 -0700655 // Add direct intent if single child, otherwise flag for multiple
Jeff Sharkey39261272009-06-03 19:15:09 -0700656 ActionList children = mActions.get(mimeType);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700657 ActionInfo firstInfo = children.get(0);
Jeff Sharkey39261272009-06-03 19:15:09 -0700658 if (children.size() == 1) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700659 view.setTag(firstInfo.buildIntent());
Jeff Sharkey39261272009-06-03 19:15:09 -0700660 } else {
661 view.setTag(children);
662 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700663
Jeff Sharkey549aa162009-05-21 01:33:30 -0700664 // Set icon and listen for clicks
665 view.setImageBitmap(firstInfo.mapping.icon);
666 view.setOnClickListener(this);
667 return view;
668 }
Jeff Sharkey39261272009-06-03 19:15:09 -0700669
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700670 /** {@inheritDoc} */
671 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
672 // Pass list item clicks along so that Intents are handled uniformly
673 onClick(view);
674 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700675
Jeff Sharkey39261272009-06-03 19:15:09 -0700676 /**
677 * Flag indicating if {@link #mArrowDown} was visible during the last call
678 * to {@link #showResolveList(int)}. Used to decide during a later call if
679 * the arrow should be restored.
680 */
681 private boolean mWasDownArrow = false;
682
683 /**
684 * Helper for showing and hiding {@link #mResolveList}, which will correctly
685 * manage {@link #mArrowDown} as needed.
686 */
687 private void showResolveList(int visibility) {
688 // Show or hide the resolve list if needed
689 if (mResolveList.getVisibility() != visibility) {
690 mResolveList.setVisibility(visibility);
691 }
692
693 if (visibility == View.VISIBLE) {
694 // If showing list, then hide and save state of down arrow
695 mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
696 mArrowDown.setVisibility(View.INVISIBLE);
697 } else {
698 // If hiding list, restore any down arrow state
699 mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
700 }
701
702 }
703
Jeff Sharkey549aa162009-05-21 01:33:30 -0700704 /** {@inheritDoc} */
705 public void onClick(View v) {
706 final Object tag = v.getTag();
707 if (tag instanceof Intent) {
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700708 // Hide the resolution list, if present
Jeff Sharkey39261272009-06-03 19:15:09 -0700709 showResolveList(View.GONE);
710
Jeff Sharkey549aa162009-05-21 01:33:30 -0700711 // Incoming tag is concrete intent, so launch
712 try {
713 mContext.startActivity((Intent)tag);
714 } catch (ActivityNotFoundException e) {
715 Log.w(TAG, NOT_FOUND);
716 Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
717 }
Jeff Sharkey39261272009-06-03 19:15:09 -0700718 } else if (tag instanceof ActionList) {
719 // Incoming tag is a mime-type, so show resolution list
720 final ActionList children = (ActionList)tag;
721
722 // Show resolution list and set adapter
723 showResolveList(View.VISIBLE);
724
725 mResolveList.setOnItemClickListener(this);
726 mResolveList.setAdapter(new BaseAdapter() {
727 public int getCount() {
728 return children.size();
729 }
730
731 public Object getItem(int position) {
732 return children.get(position);
733 }
734
735 public long getItemId(int position) {
736 return position;
737 }
738
739 public View getView(int position, View convertView, ViewGroup parent) {
740 if (convertView == null) {
741 convertView = mInflater.inflate(R.layout.fasttrack_resolve_item, parent, false);
742 }
743
744 // Set action title based on summary value
745 ActionInfo info = (ActionInfo)getItem(position);
746
747 ImageView icon1 = (ImageView)convertView.findViewById(android.R.id.icon1);
748 TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
749 TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
750
751 icon1.setImageBitmap(info.mapping.icon);
752 text1.setText(info.summaryValue);
753 text2.setText(info.detailValue);
754
755 convertView.setTag(info.buildIntent());
756 return convertView;
757 }
758 });
759
760 // Make sure we resize to make room for ListView
761 onWindowAttributesChanged(mWindow.getAttributes());
Jeff Sharkey549aa162009-05-21 01:33:30 -0700762
763 }
764 }
765
766 /** {@inheritDoc} */
767 public boolean dispatchKeyEvent(KeyEvent event) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700768 if (event.getAction() == KeyEvent.ACTION_DOWN
769 && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
770 // Back key will first dismiss any expanded resolve list, otherwise
771 // it will close the entire dialog.
772 if (mResolveList.getVisibility() == View.VISIBLE) {
773 showResolveList(View.GONE);
774 } else {
775 dismiss();
776 }
777 return true;
778 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700779 return mWindow.superDispatchKeyEvent(event);
780 }
781
782 /** {@inheritDoc} */
783 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
784 // TODO: make this window accessible
785 return false;
786 }
787
788 /** {@inheritDoc} */
789 public boolean dispatchTouchEvent(MotionEvent event) {
790 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
791 dismiss();
792 return true;
793 }
794 return mWindow.superDispatchTouchEvent(event);
795 }
796
797 /** {@inheritDoc} */
798 public boolean dispatchTrackballEvent(MotionEvent event) {
799 return mWindow.superDispatchTrackballEvent(event);
800 }
801
802 /** {@inheritDoc} */
803 public void onContentChanged() {
804 }
805
806 /** {@inheritDoc} */
807 public boolean onCreatePanelMenu(int featureId, Menu menu) {
808 return false;
809 }
810
811 /** {@inheritDoc} */
812 public View onCreatePanelView(int featureId) {
813 return null;
814 }
815
816 /** {@inheritDoc} */
817 public boolean onMenuItemSelected(int featureId, MenuItem item) {
818 return false;
819 }
820
821 /** {@inheritDoc} */
822 public boolean onMenuOpened(int featureId, Menu menu) {
823 return false;
824 }
825
826 /** {@inheritDoc} */
827 public void onPanelClosed(int featureId, Menu menu) {
828 }
829
830 /** {@inheritDoc} */
831 public boolean onPreparePanel(int featureId, View view, Menu menu) {
832 return false;
833 }
834
835 /** {@inheritDoc} */
836 public boolean onSearchRequested() {
837 return false;
838 }
839
840 /** {@inheritDoc} */
841 public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
842 if (mDecor != null) {
843 mWindowManager.updateViewLayout(mDecor, attrs);
844 }
845 }
846
847 /** {@inheritDoc} */
848 public void onWindowFocusChanged(boolean hasFocus) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700849 }
850}