blob: a3adf7672f0e6e84f5aed8a54b4b417a6217eee5 [file] [log] [blame]
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -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
Jeff Sharkey3f177592009-05-18 15:23:12 -070019import com.android.contacts.EdgeTriggerView.EdgeTriggerListener;
Jeff Sharkey802b2052009-08-04 14:21:06 -070020import com.android.contacts.ui.FastTrackWindow;
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -070021
Jeff Sharkey3f177592009-05-18 15:23:12 -070022import org.xmlpull.v1.XmlPullParser;
23import org.xmlpull.v1.XmlPullParserException;
24
25import android.app.ListActivity;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.Context;
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -070029import android.content.Intent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070030import android.content.pm.ApplicationInfo;
31import android.content.pm.PackageManager;
32import android.content.pm.PackageManager.NameNotFoundException;
Jeff Sharkey549aa162009-05-21 01:33:30 -070033import android.content.res.Resources;
Jeff Sharkey3f177592009-05-18 15:23:12 -070034import android.content.res.TypedArray;
35import android.database.Cursor;
36import android.graphics.Bitmap;
37import android.graphics.BitmapFactory;
38import android.graphics.Canvas;
39import android.graphics.Paint;
40import android.graphics.PaintFlagsDrawFilter;
Jeff Sharkey3f177592009-05-18 15:23:12 -070041import android.graphics.Rect;
Jeff Sharkey3f177592009-05-18 15:23:12 -070042import android.net.Uri;
43import android.os.Bundle;
Evan Millar66388be2009-05-28 15:41:07 -070044import android.provider.ContactsContract;
Dmitri Plotnikove1cd6792009-07-27 20:28:17 -070045import android.provider.ContactsContract.Contacts;
Evan Millar66388be2009-05-28 15:41:07 -070046import android.provider.ContactsContract.CommonDataKinds;
Evan Millar66388be2009-05-28 15:41:07 -070047import android.provider.ContactsContract.Data;
Jeff Sharkey802b2052009-08-04 14:21:06 -070048import android.provider.ContactsContract.Intents;
Dmitri Plotnikov39466592009-07-27 11:23:51 -070049import android.provider.ContactsContract.RawContacts;
Evan Millar66388be2009-05-28 15:41:07 -070050import android.provider.ContactsContract.CommonDataKinds.Photo;
51import android.provider.SocialContract.Activities;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -070052import android.text.SpannableStringBuilder;
Jeff Sharkey3f177592009-05-18 15:23:12 -070053import android.text.format.DateUtils;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -070054import android.text.style.StyleSpan;
Dmitri Plotnikov05f158f2009-05-21 11:37:15 -070055import android.text.style.UnderlineSpan;
Jeff Sharkey3f177592009-05-18 15:23:12 -070056import android.util.AttributeSet;
57import android.util.Log;
58import android.util.Xml;
Jeff Sharkey549aa162009-05-21 01:33:30 -070059import android.view.KeyEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070060import android.view.LayoutInflater;
61import android.view.View;
62import android.view.ViewGroup;
Jeff Sharkey549aa162009-05-21 01:33:30 -070063import android.view.View.OnClickListener;
Jeff Sharkey3f177592009-05-18 15:23:12 -070064import android.widget.CursorAdapter;
65import android.widget.ImageView;
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -070066import android.widget.ListView;
Dmitri Plotnikov39466592009-07-27 11:23:51 -070067import android.widget.RemoteViews;
Jeff Sharkey3f177592009-05-18 15:23:12 -070068import android.widget.TextView;
69
70import java.io.IOException;
71import java.util.HashMap;
Jeff Sharkey3f177592009-05-18 15:23:12 -070072import java.util.List;
73
Jeff Sharkey549aa162009-05-21 01:33:30 -070074public class SocialStreamActivity extends ListActivity implements OnClickListener, EdgeTriggerListener {
Jeff Sharkey3f177592009-05-18 15:23:12 -070075 private static final String TAG = "SocialStreamActivity";
76
77 private static final String[] PROJ_ACTIVITIES = new String[] {
78 Activities._ID,
Jeff Sharkeyc6ad3ab2009-07-21 19:30:15 -070079 Activities.RES_PACKAGE,
Jeff Sharkey3f177592009-05-18 15:23:12 -070080 Activities.MIMETYPE,
81 Activities.AUTHOR_CONTACT_ID,
Dmitri Plotnikove1cd6792009-07-27 20:28:17 -070082 RawContacts.CONTACT_ID,
83 Contacts.DISPLAY_NAME,
Jeff Sharkey3f177592009-05-18 15:23:12 -070084 Activities.PUBLISHED,
85 Activities.TITLE,
86 Activities.SUMMARY,
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -070087 Activities.THREAD_PUBLISHED,
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -070088 Activities.LINK,
89 Activities.THUMBNAIL,
Jeff Sharkey3f177592009-05-18 15:23:12 -070090 };
91
92 private static final int COL_ID = 0;
93 private static final int COL_PACKAGE = 1;
94 private static final int COL_MIMETYPE = 2;
95 private static final int COL_AUTHOR_CONTACT_ID = 3;
96 private static final int COL_AGGREGATE_ID = 4;
97 private static final int COL_DISPLAY_NAME = 5;
98 private static final int COL_PUBLISHED = 6;
99 private static final int COL_TITLE = 7;
100 private static final int COL_SUMMARY = 8;
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700101 private static final int COL_THREAD_PUBLISHED = 9;
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700102 private static final int COL_LINK = 10;
103 private static final int COL_THUMBNAIL = 11;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700104
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700105 public static final int PHOTO_SIZE = 54;
106 public static final int THUMBNAIL_SIZE = 54;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700107
Jeff Sharkey549aa162009-05-21 01:33:30 -0700108 private SocialAdapter mAdapter;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700109
Jeff Sharkey549aa162009-05-21 01:33:30 -0700110 private ListView mListView;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700111 private EdgeTriggerView mEdgeTrigger;
112 private FastTrackWindow mFastTrack;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700113 private MappingCache mMappingCache;
114
115 private static final boolean USE_GESTURE = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700116
117 private ContactsCache mContactsCache;
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700118
119 @Override
120 protected void onCreate(Bundle icicle) {
121 super.onCreate(icicle);
122
Jeff Sharkey3f177592009-05-18 15:23:12 -0700123 setContentView(R.layout.social_list);
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700124
Jeff Sharkey3f177592009-05-18 15:23:12 -0700125 mContactsCache = new ContactsCache(this);
126 mMappingCache = MappingCache.createAndFill(this);
127
128 Cursor cursor = managedQuery(Activities.CONTENT_URI, PROJ_ACTIVITIES, null, null);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700129 mAdapter = new SocialAdapter(this, cursor, mContactsCache, mMappingCache);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700130 mAdapter.setPhotoListener(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700131
132 setListAdapter(mAdapter);
133
Jeff Sharkey549aa162009-05-21 01:33:30 -0700134 mListView = getListView();
135 mFastTrack = new FastTrackWindow(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700136
Jeff Sharkey549aa162009-05-21 01:33:30 -0700137 if (USE_GESTURE) {
138 // Find and listen for edge triggers
139 mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
140 mEdgeTrigger.setOnEdgeTriggerListener(this);
141 }
142 }
143
144 /** {@inheritDoc} */
145 public void onClick(View v) {
146 // Clicked on photo, so show fast-track
Jeff Sharkey39261272009-06-03 19:15:09 -0700147 showFastTrack(v, (Long)v.getTag());
Jeff Sharkey3f177592009-05-18 15:23:12 -0700148 }
149
150 /** {@inheritDoc} */
151 public void onTrigger(float downX, float downY, int edge) {
152 // Find list item user triggered over
Jeff Sharkey549aa162009-05-21 01:33:30 -0700153 final int position = mListView.pointToPosition((int)downX, (int)downY);
154 if (position == ListView.INVALID_POSITION) return;
155
156 // Reverse to find the exact top of the triggered entry
157 final int index = position - mListView.getFirstVisiblePosition();
158 final View anchor = mListView.getChildAt(index);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700159
160 Cursor cursor = (Cursor)mAdapter.getItem(position);
161 long aggId = cursor.getLong(COL_AGGREGATE_ID);
162
Jeff Sharkey549aa162009-05-21 01:33:30 -0700163 showFastTrack(anchor, aggId);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700164
Jeff Sharkey549aa162009-05-21 01:33:30 -0700165 }
166
167 private int[] mLocation = new int[2];
Jeff Sharkey39261272009-06-03 19:15:09 -0700168 private Rect mRect = new Rect();
Jeff Sharkey549aa162009-05-21 01:33:30 -0700169
170 private void showFastTrack(View anchor, long aggId) {
Dmitri Plotnikove1cd6792009-07-27 20:28:17 -0700171 Uri aggUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, aggId);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700172
Jeff Sharkey549aa162009-05-21 01:33:30 -0700173 anchor.getLocationInWindow(mLocation);
Jeff Sharkey39261272009-06-03 19:15:09 -0700174 mRect.left = mLocation[0];
175 mRect.top = mLocation[1];
176 mRect.right = mRect.left + anchor.getWidth();
177 mRect.bottom = mRect.top + anchor.getHeight();
Jeff Sharkey549aa162009-05-21 01:33:30 -0700178
179 mFastTrack.dismiss();
Jeff Sharkey802b2052009-08-04 14:21:06 -0700180 mFastTrack.show(aggUri, mRect, Intents.MODE_MEDIUM);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700181 }
182
183 /** {@inheritDoc} */
184 @Override
185 public boolean onKeyDown(int keyCode, KeyEvent event) {
186 // Back key dismisses fast-track when its visible
187 if (keyCode == KeyEvent.KEYCODE_BACK && mFastTrack.isShowing()) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700188 mFastTrack.dismiss();
Jeff Sharkey549aa162009-05-21 01:33:30 -0700189 return true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700190 }
191
Jeff Sharkey549aa162009-05-21 01:33:30 -0700192 return super.onKeyDown(keyCode, event);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700193 }
194
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700195 @Override
196 protected void onListItemClick(ListView l, View v, int position, long id) {
197 Cursor cursor = (Cursor)getListAdapter().getItem(position);
198
199 // TODO check mime type and if it is supported, launch the corresponding app
200 String link = cursor.getString(COL_LINK);
201 if (link == null) {
202 return;
203 }
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700204 startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(link)));
205 }
206
Jeff Sharkey3f177592009-05-18 15:23:12 -0700207 /**
208 * List adapter for social stream data queried from
209 * {@link Activities#CONTENT_URI}.
210 */
211 private static class SocialAdapter extends CursorAdapter {
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700212 private final Context mContext;
213 private final LayoutInflater mInflater;
214 private final ContactsCache mContactsCache;
215 private final MappingCache mMappingCache;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700216 private final StyleSpan mTextStyleName;
Dmitri Plotnikov05f158f2009-05-21 11:37:15 -0700217 private final UnderlineSpan mTextStyleLink;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700218 private OnClickListener mPhotoListener;
219 private SpannableStringBuilder mBuilder = new SpannableStringBuilder();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700220
Jeff Sharkey39261272009-06-03 19:15:09 -0700221 public static final int TYPE_ACTIVITY = 0;
222 public static final int TYPE_REPLY = 1;
223
Jeff Sharkey3f177592009-05-18 15:23:12 -0700224 private static class SocialHolder {
225 ImageView photo;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700226 ImageView sourceIcon;
227 TextView content;
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700228 TextView summary;
229 ImageView thumbnail;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700230 TextView published;
231 }
232
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700233 public SocialAdapter(Context context, Cursor c, ContactsCache contactsCache,
234 MappingCache mappingCache) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700235 super(context, c, true);
236 mContext = context;
237 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
238 mContactsCache = contactsCache;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700239 mMappingCache = mappingCache;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700240 mTextStyleName = new StyleSpan(android.graphics.Typeface.BOLD);
Dmitri Plotnikov05f158f2009-05-21 11:37:15 -0700241 mTextStyleLink = new UnderlineSpan();
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700242 }
243
Jeff Sharkey549aa162009-05-21 01:33:30 -0700244 public void setPhotoListener(OnClickListener listener) {
245 mPhotoListener = listener;
246 }
247
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700248 @Override
249 public int getViewTypeCount() {
250 return 2;
251 }
252
253 @Override
254 public int getItemViewType(int position) {
255 Cursor cursor = (Cursor) getItem(position);
Jeff Sharkey39261272009-06-03 19:15:09 -0700256 return isReply(cursor) ? TYPE_ACTIVITY : TYPE_REPLY;
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700257 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700258
259 @Override
260 public void bindView(View view, Context context, Cursor cursor) {
261 SocialHolder holder = (SocialHolder)view.getTag();
262
Jeff Sharkey549aa162009-05-21 01:33:30 -0700263 long aggId = cursor.getLong(COL_AGGREGATE_ID);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700264 long contactId = cursor.getLong(COL_AUTHOR_CONTACT_ID);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700265 String name = cursor.getString(COL_DISPLAY_NAME);
266 String title = cursor.getString(COL_TITLE);
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700267 String summary = cursor.getString(COL_SUMMARY);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700268 long published = cursor.getLong(COL_PUBLISHED);
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700269 byte[] thumbnailBlob = cursor.getBlob(COL_THUMBNAIL);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700270
271 // TODO: trigger async query to find actual name and photo instead
272 // of using this lazy caching mechanism
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700273 Bitmap photo = mContactsCache.getPhoto(contactId);
274 if (photo != null) {
275 holder.photo.setImageBitmap(photo);
276 } else {
277 holder.photo.setImageResource(R.drawable.ic_contact_list_picture);
278 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700279 holder.photo.setTag(aggId);
280
281 mBuilder.clear();
282 mBuilder.append(name);
283 mBuilder.append(" ");
284 mBuilder.append(title);
285 mBuilder.setSpan(mTextStyleName, 0, name.length(), 0);
286 holder.content.setText(mBuilder);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700287
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700288 if (summary == null) {
289 holder.summary.setVisibility(View.GONE);
290 } else {
Jeff Sharkey39261272009-06-03 19:15:09 -0700291 mBuilder.clear();
292 mBuilder.append(summary);
293 mBuilder.setSpan(mTextStyleLink, 0, summary.length(), 0);
294 holder.summary.setText(mBuilder);
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700295 holder.summary.setVisibility(View.VISIBLE);
296 }
297
298 if (thumbnailBlob != null) {
299 Bitmap thumbnail =
300 BitmapFactory.decodeByteArray(thumbnailBlob, 0, thumbnailBlob.length);
301 holder.thumbnail.setImageBitmap(thumbnail);
302 holder.thumbnail.setVisibility(View.VISIBLE);
303 } else {
304 holder.thumbnail.setVisibility(View.GONE);
305 }
306
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700307 CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
308 System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700309 holder.published.setText(relativePublished);
310
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700311 if (holder.sourceIcon != null) {
312 String packageName = cursor.getString(COL_PACKAGE);
313 String mimeType = cursor.getString(COL_MIMETYPE);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700314 Mapping mapping = mMappingCache.findMapping(packageName, mimeType);
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700315 if (mapping != null && mapping.icon != null) {
316 holder.sourceIcon.setImageBitmap(mapping.icon);
317 } else {
318 holder.sourceIcon.setImageDrawable(null);
319 }
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700320 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700321 }
322
323 @Override
324 public View newView(Context context, Cursor cursor, ViewGroup parent) {
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700325 View view = mInflater.inflate(
326 isReply(cursor) ? R.layout.social_list_item_reply : R.layout.social_list_item,
327 parent, false);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700328
329 SocialHolder holder = new SocialHolder();
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700330 holder.photo = (ImageView) view.findViewById(R.id.photo);
331 holder.sourceIcon = (ImageView) view.findViewById(R.id.sourceIcon);
332 holder.content = (TextView) view.findViewById(R.id.content);
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700333 holder.summary = (TextView) view.findViewById(R.id.summary);
334 holder.thumbnail = (ImageView) view.findViewById(R.id.thumbnail);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700335 holder.published = (TextView) view.findViewById(R.id.published);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700336 view.setTag(holder);
337
Jeff Sharkey549aa162009-05-21 01:33:30 -0700338 if (!USE_GESTURE) {
339 holder.photo.setOnClickListener(mPhotoListener);
340 }
341
Jeff Sharkey3f177592009-05-18 15:23:12 -0700342 return view;
343 }
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700344
345 private boolean isReply(Cursor cursor) {
346
347 /*
348 * Comparing the message timestamp to the thread timestamp rather than checking the
349 * in_reply_to field. The rationale for this approach is that in the case when the
350 * original message to which the reply was posted is missing, we want to display
351 * the message as if it was an original; otherwise it would appear to be a reply
352 * to whatever message preceded it in the list. In the case when the original message
353 * of the thread is missing, the two timestamps will be the same.
354 */
355 long published = cursor.getLong(COL_PUBLISHED);
356 long threadPublished = cursor.getLong(COL_THREAD_PUBLISHED);
357 return published != threadPublished;
358 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700359 }
360
361 /**
Dmitri Plotnikov39466592009-07-27 11:23:51 -0700362 * Keep a cache that maps from {@link RawContacts#_ID} to {@link Photo#PHOTO}
Jeff Sharkey3f177592009-05-18 15:23:12 -0700363 * values.
364 */
365 private static class ContactsCache {
366 private static final String TAG = "ContactsCache";
367
368 private static final String[] PROJ_DETAILS = new String[] {
369 Data.MIMETYPE,
Dmitri Plotnikove1cd6792009-07-27 20:28:17 -0700370 Data.RAW_CONTACT_ID,
Jeff Sharkey3f177592009-05-18 15:23:12 -0700371 Photo.PHOTO,
372 };
373
374 private static final int COL_MIMETYPE = 0;
Dmitri Plotnikove1cd6792009-07-27 20:28:17 -0700375 private static final int COL_RAW_CONTACT_ID = 1;
Jeff Sharkey8da253a2009-05-18 21:23:19 -0700376 private static final int COL_PHOTO = 2;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700377
378 private HashMap<Long, Bitmap> mPhoto = new HashMap<Long, Bitmap>();
379
380 public ContactsCache(Context context) {
381 Log.d(TAG, "building ContactsCache...");
382
383 ContentResolver resolver = context.getContentResolver();
384 Cursor cursor = resolver.query(Data.CONTENT_URI, PROJ_DETAILS,
385 Data.MIMETYPE + "=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
386
387 while (cursor.moveToNext()) {
Dmitri Plotnikove1cd6792009-07-27 20:28:17 -0700388 long contactId = cursor.getLong(COL_RAW_CONTACT_ID);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700389 String mimeType = cursor.getString(COL_MIMETYPE);
390 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
391 byte[] photoBlob = cursor.getBlob(COL_PHOTO);
392 Bitmap photo = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
393 photo = Utilities.createBitmapThumbnail(photo, context, PHOTO_SIZE);
394
395 mPhoto.put(contactId, photo);
396 }
397 }
398
399 cursor.close();
400 Log.d(TAG, "done building ContactsCache");
401 }
402
403 public Bitmap getPhoto(long contactId) {
404 return mPhoto.get(contactId);
405 }
406 }
407
408 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700409 * Store a mapping from a package name and mime-type pair to a set of
410 * {@link RemoteViews}, default icon, and column to use from the
411 * {@link Data} table to use as a summary.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700412 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700413 public static class Mapping {
414 String packageName;
415 String mimeType;
416 String summaryColumn;
Jeff Sharkey39261272009-06-03 19:15:09 -0700417 String detailColumn;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700418 int remoteViewsRes;
419 Bitmap icon;
420
421 public Mapping() {
422 }
423
424 public Mapping(String packageName, String mimeType) {
425 this.packageName = packageName;
426 this.mimeType = mimeType;
427 }
428 }
429
430 /**
431 * Store a parsed <code>Mapping</code> object, which maps package and
432 * mime-type combinations to {@link RemoteViews} XML resources, default
433 * icons, and summary columns in the {@link Data} table.
434 */
435 public static class MappingCache extends HashMap<String, Mapping> {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700436 private static final String TAG = "MappingCache";
437
438 private static final String TAG_MAPPINGSET = "MappingSet";
439 private static final String TAG_MAPPING = "Mapping";
440
441 private static final String MAPPING_METADATA = "com.android.contacts.stylemap";
442
Jeff Sharkey3f177592009-05-18 15:23:12 -0700443
Jeff Sharkey549aa162009-05-21 01:33:30 -0700444 /**
445 * Only allow inflating through
446 * {@link MappingCache#createAndFill(Context)}.
447 */
Jeff Sharkey3f177592009-05-18 15:23:12 -0700448 private MappingCache() {
449 }
450
Jeff Sharkey549aa162009-05-21 01:33:30 -0700451 /**
452 * Add a {@link Mapping} instance to this cache, correctly using
453 * {@link #generateKey(String, String)} when storing.
454 */
Jeff Sharkey3f177592009-05-18 15:23:12 -0700455 public void addMapping(Mapping mapping) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700456 String hashKey = generateKey(mapping.packageName, mapping.mimeType);
457 put(hashKey, mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700458 }
459
460 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700461 * Generate a key used internally for mapping a specific package name
462 * and mime-type to a {@link Mapping}.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700463 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700464 private String generateKey(String packageName, String mimeType) {
465 return packageName + ";" + mimeType;
466 }
467
468 /**
469 * Find matching mapping for requested package and mime-type. Returns
470 * null if no mapping found.
471 */
472 public Mapping findMapping(String packageName, String mimeType) {
473 // Search for common mapping first
474 final String commonMapping = generateKey(CommonDataKinds.PACKAGE_COMMON, mimeType);
475 if (containsKey(commonMapping)) {
476 return get(commonMapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700477 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700478
479 // Otherwise search for package-specific mapping
480 final String specificMapping = generateKey(packageName, mimeType);
481 return get(specificMapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700482 }
483
484 /**
485 * Create a new {@link MappingCache} object and fill by walking across
486 * all packages to find those that provide mappings.
487 */
488 public static MappingCache createAndFill(Context context) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700489 Log.d(TAG, "building mime-type mapping cache...");
Jeff Sharkey3f177592009-05-18 15:23:12 -0700490 final PackageManager pm = context.getPackageManager();
491 MappingCache building = new MappingCache();
492 List<ApplicationInfo> installed = pm
493 .getInstalledApplications(PackageManager.GET_META_DATA);
494 for (ApplicationInfo info : installed) {
495 if (info.metaData != null && info.metaData.containsKey(MAPPING_METADATA)) {
496 try {
497 // Found metadata, so clone into their context to
498 // inflate reference
499 Context theirContext = context.createPackageContext(info.packageName, 0);
500 XmlPullParser mappingParser = info.loadXmlMetaData(pm, MAPPING_METADATA);
501 building.inflateMappings(theirContext, info.uid, info.packageName,
502 mappingParser);
503 } catch (NameNotFoundException e) {
504 Log.w(TAG, "Problem creating context for remote package", e);
505 } catch (InflateException e) {
506 Log.w(TAG, "Problem inflating MappingSet from remote package", e);
507 }
508 }
509 }
510 return building;
511 }
512
513 public static class InflateException extends Exception {
514 public InflateException(String message) {
515 super(message);
516 }
517
518 public InflateException(String message, Throwable throwable) {
519 super(message, throwable);
520 }
521 }
522
523 /**
524 * Inflate a <code>MappingSet</code> from an XML resource, assuming the
525 * given package name as the source.
526 */
527 public void inflateMappings(Context context, int uid, String packageName,
528 XmlPullParser parser) throws InflateException {
529 final AttributeSet attrs = Xml.asAttributeSet(parser);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700530 final Resources res = context.getResources();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700531
532 try {
533 int type;
534 while ((type = parser.next()) != XmlPullParser.START_TAG
535 && type != XmlPullParser.END_DOCUMENT) {
536 // Drain comments and whitespace
537 }
538
539 if (type != XmlPullParser.START_TAG) {
540 throw new InflateException("No start tag found");
541 }
542
543 if (!TAG_MAPPINGSET.equals(parser.getName())) {
544 throw new InflateException("Top level element must be MappingSet");
545 }
546
547 // Parse all children actions
548 final int depth = parser.getDepth();
549 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
550 && type != XmlPullParser.END_DOCUMENT) {
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700551 if (type == XmlPullParser.END_TAG) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700552 continue;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700553 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700554
555 if (!TAG_MAPPING.equals(parser.getName())) {
556 throw new InflateException("Expected Mapping tag");
557 }
558
559 // Parse kind, mime-type, and RemoteViews reference
560 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Mapping);
561
562 Mapping mapping = new Mapping();
563 mapping.packageName = packageName;
564 mapping.mimeType = a.getString(R.styleable.Mapping_mimeType);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700565 mapping.summaryColumn = a.getString(R.styleable.Mapping_summaryColumn);
Jeff Sharkey39261272009-06-03 19:15:09 -0700566 mapping.detailColumn = a.getString(R.styleable.Mapping_detailColumn);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700567 mapping.remoteViewsRes = a.getResourceId(R.styleable.Mapping_remoteViews, -1);
568
569 // Read and resize icon if provided
570 int iconRes = a.getResourceId(R.styleable.Mapping_icon, -1);
571 if (iconRes != -1) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700572 mapping.icon = BitmapFactory.decodeResource(res, iconRes);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700573 }
574
575 addMapping(mapping);
576 Log.d(TAG, "Added mapping for packageName=" + mapping.packageName
577 + ", mimetype=" + mapping.mimeType);
578 }
579 } catch (XmlPullParserException e) {
580 throw new InflateException("Problem reading XML", e);
581 } catch (IOException e) {
582 throw new InflateException("Problem reading XML", e);
583 }
584 }
585 }
586
587 /**
588 * Borrowed from Launcher for {@link Bitmap} resizing.
589 */
590 static final class Utilities {
591 private static final Paint sPaint = new Paint();
592 private static final Rect sBounds = new Rect();
593 private static final Rect sOldBounds = new Rect();
594 private static Canvas sCanvas = new Canvas();
595
596 static {
597 sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
598 Paint.FILTER_BITMAP_FLAG));
599 }
600
601 /**
602 * Returns a Bitmap representing the thumbnail of the specified Bitmap.
603 * The size of the thumbnail is defined by the dimension
604 * android.R.dimen.launcher_application_icon_size. This method is not
605 * thread-safe and should be invoked on the UI thread only.
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700606 *
Jeff Sharkey3f177592009-05-18 15:23:12 -0700607 * @param bitmap The bitmap to get a thumbnail of.
608 * @param context The application's context.
609 * @return A thumbnail for the specified bitmap or the bitmap itself if
610 * the thumbnail could not be created.
611 */
612 static Bitmap createBitmapThumbnail(Bitmap bitmap, Context context, int size) {
613 int width = size;
614 int height = size;
615
616 final int bitmapWidth = bitmap.getWidth();
617 final int bitmapHeight = bitmap.getHeight();
618
619 if (width > 0 && height > 0 && (width < bitmapWidth || height < bitmapHeight)) {
620 final float ratio = (float)bitmapWidth / bitmapHeight;
621
622 if (bitmapWidth > bitmapHeight) {
623 height = (int)(width / ratio);
624 } else if (bitmapHeight > bitmapWidth) {
625 width = (int)(height * ratio);
626 }
627
628 final Bitmap.Config c = (width == size && height == size) ? bitmap.getConfig()
629 : Bitmap.Config.ARGB_8888;
630 final Bitmap thumb = Bitmap.createBitmap(size, size, c);
631 final Canvas canvas = sCanvas;
632 final Paint paint = sPaint;
633 canvas.setBitmap(thumb);
634 paint.setDither(false);
635 paint.setFilterBitmap(true);
636 sBounds.set((size - width) / 2, (size - height) / 2, width, height);
637 sOldBounds.set(0, 0, bitmapWidth, bitmapHeight);
638 canvas.drawBitmap(bitmap, sOldBounds, sBounds, paint);
639 return thumb;
640 }
641
642 return bitmap;
643 }
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700644 }
645}