blob: f61b73ec47b7c58ca70b357c6850d61404a22ecd [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
Evan Millar54a5c9f2009-06-23 17:41:09 -0700146 // TODO We should move this to someplace more general as it is needed in a few places in the app
147 // code.
Jeff Sharkey549aa162009-05-21 01:33:30 -0700148 /**
149 * Specific mime-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
150 * distinguishes actions that should initiate a text message.
151 */
152 public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
153
154 /**
155 * Specific mime-types that should be bumped to the front of the fast-track.
156 * Other mime-types not appearing in this list follow in alphabetic order.
157 */
158 private static final String[] ORDERED_MIMETYPES = new String[] {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700159 Phones.CONTENT_ITEM_TYPE,
Jeff Sharkey39261272009-06-03 19:15:09 -0700160 Aggregates.CONTENT_ITEM_TYPE,
Jeff Sharkey549aa162009-05-21 01:33:30 -0700161 MIME_SMS_ADDRESS,
162 Email.CONTENT_ITEM_TYPE,
163 };
164
Jeff Sharkey549aa162009-05-21 01:33:30 -0700165 private static final boolean INCLUDE_PROFILE_ACTION = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700166
Jeff Sharkey39261272009-06-03 19:15:09 -0700167 private static final int TOKEN_SUMMARY = 1;
168 private static final int TOKEN_SOCIAL = 2;
169 private static final int TOKEN_DATA = 3;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700170
Jeff Sharkey3f177592009-05-18 15:23:12 -0700171 /** Message to show when no activity is found to perform an action */
172 // TODO: move this value into a resources string
173 private static final String NOT_FOUND = "Couldn't find an app to handle this action";
174
Jeff Sharkey549aa162009-05-21 01:33:30 -0700175 /**
176 * Prepare a fast-track window to show in the given {@link Context}.
177 */
178 public FastTrackWindow(Context context) {
179 mContext = new ContextThemeWrapper(context, R.style.FastTrack);
180 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
181 mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700182
Jeff Sharkey549aa162009-05-21 01:33:30 -0700183 mWindow = PolicyManager.makeNewWindow(mContext);
184 mWindow.setCallback(this);
185 mWindow.setWindowManager(mWindowManager, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700186
Jeff Sharkey549aa162009-05-21 01:33:30 -0700187 mWindow.setContentView(R.layout.fasttrack);
188
Jeff Sharkey39261272009-06-03 19:15:09 -0700189 mArrowUp = mWindow.findViewById(R.id.arrow_up);
190 mArrowDown = mWindow.findViewById(R.id.arrow_down);
191
Jeff Sharkey549aa162009-05-21 01:33:30 -0700192 mPhoto = (ImageView)mWindow.findViewById(R.id.photo);
193 mPresence = (ImageView)mWindow.findViewById(R.id.presence);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700194 mContent = (TextView)mWindow.findViewById(R.id.content);
195 mPublished = (TextView)mWindow.findViewById(R.id.published);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700196 mTrack = (ViewGroup)mWindow.findViewById(R.id.fasttrack);
Jeff Sharkey39261272009-06-03 19:15:09 -0700197 mTrackScroll = (HorizontalScrollView)mWindow.findViewById(R.id.scroll);
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700198 mResolveList = (ListView)mWindow.findViewById(android.R.id.list);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700199
200 // TODO: move generation of mime-type cache to more-efficient place
201 generateMappingCache();
202
Jeff Sharkey3f177592009-05-18 15:23:12 -0700203 }
204
205 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700206 * Prepare a fast-track window to show in the given {@link Context}, and
207 * notify the given {@link OnDismissListener} each time this dialog is
208 * dismissed.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700209 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700210 public FastTrackWindow(Context context, OnDismissListener dismissListener) {
211 this(context);
212 mDismissListener = dismissListener;
213 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700214
Jeff Sharkey549aa162009-05-21 01:33:30 -0700215 /**
216 * Generate {@link MappingCache} specifically for fast-track windows. This
217 * cache knows how to display {@link CommonDataKinds#PACKAGE_COMMON} data
218 * types using generic icons.
219 */
220 private void generateMappingCache() {
221 mMappingCache = MappingCache.createAndFill(mContext);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700222
Jeff Sharkey549aa162009-05-21 01:33:30 -0700223 Resources res = mContext.getResources();
224 Mapping mapping;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700225
Jeff Sharkey549aa162009-05-21 01:33:30 -0700226 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Aggregates.CONTENT_ITEM_TYPE);
227 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.ic_contacts_details);
228 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700229
Jeff Sharkey549aa162009-05-21 01:33:30 -0700230 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Phone.CONTENT_ITEM_TYPE);
Jeff Sharkey39261272009-06-03 19:15:09 -0700231 mapping.summaryColumn = Phone.TYPE;
232 mapping.detailColumn = Phone.NUMBER;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700233 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_call);
234 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700235
Jeff Sharkey549aa162009-05-21 01:33:30 -0700236 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, MIME_SMS_ADDRESS);
Jeff Sharkey39261272009-06-03 19:15:09 -0700237 mapping.summaryColumn = Phone.TYPE;
238 mapping.detailColumn = Phone.NUMBER;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700239 mapping.icon = BitmapFactory.decodeResource(res, R.drawable.sym_action_sms);
240 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700241
Jeff Sharkey549aa162009-05-21 01:33:30 -0700242 mapping = new Mapping(CommonDataKinds.PACKAGE_COMMON, Email.CONTENT_ITEM_TYPE);
Jeff Sharkey39261272009-06-03 19:15:09 -0700243 mapping.summaryColumn = Email.TYPE;
244 mapping.detailColumn = Email.DATA;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700245 mapping.icon = BitmapFactory.decodeResource(res, android.R.drawable.sym_action_email);
246 mMappingCache.addMapping(mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700247
Jeff Sharkey549aa162009-05-21 01:33:30 -0700248 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700249
Jeff Sharkey549aa162009-05-21 01:33:30 -0700250 /**
251 * Start showing a fast-track window for the given {@link Aggregate#_ID}
252 * pointing towards the given location.
253 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700254 public void show(Uri aggUri, Rect anchor) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700255 if (mShowing || mQuerying) {
256 Log.w(TAG, "already in process of showing");
257 return;
258 }
259
260 mAggId = ContentUris.parseId(aggUri);
Jeff Sharkey39261272009-06-03 19:15:09 -0700261 mAnchor = new Rect(anchor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700262 mQuerying = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700263
Jeff Sharkey39261272009-06-03 19:15:09 -0700264 Uri aggSummary = ContentUris.withAppendedId(
265 ContactsContract.Aggregates.CONTENT_SUMMARY_URI, mAggId);
266 Uri aggSocial = ContentUris.withAppendedId(
267 SocialContract.Activities.CONTENT_AGGREGATE_STATUS_URI, mAggId);
268 Uri aggData = Uri.withAppendedPath(aggUri,
Jeff Sharkey549aa162009-05-21 01:33:30 -0700269 ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700270
Jeff Sharkey39261272009-06-03 19:15:09 -0700271 // Start data query in background
Jeff Sharkey549aa162009-05-21 01:33:30 -0700272 mHandler = new NotifyingAsyncQueryHandler(mContext, this);
Jeff Sharkey39261272009-06-03 19:15:09 -0700273 mHandler.startQuery(TOKEN_SUMMARY, null, aggSummary, null, null, null, null);
274 mHandler.startQuery(TOKEN_SOCIAL, null, aggSocial, null, null, null, null);
275 mHandler.startQuery(TOKEN_DATA, null, aggData, null, null, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700276
277 }
278
279 /**
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700280 * Show the correct callout arrow based on a {@link R.id} reference.
281 */
282 private void showArrow(int whichArrow, int requestedX) {
283 final View showArrow = (whichArrow == R.id.arrow_up) ? mArrowUp : mArrowDown;
284 final View hideArrow = (whichArrow == R.id.arrow_up) ? mArrowDown : mArrowUp;
Jeff Sharkey39261272009-06-03 19:15:09 -0700285
286 final int arrowWidth = mArrowUp.getMeasuredWidth();
287
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700288 showArrow.setVisibility(View.VISIBLE);
289 LinearLayout.LayoutParams param = (LinearLayout.LayoutParams)showArrow.getLayoutParams();
Jeff Sharkey39261272009-06-03 19:15:09 -0700290 param.leftMargin = requestedX - arrowWidth / 2;
291
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700292 hideArrow.setVisibility(View.INVISIBLE);
293 }
294
295 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700296 * Actual internal method to show this fast-track window. Called only by
297 * {@link #considerShowing()} when all data requirements have been met.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700298 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700299 private void showInternal() {
300 mDecor = mWindow.getDecorView();
301 WindowManager.LayoutParams l = mWindow.getAttributes();
Jeff Sharkey39261272009-06-03 19:15:09 -0700302
303 l.width = WindowManager.LayoutParams.FILL_PARENT;
304 l.height = WindowManager.LayoutParams.WRAP_CONTENT;
305
306 // Force layout measuring pass so we have baseline numbers
307 mDecor.measure(l.width, l.height);
308
309 final int blockHeight = mDecor.getMeasuredHeight();
310 final int arrowHeight = mArrowUp.getHeight();
311
Jeff Sharkey549aa162009-05-21 01:33:30 -0700312 l.gravity = Gravity.TOP | Gravity.LEFT;
313 l.x = 0;
Jeff Sharkey39261272009-06-03 19:15:09 -0700314
315 if (mAnchor.top > blockHeight) {
316 // Show downwards callout when enough room, aligning bottom block
317 // edge with top of anchor area, and adjusting to inset arrow.
318 showArrow(R.id.arrow_down, mAnchor.centerX());
319 l.y = mAnchor.top - blockHeight + arrowHeight;
320
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700321 } else {
Jeff Sharkey39261272009-06-03 19:15:09 -0700322 // Otherwise show upwards callout, aligning block top with bottom of
323 // anchor area, and adjusting to inset arrow.
324 showArrow(R.id.arrow_up, mAnchor.centerX());
325 l.y = mAnchor.bottom - arrowHeight;
326
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700327 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700328
329 l.dimAmount = 0.6f;
330 l.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND
331 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
Jeff Sharkey39261272009-06-03 19:15:09 -0700332 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
Jeff Sharkey549aa162009-05-21 01:33:30 -0700333 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
334 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
335
336 mWindowManager.addView(mDecor, l);
337 mShowing = true;
338 mQuerying = false;
339 }
340
341 /**
342 * Dismiss this fast-track window if showing.
343 */
344 public void dismiss() {
345 if (!mQuerying && !mShowing) {
346 Log.d(TAG, "not visible, ignore");
347 return;
348 }
349
350 // Cancel any pending queries
Jeff Sharkey39261272009-06-03 19:15:09 -0700351 mHandler.cancelOperation(TOKEN_SUMMARY);
352 mHandler.cancelOperation(TOKEN_SOCIAL);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700353 mHandler.cancelOperation(TOKEN_DATA);
354
355 // Reset all views to prepare for possible recycling
356 mPhoto.setImageResource(R.drawable.ic_contact_list_picture);
Jeff Sharkey39261272009-06-03 19:15:09 -0700357 mPresence.setImageDrawable(null);
358 mPublished.setText(null);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700359 mContent.setText(null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700360
Jeff Sharkey39261272009-06-03 19:15:09 -0700361 mDisplayName = null;
362 mSocialTitle = null;
363
364 // Clear track actions and scroll to hard left
Jeff Sharkey549aa162009-05-21 01:33:30 -0700365 mActions.clear();
366 mTrack.removeAllViews();
Jeff Sharkey39261272009-06-03 19:15:09 -0700367 mTrackScroll.fullScroll(View.FOCUS_LEFT);
368 mWasDownArrow = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700369
Jeff Sharkey39261272009-06-03 19:15:09 -0700370 showResolveList(View.GONE);
371
372 mHasSocial = false;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700373 mHasActions = false;
374
375 if (mDecor == null || !mShowing) {
376 Log.d(TAG, "not showing, ignore");
377 return;
378 }
379
380 mWindowManager.removeView(mDecor);
381 mDecor = null;
382 mWindow.closeAllPanels();
383 mShowing = false;
384
385 // Notify any listeners that we've been dismissed
386 if (mDismissListener != null) {
387 mDismissListener.onDismiss(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700388 }
389 }
390
Jeff Sharkey549aa162009-05-21 01:33:30 -0700391 /**
392 * Returns true if this fast-track window is showing or querying.
393 */
394 public boolean isShowing() {
395 return mShowing || mQuerying;
396 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700397
Jeff Sharkey549aa162009-05-21 01:33:30 -0700398 /**
399 * Consider showing this window, which will only call through to
400 * {@link #showInternal()} when all data items are present.
401 */
402 private synchronized void considerShowing() {
Jeff Sharkey39261272009-06-03 19:15:09 -0700403 if (mHasSummary && mHasSocial && mHasActions && !mShowing) {
404 // Now that all queries have been finished, build summary string.
405 mBuilder.clear();
406 mBuilder.append(mDisplayName);
407 mBuilder.append(" ");
408 mBuilder.append(mSocialTitle);
409 mBuilder.setSpan(mStyleBold, 0, mDisplayName.length(), 0);
410 mBuilder.setSpan(mStyleBlack, 0, mDisplayName.length(), 0);
411 mContent.setText(mBuilder);
412
Jeff Sharkey549aa162009-05-21 01:33:30 -0700413 showInternal();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700414 }
415 }
416
417 /** {@inheritDoc} */
418 public void onQueryComplete(int token, Object cookie, Cursor cursor) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700419 if (cursor == null) {
420 return;
Jeff Sharkey39261272009-06-03 19:15:09 -0700421 } else if (token == TOKEN_SUMMARY) {
422 handleSummary(cursor);
423 } else if (token == TOKEN_SOCIAL) {
424 handleSocial(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700425 } else if (token == TOKEN_DATA) {
426 handleData(cursor);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700427 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700428 }
429
430 /**
Jeff Sharkey39261272009-06-03 19:15:09 -0700431 * Handle the result from the {@link TOKEN_SUMMARY} query.
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700432 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700433 private void handleSummary(Cursor cursor) {
434 final int colDisplayName = cursor.getColumnIndex(Aggregates.DISPLAY_NAME);
435 final int colStatus = cursor.getColumnIndex(PresenceColumns.PRESENCE_STATUS);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700436
437 if (cursor.moveToNext()) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700438 mDisplayName = cursor.getString(colDisplayName);
439
440 int status = cursor.getInt(colStatus);
441 int statusRes = Presence.getPresenceIconResourceId(status);
442 mPresence.setImageResource(statusRes);
443 }
444
445 mHasSummary = true;
446 considerShowing();
447 }
448
449 /**
450 * Handle the result from the {@link TOKEN_SOCIAL} query.
451 */
452 private void handleSocial(Cursor cursor) {
453 final int colTitle = cursor.getColumnIndex(Activities.TITLE);
454 final int colPublished = cursor.getColumnIndex(Activities.PUBLISHED);
455
456 if (cursor.moveToNext()) {
457 mSocialTitle = cursor.getString(colTitle);
458
459 long published = cursor.getLong(colPublished);
460 CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
461 System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
462 mPublished.setText(relativePublished);
Jeff Sharkey80a193a2009-05-21 14:18:18 -0700463
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700464 }
465
Jeff Sharkey39261272009-06-03 19:15:09 -0700466 mHasSocial = true;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700467 considerShowing();
468 }
469
470 /**
471 * Description of a specific, actionable {@link Data#_ID} item. May have a
472 * {@link Mapping} associated with it to find {@link RemoteViews} or icon,
473 * and may have built a summary of itself for UI display.
474 */
475 private class ActionInfo {
476 long dataId;
477 String packageName;
478 String mimeType;
479
480 Mapping mapping;
Jeff Sharkey39261272009-06-03 19:15:09 -0700481 CharSequence summaryValue;
482 String detailValue;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700483
484 /**
485 * Create an action from common {@link Data} elements.
486 */
487 public ActionInfo(long dataId, String packageName, String mimeType) {
488 this.dataId = dataId;
489 this.packageName = packageName;
490 this.mimeType = mimeType;
491 }
492
493 /**
494 * Attempt to find a {@link Mapping} for the package and mime-type
495 * defined by this action. Returns true if one was found.
496 */
497 public boolean findMapping(MappingCache cache) {
498 mapping = cache.findMapping(packageName, mimeType);
499 return (mapping != null);
500 }
501
502 /**
503 * Given a {@link Cursor} pointed at the {@link Data} row associated
504 * with this action, use the {@link Mapping} to build a text summary.
505 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700506 public void buildDetails(Cursor cursor) {
507 if (mapping == null) return;
508
509 // Try finding common display label for this item, otherwise fall
510 // back to reading from defined summary column.
511 summaryValue = ContactsUtils.getDisplayLabel(mContext, mimeType, cursor);
512 if (summaryValue == null && mapping.summaryColumn != null) {
513 summaryValue = cursor.getString(cursor.getColumnIndex(mapping.summaryColumn));
514 }
515
516 // Read detailed value, if mapping was defined
517 if (mapping.detailColumn != null) {
518 detailValue = cursor.getString(cursor.getColumnIndex(mapping.detailColumn));
Jeff Sharkey549aa162009-05-21 01:33:30 -0700519 }
520 }
521
522 /**
523 * Build an {@link Intent} that will perform this action.
524 */
525 public Intent buildIntent() {
526 // Handle well-known mime-types with special care
527 if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700528 Uri callUri = Uri.parse("tel:" + Uri.encode(detailValue));
Jeff Sharkey549aa162009-05-21 01:33:30 -0700529 return new Intent(Intent.ACTION_DIAL, callUri);
530
531 } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700532 Uri smsUri = Uri.fromParts("smsto", detailValue, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700533 return new Intent(Intent.ACTION_SENDTO, smsUri);
534
535 } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700536 Uri mailUri = Uri.fromParts("mailto", detailValue, null);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700537 return new Intent(Intent.ACTION_SENDTO, mailUri);
538
539 }
540
541 // Otherwise fall back to default VIEW action
542 Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
543
544 Intent intent = new Intent(Intent.ACTION_VIEW);
545 intent.setData(dataUri);
546
547 return intent;
548 }
549 }
550
551 /**
Jeff Sharkey39261272009-06-03 19:15:09 -0700552 * Provide a strongly-typed {@link LinkedList} that holds a list of
553 * {@link ActionInfo} objects.
554 */
555 private class ActionList extends LinkedList<ActionInfo> {
556 }
557
558 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700559 * Provide a simple way of collecting one or more {@link ActionInfo} objects
560 * under a mime-type key.
561 */
Jeff Sharkey39261272009-06-03 19:15:09 -0700562 private class ActionMap extends HashMap<String, ActionList> {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700563 private void collect(String mimeType, ActionInfo info) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700564 // Create list for this mimetype when needed
565 ActionList collectList = get(mimeType);
566 if (collectList == null) {
567 collectList = new ActionList();
568 put(mimeType, collectList);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700569 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700570 collectList.add(info);
571 }
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700572 }
573
574 /**
575 * Handle the result from the {@link TOKEN_DATA} query.
576 */
577 private void handleData(Cursor cursor) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700578 final int colId = cursor.getColumnIndex(Data._ID);
Jeff Sharkey06bd5a82009-06-15 20:06:48 -0700579 final int colPackage = cursor.getColumnIndex(Contacts.PACKAGE);
Jeff Sharkey39261272009-06-03 19:15:09 -0700580 final int colMimeType = cursor.getColumnIndex(Data.MIMETYPE);
581 final int colPhoto = cursor.getColumnIndex(Photo.PHOTO);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700582
Jeff Sharkey549aa162009-05-21 01:33:30 -0700583 ActionInfo info;
584
585 // Add the profile shortcut action if requested
586 if (INCLUDE_PROFILE_ACTION) {
587 final String mimeType = Aggregates.CONTENT_ITEM_TYPE;
588 info = new ActionInfo(mAggId, CommonDataKinds.PACKAGE_COMMON, mimeType);
589 if (info.findMapping(mMappingCache)) {
590 mActions.collect(mimeType, info);
591 }
592 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700593
594 while (cursor.moveToNext()) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700595 final long dataId = cursor.getLong(colId);
596 final String packageName = cursor.getString(colPackage);
597 final String mimeType = cursor.getString(colMimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700598
Jeff Sharkey549aa162009-05-21 01:33:30 -0700599 // Handle when a photo appears in the various data items
600 // TODO: accept a photo only if its marked as primary
601 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700602 byte[] photoBlob = cursor.getBlob(colPhoto);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700603 Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700604 mPhoto.setImageBitmap(photoBitmap);
Jeff Sharkey69126682009-05-18 21:33:41 -0700605 continue;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700606 }
607
Jeff Sharkey549aa162009-05-21 01:33:30 -0700608 // Build an action for this data entry, find a mapping to a UI
609 // element, build its summary from the cursor, and collect it along
610 // with all others of this mime-type.
611 info = new ActionInfo(dataId, packageName, mimeType);
612 if (info.findMapping(mMappingCache)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700613 info.buildDetails(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700614 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700615 }
616
Jeff Sharkey549aa162009-05-21 01:33:30 -0700617 // If phone number, also insert as text message action
618 if (Phones.CONTENT_ITEM_TYPE.equals(mimeType)) {
619 info = new ActionInfo(dataId, packageName, MIME_SMS_ADDRESS);
620 if (info.findMapping(mMappingCache)) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700621 info.buildDetails(cursor);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700622 mActions.collect(info.mimeType, info);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700623 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700624 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700625 }
626
627 cursor.close();
628
Jeff Sharkey549aa162009-05-21 01:33:30 -0700629 // Turn our list of actions into UI elements, starting with common types
630 Set<String> containedTypes = mActions.keySet();
631 for (String mimeType : ORDERED_MIMETYPES) {
632 if (containedTypes.contains(mimeType)) {
633 mTrack.addView(inflateAction(mimeType));
634 containedTypes.remove(mimeType);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700635 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700636 }
637
Jeff Sharkey549aa162009-05-21 01:33:30 -0700638 // Then continue with remaining mime-types in alphabetical order
639 String[] remainingTypes = containedTypes.toArray(new String[containedTypes.size()]);
640 Arrays.sort(remainingTypes);
641 for (String mimeType : remainingTypes) {
642 mTrack.addView(inflateAction(mimeType));
643 }
644
645 mHasActions = true;
646 considerShowing();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700647 }
648
649 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700650 * Inflate the in-track view for the action of the given mime-type. Will use
651 * the icon provided by the {@link Mapping}.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700652 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700653 private View inflateAction(String mimeType) {
654 ImageView view = (ImageView)mInflater.inflate(R.layout.fasttrack_item, mTrack, false);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700655
Jeff Sharkey549aa162009-05-21 01:33:30 -0700656 // Add direct intent if single child, otherwise flag for multiple
Jeff Sharkey39261272009-06-03 19:15:09 -0700657 ActionList children = mActions.get(mimeType);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700658 ActionInfo firstInfo = children.get(0);
Jeff Sharkey39261272009-06-03 19:15:09 -0700659 if (children.size() == 1) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700660 view.setTag(firstInfo.buildIntent());
Jeff Sharkey39261272009-06-03 19:15:09 -0700661 } else {
662 view.setTag(children);
663 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700664
Jeff Sharkey549aa162009-05-21 01:33:30 -0700665 // Set icon and listen for clicks
666 view.setImageBitmap(firstInfo.mapping.icon);
667 view.setOnClickListener(this);
668 return view;
669 }
Jeff Sharkey39261272009-06-03 19:15:09 -0700670
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700671 /** {@inheritDoc} */
672 public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
673 // Pass list item clicks along so that Intents are handled uniformly
674 onClick(view);
675 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700676
Jeff Sharkey39261272009-06-03 19:15:09 -0700677 /**
678 * Flag indicating if {@link #mArrowDown} was visible during the last call
679 * to {@link #showResolveList(int)}. Used to decide during a later call if
680 * the arrow should be restored.
681 */
682 private boolean mWasDownArrow = false;
683
684 /**
685 * Helper for showing and hiding {@link #mResolveList}, which will correctly
686 * manage {@link #mArrowDown} as needed.
687 */
688 private void showResolveList(int visibility) {
689 // Show or hide the resolve list if needed
690 if (mResolveList.getVisibility() != visibility) {
691 mResolveList.setVisibility(visibility);
692 }
693
694 if (visibility == View.VISIBLE) {
695 // If showing list, then hide and save state of down arrow
696 mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
697 mArrowDown.setVisibility(View.INVISIBLE);
698 } else {
699 // If hiding list, restore any down arrow state
700 mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
701 }
702
703 }
704
Jeff Sharkey549aa162009-05-21 01:33:30 -0700705 /** {@inheritDoc} */
706 public void onClick(View v) {
707 final Object tag = v.getTag();
708 if (tag instanceof Intent) {
Jeff Sharkey51ca5442009-05-21 16:12:18 -0700709 // Hide the resolution list, if present
Jeff Sharkey39261272009-06-03 19:15:09 -0700710 showResolveList(View.GONE);
711
Jeff Sharkey549aa162009-05-21 01:33:30 -0700712 // Incoming tag is concrete intent, so launch
713 try {
714 mContext.startActivity((Intent)tag);
715 } catch (ActivityNotFoundException e) {
716 Log.w(TAG, NOT_FOUND);
717 Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
718 }
Jeff Sharkey39261272009-06-03 19:15:09 -0700719 } else if (tag instanceof ActionList) {
720 // Incoming tag is a mime-type, so show resolution list
721 final ActionList children = (ActionList)tag;
722
723 // Show resolution list and set adapter
724 showResolveList(View.VISIBLE);
725
726 mResolveList.setOnItemClickListener(this);
727 mResolveList.setAdapter(new BaseAdapter() {
728 public int getCount() {
729 return children.size();
730 }
731
732 public Object getItem(int position) {
733 return children.get(position);
734 }
735
736 public long getItemId(int position) {
737 return position;
738 }
739
740 public View getView(int position, View convertView, ViewGroup parent) {
741 if (convertView == null) {
742 convertView = mInflater.inflate(R.layout.fasttrack_resolve_item, parent, false);
743 }
744
745 // Set action title based on summary value
746 ActionInfo info = (ActionInfo)getItem(position);
747
748 ImageView icon1 = (ImageView)convertView.findViewById(android.R.id.icon1);
749 TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
750 TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
751
752 icon1.setImageBitmap(info.mapping.icon);
753 text1.setText(info.summaryValue);
754 text2.setText(info.detailValue);
755
756 convertView.setTag(info.buildIntent());
757 return convertView;
758 }
759 });
760
761 // Make sure we resize to make room for ListView
762 onWindowAttributesChanged(mWindow.getAttributes());
Jeff Sharkey549aa162009-05-21 01:33:30 -0700763
764 }
765 }
766
767 /** {@inheritDoc} */
768 public boolean dispatchKeyEvent(KeyEvent event) {
Jeff Sharkey39261272009-06-03 19:15:09 -0700769 if (event.getAction() == KeyEvent.ACTION_DOWN
770 && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
771 // Back key will first dismiss any expanded resolve list, otherwise
772 // it will close the entire dialog.
773 if (mResolveList.getVisibility() == View.VISIBLE) {
774 showResolveList(View.GONE);
775 } else {
776 dismiss();
777 }
778 return true;
779 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700780 return mWindow.superDispatchKeyEvent(event);
781 }
782
783 /** {@inheritDoc} */
784 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
785 // TODO: make this window accessible
786 return false;
787 }
788
789 /** {@inheritDoc} */
790 public boolean dispatchTouchEvent(MotionEvent event) {
791 if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
792 dismiss();
793 return true;
794 }
795 return mWindow.superDispatchTouchEvent(event);
796 }
797
798 /** {@inheritDoc} */
799 public boolean dispatchTrackballEvent(MotionEvent event) {
800 return mWindow.superDispatchTrackballEvent(event);
801 }
802
803 /** {@inheritDoc} */
804 public void onContentChanged() {
805 }
806
807 /** {@inheritDoc} */
808 public boolean onCreatePanelMenu(int featureId, Menu menu) {
809 return false;
810 }
811
812 /** {@inheritDoc} */
813 public View onCreatePanelView(int featureId) {
814 return null;
815 }
816
817 /** {@inheritDoc} */
818 public boolean onMenuItemSelected(int featureId, MenuItem item) {
819 return false;
820 }
821
822 /** {@inheritDoc} */
823 public boolean onMenuOpened(int featureId, Menu menu) {
824 return false;
825 }
826
827 /** {@inheritDoc} */
828 public void onPanelClosed(int featureId, Menu menu) {
829 }
830
831 /** {@inheritDoc} */
832 public boolean onPreparePanel(int featureId, View view, Menu menu) {
833 return false;
834 }
835
836 /** {@inheritDoc} */
837 public boolean onSearchRequested() {
838 return false;
839 }
840
841 /** {@inheritDoc} */
842 public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams attrs) {
843 if (mDecor != null) {
844 mWindowManager.updateViewLayout(mDecor, attrs);
845 }
846 }
847
848 /** {@inheritDoc} */
849 public void onWindowFocusChanged(boolean hasFocus) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700850 }
851}