blob: 0cf9daf10c4fb17d78499d1606b7015fe336bad0 [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;
20import com.android.contacts.FloatyListView.FloatyWindow;
21import com.android.contacts.SocialStreamActivity.MappingCache;
22import com.android.contacts.SocialStreamActivity.MappingCache.Mapping;
23import com.android.providers.contacts2.ContactsContract;
Jeff Sharkeyecedf752009-05-18 22:07:40 -070024import com.android.providers.contacts2.ContactsContract.Aggregates;
Jeff Sharkey3f177592009-05-18 15:23:12 -070025import com.android.providers.contacts2.ContactsContract.CommonDataKinds;
26import com.android.providers.contacts2.ContactsContract.Data;
27import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Email;
28import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Im;
29import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Phone;
Jeff Sharkeyecedf752009-05-18 22:07:40 -070030import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Photo;
Jeff Sharkey3f177592009-05-18 15:23:12 -070031import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Postal;
32
33import android.content.ActivityNotFoundException;
34import android.content.ContentUris;
35import android.content.Context;
36import android.content.Intent;
37import android.content.res.Resources;
38import android.database.Cursor;
Jeff Sharkeyecedf752009-05-18 22:07:40 -070039import android.graphics.Bitmap;
40import android.graphics.BitmapFactory;
Jeff Sharkey3f177592009-05-18 15:23:12 -070041import android.net.Uri;
42import android.util.Log;
43import android.view.Gravity;
44import android.view.LayoutInflater;
45import android.view.View;
46import android.view.ViewGroup;
47import android.view.ViewTreeObserver;
48import android.view.View.OnClickListener;
49import android.view.ViewTreeObserver.OnScrollChangedListener;
50import android.widget.AbsListView;
51import android.widget.ImageView;
52import android.widget.LinearLayout;
53import android.widget.ListView;
54import android.widget.PopupWindow;
55import android.widget.TextView;
56import android.widget.Toast;
57import android.widget.AbsListView.OnScrollListener;
58import android.widget.Gallery.LayoutParams;
59
60import java.lang.ref.WeakReference;
61import java.util.ArrayList;
62import java.util.Collections;
63import java.util.Comparator;
64import java.util.HashMap;
65import java.util.Iterator;
66import java.util.PriorityQueue;
67
68/**
69 * {@link PopupWindow} that shows fast-track details for a specific aggregate.
70 * This window implements {@link FloatyWindow} so that it can be quickly
71 * repositioned by someone like {@link FloatyListView}.
72 */
73public class FastTrackWindow extends PopupWindow implements QueryCompleteListener, FloatyWindow {
74 private static final String TAG = "FastTrackWindow";
75
76 private Context mContext;
77 private View mParent;
78
79 /** Mapping cache from mime-type to icons and actions */
80 private MappingCache mMappingCache;
81
82 private ViewGroup mContent;
83
84 private Uri mDataUri;
85 private NotifyingAsyncQueryHandler mHandler;
86
87 private boolean mShowing = false;
88 private boolean mHasPosition = false;
Jeff Sharkeyecedf752009-05-18 22:07:40 -070089 private boolean mHasDisplayName = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -070090 private boolean mHasData = false;
91
92 public static final int ICON_SIZE = 42;
93 public static final int ICON_PADDING = 3;
94 private static final int VERTICAL_OFFSET = 74;
95
96 private int mFirstX;
97 private int mFirstY;
98
Jeff Sharkeyecedf752009-05-18 22:07:40 -070099 private static final int TOKEN_DISPLAY_NAME = 1;
100 private static final int TOKEN_DATA = 2;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700101
102 private static final int GRAVITY = Gravity.LEFT | Gravity.TOP;
103
104 /** Message to show when no activity is found to perform an action */
105 // TODO: move this value into a resources string
106 private static final String NOT_FOUND = "Couldn't find an app to handle this action";
107
108 /** List of default mime-type icons */
109 private static HashMap<String, Integer> sMimeIcons = new HashMap<String, Integer>();
110
111 /** List of mime-type sorting scores */
112 private static HashMap<String, Integer> sMimeScores = new HashMap<String, Integer>();
113
114 static {
115 sMimeIcons.put(Phone.CONTENT_ITEM_TYPE, android.R.drawable.sym_action_call);
116 sMimeIcons.put(Email.CONTENT_ITEM_TYPE, android.R.drawable.sym_action_email);
117 sMimeIcons.put(Im.CONTENT_ITEM_TYPE, android.R.drawable.sym_action_chat);
118// sMimeIcons.put(Phone.CONTENT_ITEM_TYPE, R.drawable.sym_action_sms);
119 sMimeIcons.put(Postal.CONTENT_ITEM_TYPE, R.drawable.sym_action_map);
120
121 // For scoring, put phone numbers and E-mail up front, and addresses last
122 sMimeScores.put(Phone.CONTENT_ITEM_TYPE, -200);
123 sMimeScores.put(Email.CONTENT_ITEM_TYPE, -100);
124 sMimeScores.put(Postal.CONTENT_ITEM_TYPE, 100);
125 }
126
127 /**
128 * Create a new fast-track window for the given aggregate, using the
129 * provided {@link MappingCache} for icon as needed.
130 */
131 public FastTrackWindow(Context context, View parent, Uri aggUri, MappingCache mappingCache) {
132 super(context);
133
134 final Resources resources = context.getResources();
135
136 mContext = context;
137 mParent = parent;
138
139 mMappingCache = mappingCache;
140
141 // Inflate content view
142 LayoutInflater inflater = (LayoutInflater)context
143 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
144 mContent = (ViewGroup)inflater.inflate(R.layout.fasttrack, null, false);
145
146 setContentView(mContent);
Jeff Sharkeye913e5e2009-05-18 17:51:13 -0700147// setAnimationStyle(android.R.style.Animation_LeftEdge);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700148
149 setBackgroundDrawable(resources.getDrawable(R.drawable.fasttrack));
150
151 setWidth(LayoutParams.WRAP_CONTENT);
152 setHeight(LayoutParams.WRAP_CONTENT);
153
154 setClippingEnabled(false);
155 setFocusable(false);
156
157 // Start data query in background
158 mDataUri = Uri.withAppendedPath(aggUri, ContactsContract.Aggregates.Data.CONTENT_DIRECTORY);
159
160 mHandler = new NotifyingAsyncQueryHandler(context, this);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700161 mHandler.startQuery(TOKEN_DISPLAY_NAME, null, aggUri, null, null, null, null);
162 mHandler.startQuery(TOKEN_DATA, null, mDataUri, null, null, null, null);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700163
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700164 // TODO: poll around for latest status message or location details
Jeff Sharkey3f177592009-05-18 15:23:12 -0700165 }
166
167 /**
168 * Consider showing this window, which requires both a given position and
169 * completed query results.
170 */
171 private synchronized void considerShowing() {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700172 if (mHasData && mHasPosition && mHasDisplayName && !mShowing) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700173 mShowing = true;
174 showAtLocation(mParent, GRAVITY, mFirstX, mFirstY);
175 }
176 }
177
178 /** {@inheritDoc} */
179 public void showAt(int x, int y) {
180 // Adjust vertical position by height
181 y -= VERTICAL_OFFSET;
182
183 // Show dialog or update existing location
184 if (!mShowing) {
185 mFirstX = x;
186 mFirstY = y;
187 mHasPosition = true;
188 considerShowing();
189 } else {
190 update(x, y, -1, -1, true);
191 }
192 }
193
194 /** {@inheritDoc} */
195 public void onQueryComplete(int token, Object cookie, Cursor cursor) {
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700196 if (cursor == null) {
197 return;
198 }
199 switch (token) {
200 case TOKEN_DISPLAY_NAME:
201 handleDisplayName(cursor);
202 break;
203 case TOKEN_DATA:
204 handleData(cursor);
205 break;
206 }
207 considerShowing();
208 }
209
210 /**
211 * Handle the result from the {@link TOKEN_DISPLAY_NAME} query.
212 */
213 private void handleDisplayName(Cursor cursor) {
214 final TextView displayname = (TextView)mContent.findViewById(R.id.displayname);
215 final int COL_DISPLAY_NAME = cursor.getColumnIndex(Aggregates.DISPLAY_NAME);
216
217 if (cursor.moveToNext()) {
218 String foundName = cursor.getString(COL_DISPLAY_NAME);
219 displayname.setText(foundName);
220 }
221
222 mHasDisplayName = true;
223 }
224
225 /**
226 * Handle the result from the {@link TOKEN_DATA} query.
227 */
228 private void handleData(Cursor cursor) {
229 final ImageView photo = (ImageView)mContent.findViewById(R.id.photo);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700230 final ViewGroup fastTrack = (ViewGroup)mContent.findViewById(R.id.fasttrack);
231
232 // Build list of actions for this contact, this could be done better in
233 // the future using an Adapter
234 ArrayList<ImageView> list = new ArrayList<ImageView>(cursor.getCount());
235
236 final int COL_ID = cursor.getColumnIndex(Data._ID);
237 final int COL_PACKAGE = cursor.getColumnIndex(Data.PACKAGE);
238 final int COL_MIMETYPE = cursor.getColumnIndex(Data.MIMETYPE);
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700239 final int COL_PHOTO = cursor.getColumnIndex(Photo.PHOTO);
240
241 boolean foundDisplayName = false;
242 boolean foundPhoto = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700243
244 while (cursor.moveToNext()) {
245 final long dataId = cursor.getLong(COL_ID);
246 final String packageName = cursor.getString(COL_PACKAGE);
247 final String mimeType = cursor.getString(COL_MIMETYPE);
248
Jeff Sharkeyecedf752009-05-18 22:07:40 -0700249 // Handle finding the photo among various return rows
250 if (!foundPhoto && Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
251 byte[] photoBlob = cursor.getBlob(COL_PHOTO);
252 Bitmap photoBitmap = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
253 photo.setImageBitmap(photoBitmap);
254 photo.setVisibility(View.VISIBLE);
255 foundPhoto = true;
256 }
257
Jeff Sharkey3f177592009-05-18 15:23:12 -0700258 ImageView action;
259
260 // First, try looking in mapping cache for possible icon match
261 Mapping mapping = mMappingCache.getMapping(packageName, mimeType);
262 if (mapping != null && mapping.icon != null) {
263 action = new ImageView(mContext);
264 action.setImageBitmap(mapping.icon);
265
266 } else if (sMimeIcons.containsKey(mimeType)) {
267 // Otherwise fall back to generic icons
268 int icon = sMimeIcons.get(mimeType);
269 action = new ImageView(mContext);
270 action.setImageResource(icon);
271
272 } else {
273 // No icon found, so don't insert any action button
Jeff Sharkey69126682009-05-18 21:33:41 -0700274 continue;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700275
276 }
277
278 LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ICON_SIZE, ICON_SIZE);
279 params.rightMargin = ICON_PADDING;
280 action.setLayoutParams(params);
281
282 // Find the sorting score for this mime-type, otherwise allocate a
283 // new one to make sure the same types are grouped together.
284 if (!sMimeScores.containsKey(mimeType)) {
285 sMimeScores.put(mimeType, sMimeScores.size());
286 }
287
288 int mimeScore = sMimeScores.get(mimeType);
289 action.setTag(mimeScore);
290
291 final Intent intent = buildIntentForMime(dataId, mimeType, cursor);
292 action.setOnClickListener(new OnClickListener() {
293 public void onClick(View v) {
294 try {
295 mContext.startActivity(intent);
296 } catch (ActivityNotFoundException e) {
297 Log.w(TAG, NOT_FOUND, e);
298 Toast.makeText(mContext, NOT_FOUND, Toast.LENGTH_SHORT).show();
299 }
300 }
301 });
302
303 list.add(action);
304 }
305
306 cursor.close();
307
308 // Sort the final list based on mime-type scores
309 Collections.sort(list, new Comparator<ImageView>() {
310 public int compare(ImageView object1, ImageView object2) {
311 return (Integer)object1.getTag() - (Integer)object2.getTag();
312 }
313 });
314
315 for (ImageView action : list) {
316 fastTrack.addView(action);
317 }
318
319 mHasData = true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700320 }
321
322 /**
323 * Build an {@link Intent} that will trigger the action described by the
324 * given {@link Cursor} and mime-type.
325 */
326 public Intent buildIntentForMime(long dataId, String mimeType, Cursor cursor) {
327 if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
328 final String data = cursor.getString(cursor.getColumnIndex(Phone.NUMBER));
329 Uri callUri = Uri.parse("tel:" + Uri.encode(data));
330 return new Intent(Intent.ACTION_DIAL, callUri);
331
332 } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
333 final String data = cursor.getString(cursor.getColumnIndex(Email.DATA));
334 return new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", data, null));
335
336// } else if (CommonDataKinds.Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
337// return new Intent(Intent.ACTION_SENDTO, constructImToUrl(host, data));
338
339 } else if (CommonDataKinds.Postal.CONTENT_ITEM_TYPE.equals(mimeType)) {
340 final String data = cursor.getString(cursor.getColumnIndex(Postal.DATA));
341 Uri mapsUri = Uri.parse("geo:0,0?q=" + Uri.encode(data));
342 return new Intent(Intent.ACTION_VIEW, mapsUri);
343
344 }
345
346 // Otherwise fall back to default VIEW action
347 Uri dataUri = ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId);
348
349 Intent intent = new Intent(Intent.ACTION_VIEW);
350 intent.setData(dataUri);
351
352 return intent;
353 }
354}