blob: b96cf16b1f3e4150e3a4acfe7c730aa8be67256d [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;
Jeff Sharkey06bd5a82009-06-15 20:06:48 -070040import android.provider.ContactsContract.Contacts;
Evan Millar66388be2009-05-28 15:41:07 -070041import android.provider.ContactsContract.Data;
Jeff Sharkey39261272009-06-03 19:15:09 -070042import android.provider.ContactsContract.Presence;
Evan Millar66388be2009-05-28 15:41:07 -070043import android.provider.ContactsContract.CommonDataKinds.Email;
Evan Millar66388be2009-05-28 15:41:07 -070044import android.provider.ContactsContract.CommonDataKinds.Phone;
45import android.provider.ContactsContract.CommonDataKinds.Photo;
Jeff Sharkey39261272009-06-03 19:15:09 -070046import android.provider.Im.PresenceColumns;
47import android.provider.SocialContract.Activities;
Jeff Sharkey80a193a2009-05-21 14:18:18 -070048import android.text.SpannableStringBuilder;
Jeff Sharkey39261272009-06-03 19:15:09 -070049import android.text.format.DateUtils;
Jeff Sharkey80a193a2009-05-21 14:18:18 -070050import android.text.style.CharacterStyle;
51import android.text.style.ForegroundColorSpan;
52import android.text.style.StyleSpan;
Jeff Sharkey3f177592009-05-18 15:23:12 -070053import android.util.Log;
Jeff Sharkey549aa162009-05-21 01:33:30 -070054import android.view.ContextThemeWrapper;
Jeff Sharkey3f177592009-05-18 15:23:12 -070055import android.view.Gravity;
Jeff Sharkey549aa162009-05-21 01:33:30 -070056import android.view.KeyEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070057import android.view.LayoutInflater;
Jeff Sharkey549aa162009-05-21 01:33:30 -070058import android.view.Menu;
59import android.view.MenuItem;
60import android.view.MotionEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070061import android.view.View;
62import android.view.ViewGroup;
Jeff Sharkey549aa162009-05-21 01:33:30 -070063import android.view.Window;
64import android.view.WindowManager;
Jeff Sharkey3f177592009-05-18 15:23:12 -070065import android.view.View.OnClickListener;
Jeff Sharkey549aa162009-05-21 01:33:30 -070066import android.view.accessibility.AccessibilityEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070067import android.widget.AbsListView;
Jeff Sharkey51ca5442009-05-21 16:12:18 -070068import android.widget.AdapterView;
69import android.widget.BaseAdapter;
Jeff Sharkey39261272009-06-03 19:15:09 -070070import android.widget.HorizontalScrollView;
Jeff Sharkey3f177592009-05-18 15:23:12 -070071import android.widget.ImageView;
72import android.widget.LinearLayout;
73import android.widget.ListView;
Jeff Sharkey39261272009-06-03 19:15:09 -070074import android.widget.RemoteViews;
Jeff Sharkey3f177592009-05-18 15:23:12 -070075import android.widget.TextView;
76import android.widget.Toast;
Jeff Sharkey3f177592009-05-18 15:23:12 -070077
Jeff Sharkey549aa162009-05-21 01:33:30 -070078import java.util.Arrays;
Jeff Sharkey3f177592009-05-18 15:23:12 -070079import java.util.HashMap;
Jeff Sharkey549aa162009-05-21 01:33:30 -070080import java.util.LinkedList;
Jeff Sharkey549aa162009-05-21 01:33:30 -070081import java.util.Set;
Jeff Sharkey3f177592009-05-18 15:23:12 -070082
83/**
Jeff Sharkey549aa162009-05-21 01:33:30 -070084 * Window that shows fast-track contact details for a specific
Jeff Sharkey39261272009-06-03 19:15:09 -070085 * {@link Aggregates#_ID}.
Jeff Sharkey3f177592009-05-18 15:23:12 -070086 */
Jeff Sharkey39261272009-06-03 19:15:09 -070087public class FastTrackWindow implements Window.Callback, QueryCompleteListener, OnClickListener,
88 AbsListView.OnItemClickListener {
Jeff Sharkey3f177592009-05-18 15:23:12 -070089 private static final String TAG = "FastTrackWindow";
90
Jeff Sharkey549aa162009-05-21 01:33:30 -070091 /**
92 * Interface used to allow the person showing a {@link FastTrackWindow} to
93 * know when the window has been dismissed.
94 */
95 interface OnDismissListener {
96 public void onDismiss(FastTrackWindow dialog);
97 }
98
99 final Context mContext;
100 final LayoutInflater mInflater;
101 final WindowManager mWindowManager;
102 Window mWindow;
103 View mDecor;
104
105 private boolean mQuerying = false;
106 private boolean mShowing = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700107
108 /** Mapping cache from mime-type to icons and actions */
109 private MappingCache mMappingCache;
110
Jeff Sharkey3f177592009-05-18 15:23:12 -0700111 private NotifyingAsyncQueryHandler mHandler;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700112 private OnDismissListener mDismissListener;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700113
Jeff Sharkey549aa162009-05-21 01:33:30 -0700114 private long mAggId;
Jeff Sharkey39261272009-06-03 19:15:09 -0700115 private Rect mAnchor;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700116
Jeff Sharkey39261272009-06-03 19:15:09 -0700117 private boolean mHasSummary = false;
118 private boolean mHasSocial = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700119 private boolean mHasActions = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700120
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700121 private View mArrowUp;
122 private View mArrowDown;
123
Jeff Sharkey549aa162009-05-21 01:33:30 -0700124 private ImageView mPhoto;
125 private ImageView mPresence;
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700126 private TextView mContent;
127 private TextView mPublished;
Jeff Sharkey39261272009-06-03 19:15:09 -0700128 private HorizontalScrollView mTrackScroll;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700129 private ViewGroup mTrack;
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700130 private ListView mResolveList;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700131
Jeff Sharkey39261272009-06-03 19:15:09 -0700132 private String mDisplayName = null;
133 private String mSocialTitle = null;
134
135 private SpannableStringBuilder mBuilder = new SpannableStringBuilder();
136 private CharacterStyle mStyleBold = new StyleSpan(android.graphics.Typeface.BOLD);
137 private CharacterStyle mStyleBlack = new ForegroundColorSpan(Color.BLACK);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700138
139 /**
140 * Set of {@link ActionInfo} that are associated with the aggregate
Jeff Sharkey39261272009-06-03 19:15:09 -0700141 * currently displayed by this fast-track window, represented as a map from
142 * {@link String} mimetype to {@link ActionList}.
Jeff Sharkey549aa162009-05-21 01:33:30 -0700143 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700144 private ActionMap mActions = new ActionMap();
Jeff Sharkey549aa162009-05-21 01:33:30 -0700145
146 /**
147 * Specific mime-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
148 * distinguishes actions that should initiate a text message.
149 */
150 public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
151
152 /**
153 * Specific mime-types that should be bumped to the front of the fast-track.
154 * Other mime-types not appearing in this list follow in alphabetic order.
155 */
156 private static final String[] ORDERED_MIMETYPES = new String[] {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700157 Phones.CONTENT_ITEM_TYPE,
Jeff Sharkey39261272009-06-03 19:15:09 -0700158 Aggregates.CONTENT_ITEM_TYPE,
Jeff Sharkey549aa162009-05-21 01:33:30 -0700159 MIME_SMS_ADDRESS,
160 Email.CONTENT_ITEM_TYPE,
161 };
162
Jeff Sharkey549aa162009-05-21 01:33:30 -0700163 private static final boolean INCLUDE_PROFILE_ACTION = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700164
Jeff Sharkey39261272009-06-03 19:15:09 -0700165 private static final int TOKEN_SUMMARY = 1;
166 private static final int TOKEN_SOCIAL = 2;
167 private static final int TOKEN_DATA = 3;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700168
Jeff Sharkey3f177592009-05-18 15:23:12 -0700169 /** Message to show when no activity is found to perform an action */
170 // TODO: move this value into a resources string
171 private static final String NOT_FOUND = "Couldn't find an app to handle this action";
172
Jeff Sharkey549aa162009-05-21 01:33:30 -0700173 /**
174 * Prepare a fast-track window to show in the given {@link Context}.
175 */
176 public FastTrackWindow(Context context) {
177 mContext = new ContextThemeWrapper(context, R.style.FastTrack);
178 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
179 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700180
Jeff Sharkey549aa162009-05-21 01:33:30 -0700181 mWindow = PolicyManager.makeNewWindow(mContext);
182 mWindow.setCallback(this);
183 mWindow.setWindowManager(mWindowManager, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700184
Jeff Sharkey549aa162009-05-21 01:33:30 -0700185 mWindow.setContentView(R.layout.fasttrack);
186
Jeff Sharkey39261272009-06-03 19:15:09 -0700187 mArrowUp = mWindow.findViewById(R.id.arrow_up);
188 mArrowDown = mWindow.findViewById(R.id.arrow_down);
189
Jeff Sharkey549aa162009-05-21 01:33:30 -0700190 mPhoto = (ImageView)mWindow.findViewById(R.id.photo);
191 mPresence = (ImageView)mWindow.findViewById(R.id.presence);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700192 mContent = (TextView)mWindow.findViewById(R.id.content);
193 mPublished = (TextView)mWindow.findViewById(R.id.published);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700194 mTrack = (ViewGroup)mWindow.findViewById(R.id.fasttrack);
Jeff Sharkey39261272009-06-03 19:15:09 -0700195 mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700196 mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700197
198 // TODO: move generation of mime-type cache to more-efficient place
199 generateMappingCache();
200
Jeff Sharkey3f177592009-05-18 15:23:12 -0700201 }
202
203 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700204 * Prepare a fast-track window to show in the given {@link Context}, and
205 * notify the given {@link OnDismissListener} each time this dialog is
206 * dismissed.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700207 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700208 public FastTrackWindow(Context context, OnDismissListener dismissListener) {
209 this(context);
210 mDismissListener = dismissListener;
211 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700212
Jeff Sharkey549aa162009-05-21 01:33:30 -0700213 /**
214 * Generate {@link MappingCache} specifically for fast-track windows. This
215 * cache knows how to display {@link CommonDataKinds#PACKAGE_COMMON} data
216 * types using generic icons.
217 */
218 private void generateMappingCache() {
219 mMappingCache = MappingCache.createAndFill(mContext);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700220
Jeff Sharkey549aa162009-05-21 01:33:30 -0700221 Resources res = mContext.getResources();
222 Mapping mapping;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700223
Jeff Sharkey549aa162009-05-21 01:33:30 -0700224 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Aggregates.CONTENT_ITEM_TYPE);
225 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.ic_contacts_details);
226 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700227
Jeff Sharkey549aa162009-05-21 01:33:30 -0700228 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Phone.CONTENT_ITEM_TYPE);
Jeff Sharkey39261272009-06-03 19:15:09 -0700229 mapping.summaryColumn = Phone.TYPE;
230 mapping.detailColumn = Phone.NUMBER;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700231 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_call);
232 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700233
Jeff Sharkey549aa162009-05-21 01:33:30 -0700234 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, MIME_SMS_ADDRESS);
Jeff Sharkey39261272009-06-03 19:15:09 -0700235 mapping.summaryColumn = Phone.TYPE;
236 mapping.detailColumn = Phone.NUMBER;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700237 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);
Jeff Sharkey39261272009-06-03 19:15:09 -0700241 mapping.summaryColumn = Email.TYPE;
242 mapping.detailColumn = Email.DATA;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700243 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_email);
244 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700245
Jeff Sharkey549aa162009-05-21 01:33:30 -0700246 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700247
Jeff Sharkey549aa162009-05-21 01:33:30 -0700248 /**
249 * Start showing a fast-track window for the given {@link Aggregate#_ID}
250 * pointing towards the given location.
251 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700252 public void show(Uri aggUri, Rect anchor) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700253 if (mShowing || mQuerying) {
254 Log.w(TAG, "already in process of showing");
255 return;
256 }
257
258 mAggId = ContentUris.parseId(aggUri);
Jeff Sharkey39261272009-06-03 19:15:09 -0700259 mAnchor = new Rect(anchor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700260 mQuerying = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700261
Jeff Sharkey39261272009-06-03 19:15:09 -0700262 Uri aggSummary = ContentUris.withAppendedId(
263 ContactsContract.Aggregates.CONTENT_SUMMARY_URI, mAggId);
264 Uri aggSocial = ContentUris.withAppendedId(
265 SocialContract.Activities.CONTENT_AGGREGATE_STATUS_URI, mAggId);
266 Uri aggData = Uri.withAppendedPath(aggUri,
Jeff Sharkey549aa162009-05-21 01:33:30 -0700267 ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700268
Jeff Sharkey39261272009-06-03 19:15:09 -0700269 // Start data query in background
Jeff Sharkey549aa162009-05-21 01:33:30 -0700270 mHandler = new NotifyingAsyncQueryHandler(mContext, this);
Jeff Sharkey39261272009-06-03 19:15:09 -0700271 mHandler.startQuery(TOKEN_SUMMARY, null, aggSummary, null, null, null, null);
272 mHandler.startQuery(TOKEN_SOCIAL, null, aggSocial, null, null, null, null);
273 mHandler.startQuery(TOKEN_DATA, null, aggData, null, null, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700274
275 }
276
277 /**
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700278 * Show the correct callout arrow based on a {@link R.id} reference.
279 */
280 private void showArrow(int whichArrow, int requestedX) {
281 final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
282 final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
Jeff Sharkey39261272009-06-03 19:15:09 -0700283
284 final int arrowWidth = mArrowUp.getMeasuredWidth();
285
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700286 showArrow.setVisibility(View.VISIBLE);
287 LinearLayout.LayoutParams param = (LinearLayout.LayoutParams)showArrow.getLayoutParams();
Jeff Sharkey39261272009-06-03 19:15:09 -0700288 param.leftMargin = requestedX - arrowWidth / 2;
289
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700290 hideArrow.setVisibility(View.INVISIBLE);
291 }
292
293 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700294 * Actual internal method to show this fast-track window. Called only by
295 * {@link #considerShowing()} when all data requirements have been met.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700296 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700297 private void showInternal() {
298 mDecor = mWindow.getDecorView();
299 WindowManager.LayoutParams l = mWindow.getAttributes();
Jeff Sharkey39261272009-06-03 19:15:09 -0700300
301 l.width = WindowManager.LayoutParams.FILL_PARENT;
302 l.height = WindowManager.LayoutParams.WRAP_CONTENT;
303
304 // Force layout measuring pass so we have baseline numbers
305 mDecor.measure(l.width, l.height);
306
307 final int blockHeight = mDecor.getMeasuredHeight();
308 final int arrowHeight = mArrowUp.getHeight();
309
Jeff Sharkey549aa162009-05-21 01:33:30 -0700310 l.gravity = Gravity.TOP | Gravity.LEFT;
311 l.x = 0;
Jeff Sharkey39261272009-06-03 19:15:09 -0700312
313 if (mAnchor.top > blockHeight) {
314 // Show downwards callout when enough room, aligning bottom block
315 // edge with top of anchor area, and adjusting to inset arrow.
316 showArrow(R.id.arrow_down, mAnchor.centerX());
317 l.y = mAnchor.top - blockHeight + arrowHeight;
318
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700319 } else {
Jeff Sharkey39261272009-06-03 19:15:09 -0700320 // Otherwise show upwards callout, aligning block top with bottom of
321 // anchor area, and adjusting to inset arrow.
322 showArrow(R.id.arrow_up, mAnchor.centerX());
323 l.y = mAnchor.bottom - arrowHeight;
324
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700325 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700326
327 l.dimAmount = 0.6f;
328 l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND
329 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
Jeff Sharkey39261272009-06-03 19:15:09 -0700330 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
Jeff Sharkey549aa162009-05-21 01:33:30 -0700331 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
332 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
333
334 mWindowManager.addView(mDecor, l);
335 mShowing = true;
336 mQuerying = false;
337 }
338
339 /**
340 * Dismiss this fast-track window if showing.
341 */
342 public void dismiss() {
343 if (!mQuerying && !mShowing) {
344 Log.d(TAG, "not visible, ignore");
345 return;
346 }
347
348 // Cancel any pending queries
Jeff Sharkey39261272009-06-03 19:15:09 -0700349 mHandler.cancelOperation(TOKEN_SUMMARY);
350 mHandler.cancelOperation(TOKEN_SOCIAL);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700351 mHandler.cancelOperation(TOKEN_DATA);
352
353 // Reset all views to prepare for possible recycling
354 mPhoto.setImageResource(R.drawable.ic_contact_list_picture);
Jeff Sharkey39261272009-06-03 19:15:09 -0700355 mPresence.setImageDrawable(null);
356 mPublished.setText(null);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700357 mContent.setText(null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700358
Jeff Sharkey39261272009-06-03 19:15:09 -0700359 mDisplayName = null;
360 mSocialTitle = null;
361
362 // Clear track actions and scroll to hard left
Jeff Sharkey549aa162009-05-21 01:33:30 -0700363 mActions.clear();
364 mTrack.removeAllViews();
Jeff Sharkey39261272009-06-03 19:15:09 -0700365 mTrackScroll.fullScroll(View.FOCUS_LEFT);
366 mWasDownArrow = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700367
Jeff Sharkey39261272009-06-03 19:15:09 -0700368 showResolveList(View.GONE);
369
370 mHasSocial = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700371 mHasActions = false;
372
373 if (mDecor == null || !mShowing) {
374 Log.d(TAG, "not showing, ignore");
375 return;
376 }
377
378 mWindowManager.removeView(mDecor);
379 mDecor = null;
380 mWindow.closeAllPanels();
381 mShowing = false;
382
383 // Notify any listeners that we've been dismissed
384 if (mDismissListener != null) {
385 mDismissListener.onDismiss(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700386 }
387 }
388
Jeff Sharkey549aa162009-05-21 01:33:30 -0700389 /**
390 * Returns true if this fast-track window is showing or querying.
391 */
392 public boolean isShowing() {
393 return mShowing || mQuerying;
394 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700395
Jeff Sharkey549aa162009-05-21 01:33:30 -0700396 /**
397 * Consider showing this window, which will only call through to
398 * {@link #showInternal()} when all data items are present.
399 */
400 private synchronized void considerShowing() {
Jeff Sharkey39261272009-06-03 19:15:09 -0700401 if (mHasSummary && mHasSocial && mHasActions && !mShowing) {
402 // Now that all queries have been finished, build summary string.
403 mBuilder.clear();
404 mBuilder.append(mDisplayName);
405 mBuilder.append(" ");
406 mBuilder.append(mSocialTitle);
407 mBuilder.setSpan(mStyleBold, 0, mDisplayName.length(), 0);
408 mBuilder.setSpan(mStyleBlack, 0, mDisplayName.length(), 0);
409 mContent.setText(mBuilder);
410
Jeff Sharkey549aa162009-05-21 01:33:30 -0700411 showInternal();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700412 }
413 }
414
415 /** {@inheritDoc} */
416 public void onQueryComplete(int token, Object cookie, Cursor cursor) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700417 if (cursor == null) {
418 return;
Jeff Sharkey39261272009-06-03 19:15:09 -0700419 } else if (token == TOKEN_SUMMARY) {
420 handleSummary(cursor);
421 } else if (token == TOKEN_SOCIAL) {
422 handleSocial(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700423 } else if (token == TOKEN_DATA) {
424 handleData(cursor);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700425 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700426 }
427
428 /**
Jeff Sharkey39261272009-06-03 19:15:09 -0700429 * Handle the result from the {@link TOKEN_SUMMARY} query.
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700430 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700431 private void handleSummary(Cursor cursor) {
432 final int colDisplayName = cursor.getColumnIndex(Aggregates.DISPLAY_NAME);
433 final int colStatus = cursor.getColumnIndex(PresenceColumns.PRESENCE_STATUS);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700434
435 if (cursor.moveToNext()) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700436 mDisplayName = cursor.getString(colDisplayName);
437
438 int status = cursor.getInt(colStatus);
439 int statusRes = Presence.getPresenceIconResourceId(status);
440 mPresence.setImageResource(statusRes);
441 }
442
443 mHasSummary = true;
444 considerShowing();
445 }
446
447 /**
448 * Handle the result from the {@link TOKEN_SOCIAL} query.
449 */
450 private void handleSocial(Cursor cursor) {
451 final int colTitle = cursor.getColumnIndex(Activities.TITLE);
452 final int colPublished = cursor.getColumnIndex(Activities.PUBLISHED);
453
454 if (cursor.moveToNext()) {
455 mSocialTitle = cursor.getString(colTitle);
456
457 long published = cursor.getLong(colPublished);
458 CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
459 System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
460 mPublished.setText(relativePublished);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700461
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700462 }
463
Jeff Sharkey39261272009-06-03 19:15:09 -0700464 mHasSocial = true;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700465 considerShowing();
466 }
467
468 /**
469 * Description of a specific, actionable {@link Data#_ID} item. May have a
470 * {@link Mapping} associated with it to find {@link RemoteViews} or icon,
471 * and may have built a summary of itself for UI display.
472 */
473 private class ActionInfo {
474 long dataId;
475 String packageName;
476 String mimeType;
477
478 Mapping mapping;
Jeff Sharkey39261272009-06-03 19:15:09 -0700479 CharSequence summaryValue;
480 String detailValue;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700481
482 /**
483 * Create an action from common {@link Data} elements.
484 */
485 public ActionInfo(long dataId, String packageName, String mimeType) {
486 this.dataId = dataId;
487 this.packageName = packageName;
488 this.mimeType = mimeType;
489 }
490
491 /**
492 * Attempt to find a {@link Mapping} for the package and mime-type
493 * defined by this action. Returns true if one was found.
494 */
495 public boolean findMapping(MappingCache cache) {
496 mapping = cache.findMapping(packageName, mimeType);
497 return (mapping != null);
498 }
499
500 /**
501 * Given a {@link Cursor} pointed at the {@link Data} row associated
502 * with this action, use the {@link Mapping} to build a text summary.
503 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700504 public void buildDetails(Cursor cursor) {
505 if (mapping == null) return;
506
507 // Try finding common display label for this item, otherwise fall
508 // back to reading from defined summary column.
509 summaryValue = ContactsUtils.getDisplayLabel(mContext, mimeType, cursor);
510 if (summaryValue == null && mapping.summaryColumn != null) {
511 summaryValue = cursor.getString(cursor.getColumnIndex(mapping.summaryColumn));
512 }
513
514 // Read detailed value, if mapping was defined
515 if (mapping.detailColumn != null) {
516 detailValue = cursor.getString(cursor.getColumnIndex(mapping.detailColumn));
Jeff Sharkey549aa162009-05-21 01:33:30 -0700517 }
518 }
519
520 /**
521 * Build an {@link Intent} that will perform this action.
522 */
523 public Intent buildIntent() {
524 // Handle well-known mime-types with special care
525 if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700526 Uri callUri = Uri.parse("tel:" + Uri.encode(detailValue));
Jeff Sharkey549aa162009-05-21 01:33:30 -0700527 return new Intent(Intent.ACTION_DIAL, callUri);
528
529 } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700530 Uri smsUri = Uri.fromParts("smsto", detailValue, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700531 return new Intent(Intent.ACTION_SENDTO, smsUri);
532
533 } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700534 Uri mailUri = Uri.fromParts("mailto", detailValue, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700535 return new Intent(Intent.ACTION_SENDTO, mailUri);
536
537 }
538
539 // Otherwise fall back to default VIEW action
540 Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
541
542 Intent intent = new Intent(Intent.ACTION_VIEW);
543 intent.setData(dataUri);
544
545 return intent;
546 }
547 }
548
549 /**
Jeff Sharkey39261272009-06-03 19:15:09 -0700550 * Provide a strongly-typed {@link LinkedList} that holds a list of
551 * {@link ActionInfo} objects.
552 */
553 private class ActionList extends LinkedList<ActionInfo> {
554 }
555
556 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700557 * Provide a simple way of collecting one or more {@link ActionInfo} objects
558 * under a mime-type key.
559 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700560 private class ActionMap extends HashMap<String, ActionList> {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700561 private void collect(String mimeType, ActionInfo info) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700562 // Create list for this mimetype when needed
563 ActionList collectList = get(mimeType);
564 if (collectList == null) {
565 collectList = new ActionList();
566 put(mimeType, collectList);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700567 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700568 collectList.add(info);
569 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700570 }
571
572 /**
573 * Handle the result from the {@link TOKEN_DATA} query.
574 */
575 private void handleData(Cursor cursor) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700576 final int colId = cursor.getColumnIndex(Data._ID);
Jeff Sharkey06bd5a82009-06-15 20:06:48 -0700577 final int colPackage = cursor.getColumnIndex(Contacts.PACKAGE);
Jeff Sharkey39261272009-06-03 19:15:09 -0700578 final int colMimeType = cursor.getColumnIndex(Data.MIMETYPE);
579 final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700580
Jeff Sharkey549aa162009-05-21 01:33:30 -0700581 ActionInfo info;
582
583 // Add the profile shortcut action if requested
584 if (INCLUDE_PROFILE_ACTION) {
585 final String mimeType = Aggregates.CONTENT_ITEM_TYPE;
586 info = new ActionInfo(mAggId, CommonDataKinds.PACKAGE_COMMON, mimeType);
587 if (info.findMapping(mMappingCache)) {
588 mActions.collect(mimeType, info);
589 }
590 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700591
592 while (cursor.moveToNext()) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700593 final long dataId = cursor.getLong(colId);
594 final String packageName = cursor.getString(colPackage);
595 final String mimeType = cursor.getString(colMimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700596
Jeff Sharkey549aa162009-05-21 01:33:30 -0700597 // Handle when a photo appears in the various data items
598 // TODO: accept a photo only if its marked as primary
599 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700600 byte[] photoBlob = cursor.getBlob(colPhoto);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700601 Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700602 mPhoto.setImageBitmap(photoBitmap);
Jeff Sharkey69126682009-05-18 21:33:41 -0700603 continue;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700604 }
605
Jeff Sharkey549aa162009-05-21 01:33:30 -0700606 // Build an action for this data entry, find a mapping to a UI
607 // element, build its summary from the cursor, and collect it along
608 // with all others of this mime-type.
609 info = new ActionInfo(dataId, packageName, mimeType);
610 if (info.findMapping(mMappingCache)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700611 info.buildDetails(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700612 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700613 }
614
Jeff Sharkey549aa162009-05-21 01:33:30 -0700615 // If phone number, also insert as text message action
616 if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) {
617 info = new ActionInfo(dataId, packageName, MIME_SMS_ADDRESS);
618 if (info.findMapping(mMappingCache)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700619 info.buildDetails(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700620 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700621 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700622 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700623 }
624
625 cursor.close();
626
Jeff Sharkey549aa162009-05-21 01:33:30 -0700627 // Turn our list of actions into UI elements, starting with common types
628 Set<String> containedTypes = mActions.keySet();
629 for (String mimeType : ORDERED_MIMETYPES) {
630 if (containedTypes.contains(mimeType)) {
631 mTrack.addView(inflateAction(mimeType));
632 containedTypes.remove(mimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700633 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700634 }
635
Jeff Sharkey549aa162009-05-21 01:33:30 -0700636 // Then continue with remaining mime-types in alphabetical order
637 String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
638 Arrays.sort(remainingTypes);
639 for (String mimeType : remainingTypes) {
640 mTrack.addView(inflateAction(mimeType));
641 }
642
643 mHasActions = true;
644 considerShowing();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700645 }
646
647 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700648 * Inflate the in-track view for the action of the given mime-type. Will use
649 * the icon provided by the {@link Mapping}.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700650 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700651 private View inflateAction(String mimeType) {
652 ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700653
Jeff Sharkey549aa162009-05-21 01:33:30 -0700654 // Add direct intent if single child, otherwise flag for multiple
Jeff Sharkey39261272009-06-03 19:15:09 -0700655 ActionList children = mActions.get(mimeType);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700656 ActionInfo firstInfo = children.get(0);
Jeff Sharkey39261272009-06-03 19:15:09 -0700657 if (children.size() == 1) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700658 view.setTag(firstInfo.buildIntent());
Jeff Sharkey39261272009-06-03 19:15:09 -0700659 } else {
660 view.setTag(children);
661 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700662
Jeff Sharkey549aa162009-05-21 01:33:30 -0700663 // Set icon and listen for clicks
664 view.setImageBitmap(firstInfo.mapping.icon);
665 view.setOnClickListener(this);
666 return view;
667 }
Jeff Sharkey39261272009-06-03 19:15:09 -0700668
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700669 /** {@inheritDoc} */
670 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
671 // Pass list item clicks along so that Intents are handled uniformly
672 onClick(view);
673 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700674
Jeff Sharkey39261272009-06-03 19:15:09 -0700675 /**
676 * Flag indicating if {@link #mArrowDown} was visible during the last call
677 * to {@link #showResolveList(int)}. Used to decide during a later call if
678 * the arrow should be restored.
679 */
680 private boolean mWasDownArrow = false;
681
682 /**
683 * Helper for showing and hiding {@link #mResolveList}, which will correctly
684 * manage {@link #mArrowDown} as needed.
685 */
686 private void showResolveList(int visibility) {
687 // Show or hide the resolve list if needed
688 if (mResolveList.getVisibility() != visibility) {
689 mResolveList.setVisibility(visibility);
690 }
691
692 if (visibility == View.VISIBLE) {
693 // If showing list, then hide and save state of down arrow
694 mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
695 mArrowDown.setVisibility(View.INVISIBLE);
696 } else {
697 // If hiding list, restore any down arrow state
698 mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
699 }
700
701 }
702
Jeff Sharkey549aa162009-05-21 01:33:30 -0700703 /** {@inheritDoc} */
704 public void onClick(View v) {
705 final Object tag = v.getTag();
706 if (tag instanceof Intent) {
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700707 // Hide the resolution list, if present
Jeff Sharkey39261272009-06-03 19:15:09 -0700708 showResolveList(View.GONE);
709
Jeff Sharkey549aa162009-05-21 01:33:30 -0700710 // Incoming tag is concrete intent, so launch
711 try {
712 mContext.startActivity((Intent)tag);
713 } catch (ActivityNotFoundException e) {
714 Log.w(TAG, NOT_FOUND);
715 Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
716 }
Jeff Sharkey39261272009-06-03 19:15:09 -0700717 } else if (tag instanceof ActionList) {
718 // Incoming tag is a mime-type, so show resolution list
719 final ActionList children = (ActionList)tag;
720
721 // Show resolution list and set adapter
722 showResolveList(View.VISIBLE);
723
724 mResolveList.setOnItemClickListener(this);
725 mResolveList.setAdapter(new BaseAdapter() {
726 public int getCount() {
727 return children.size();
728 }
729
730 public Object getItem(int position) {
731 return children.get(position);
732 }
733
734 public long getItemId(int position) {
735 return position;
736 }
737
738 public View getView(int position, View convertView, ViewGroup parent) {
739 if (convertView == null) {
740 convertView = mInflater.inflate(R.layout.fasttrack_resolve_item, parent, false);
741 }
742
743 // Set action title based on summary value
744 ActionInfo info = (ActionInfo)getItem(position);
745
746 ImageView icon1 = (ImageView)convertView.findViewById(android.R.id.icon1);
747 TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
748 TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
749
750 icon1.setImageBitmap(info.mapping.icon);
751 text1.setText(info.summaryValue);
752 text2.setText(info.detailValue);
753
754 convertView.setTag(info.buildIntent());
755 return convertView;
756 }
757 });
758
759 // Make sure we resize to make room for ListView
760 onWindowAttributesChanged(mWindow.getAttributes());
Jeff Sharkey549aa162009-05-21 01:33:30 -0700761
762 }
763 }
764
765 /** {@inheritDoc} */
766 public boolean dispatchKeyEvent(KeyEvent event) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700767 if (event.getAction() == KeyEvent.ACTION_DOWN
768 && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
769 // Back key will first dismiss any expanded resolve list, otherwise
770 // it will close the entire dialog.
771 if (mResolveList.getVisibility() == View.VISIBLE) {
772 showResolveList(View.GONE);
773 } else {
774 dismiss();
775 }
776 return true;
777 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700778 return mWindow.superDispatchKeyEvent(event);
779 }
780
781 /** {@inheritDoc} */
782 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
783 // TODO: make this window accessible
784 return false;
785 }
786
787 /** {@inheritDoc} */
788 public boolean dispatchTouchEvent(MotionEvent event) {
789 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
790 dismiss();
791 return true;
792 }
793 return mWindow.superDispatchTouchEvent(event);
794 }
795
796 /** {@inheritDoc} */
797 public boolean dispatchTrackballEvent(MotionEvent event) {
798 return mWindow.superDispatchTrackballEvent(event);
799 }
800
801 /** {@inheritDoc} */
802 public void onContentChanged() {
803 }
804
805 /** {@inheritDoc} */
806 public boolean onCreatePanelMenu(int featureId, Menu menu) {
807 return false;
808 }
809
810 /** {@inheritDoc} */
811 public View onCreatePanelView(int featureId) {
812 return null;
813 }
814
815 /** {@inheritDoc} */
816 public boolean onMenuItemSelected(int featureId, MenuItem item) {
817 return false;
818 }
819
820 /** {@inheritDoc} */
821 public boolean onMenuOpened(int featureId, Menu menu) {
822 return false;
823 }
824
825 /** {@inheritDoc} */
826 public void onPanelClosed(int featureId, Menu menu) {
827 }
828
829 /** {@inheritDoc} */
830 public boolean onPreparePanel(int featureId, View view, Menu menu) {
831 return false;
832 }
833
834 /** {@inheritDoc} */
835 public boolean onSearchRequested() {
836 return false;
837 }
838
839 /** {@inheritDoc} */
840 public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
841 if (mDecor != null) {
842 mWindowManager.updateViewLayout(mDecor, attrs);
843 }
844 }
845
846 /** {@inheritDoc} */
847 public void onWindowFocusChanged(boolean hasFocus) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700848 }
849}