blob: 65c9932bb86c65daa237194493cab8f0e9de1869 [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;
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -070020
Jeff Sharkey3f177592009-05-18 15:23:12 -070021import org.xmlpull.v1.XmlPullParser;
22import org.xmlpull.v1.XmlPullParserException;
23
24import android.app.ListActivity;
25import android.content.ContentResolver;
26import android.content.ContentUris;
27import android.content.Context;
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -070028import android.content.Intent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070029import android.content.pm.ApplicationInfo;
30import android.content.pm.PackageManager;
31import android.content.pm.PackageManager.NameNotFoundException;
Jeff Sharkey549aa162009-05-21 01:33:30 -070032import android.content.res.Resources;
Jeff Sharkey3f177592009-05-18 15:23:12 -070033import android.content.res.TypedArray;
34import android.database.Cursor;
35import android.graphics.Bitmap;
36import android.graphics.BitmapFactory;
37import android.graphics.Canvas;
38import android.graphics.Paint;
39import android.graphics.PaintFlagsDrawFilter;
Jeff Sharkey3f177592009-05-18 15:23:12 -070040import android.graphics.Rect;
Jeff Sharkey3f177592009-05-18 15:23:12 -070041import android.net.Uri;
42import android.os.Bundle;
Evan Millar66388be2009-05-28 15:41:07 -070043import android.provider.ContactsContract;
44import android.provider.ContactsContract.Aggregates;
45import android.provider.ContactsContract.CommonDataKinds;
Evan Millar66388be2009-05-28 15:41:07 -070046import android.provider.ContactsContract.Data;
Dmitri Plotnikov39466592009-07-27 11:23:51 -070047import android.provider.ContactsContract.RawContacts;
Evan Millar66388be2009-05-28 15:41:07 -070048import android.provider.ContactsContract.CommonDataKinds.Photo;
49import android.provider.SocialContract.Activities;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -070050import android.text.SpannableStringBuilder;
Jeff Sharkey3f177592009-05-18 15:23:12 -070051import android.text.format.DateUtils;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -070052import android.text.style.StyleSpan;
Dmitri Plotnikov05f158f2009-05-21 11:37:15 -070053import android.text.style.UnderlineSpan;
Jeff Sharkey3f177592009-05-18 15:23:12 -070054import android.util.AttributeSet;
55import android.util.Log;
56import android.util.Xml;
Jeff Sharkey549aa162009-05-21 01:33:30 -070057import android.view.KeyEvent;
Jeff Sharkey3f177592009-05-18 15:23:12 -070058import android.view.LayoutInflater;
59import android.view.View;
60import android.view.ViewGroup;
Jeff Sharkey549aa162009-05-21 01:33:30 -070061import android.view.View.OnClickListener;
Jeff Sharkey3f177592009-05-18 15:23:12 -070062import android.widget.CursorAdapter;
63import android.widget.ImageView;
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -070064import android.widget.ListView;
Dmitri Plotnikov39466592009-07-27 11:23:51 -070065import android.widget.RemoteViews;
Jeff Sharkey3f177592009-05-18 15:23:12 -070066import android.widget.TextView;
67
68import java.io.IOException;
69import java.util.HashMap;
Jeff Sharkey3f177592009-05-18 15:23:12 -070070import java.util.List;
71
Jeff Sharkey549aa162009-05-21 01:33:30 -070072public class SocialStreamActivity extends ListActivity implements OnClickListener, EdgeTriggerListener {
Jeff Sharkey3f177592009-05-18 15:23:12 -070073 private static final String TAG = "SocialStreamActivity";
74
75 private static final String[] PROJ_ACTIVITIES = new String[] {
76 Activities._ID,
Jeff Sharkeyc6ad3ab2009-07-21 19:30:15 -070077 Activities.RES_PACKAGE,
Jeff Sharkey3f177592009-05-18 15:23:12 -070078 Activities.MIMETYPE,
79 Activities.AUTHOR_CONTACT_ID,
Dmitri Plotnikov39466592009-07-27 11:23:51 -070080 RawContacts.AGGREGATE_ID,
Jeff Sharkey3f177592009-05-18 15:23:12 -070081 Aggregates.DISPLAY_NAME,
82 Activities.PUBLISHED,
83 Activities.TITLE,
84 Activities.SUMMARY,
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -070085 Activities.THREAD_PUBLISHED,
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -070086 Activities.LINK,
87 Activities.THUMBNAIL,
Jeff Sharkey3f177592009-05-18 15:23:12 -070088 };
89
90 private static final int COL_ID = 0;
91 private static final int COL_PACKAGE = 1;
92 private static final int COL_MIMETYPE = 2;
93 private static final int COL_AUTHOR_CONTACT_ID = 3;
94 private static final int COL_AGGREGATE_ID = 4;
95 private static final int COL_DISPLAY_NAME = 5;
96 private static final int COL_PUBLISHED = 6;
97 private static final int COL_TITLE = 7;
98 private static final int COL_SUMMARY = 8;
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -070099 private static final int COL_THREAD_PUBLISHED = 9;
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700100 private static final int COL_LINK = 10;
101 private static final int COL_THUMBNAIL = 11;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700102
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700103 public static final int PHOTO_SIZE = 54;
104 public static final int THUMBNAIL_SIZE = 54;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700105
Jeff Sharkey549aa162009-05-21 01:33:30 -0700106 private SocialAdapter mAdapter;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700107
Jeff Sharkey549aa162009-05-21 01:33:30 -0700108 private ListView mListView;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700109 private EdgeTriggerView mEdgeTrigger;
110 private FastTrackWindow mFastTrack;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700111 private MappingCache mMappingCache;
112
113 private static final boolean USE_GESTURE = false;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700114
115 private ContactsCache mContactsCache;
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700116
117 @Override
118 protected void onCreate(Bundle icicle) {
119 super.onCreate(icicle);
120
Jeff Sharkey3f177592009-05-18 15:23:12 -0700121 setContentView(R.layout.social_list);
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700122
Jeff Sharkey3f177592009-05-18 15:23:12 -0700123 mContactsCache = new ContactsCache(this);
124 mMappingCache = MappingCache.createAndFill(this);
125
126 Cursor cursor = managedQuery(Activities.CONTENT_URI, PROJ_ACTIVITIES, null, null);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700127 mAdapter = new SocialAdapter(this, cursor, mContactsCache, mMappingCache);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700128 mAdapter.setPhotoListener(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700129
130 setListAdapter(mAdapter);
131
Jeff Sharkey549aa162009-05-21 01:33:30 -0700132 mListView = getListView();
133 mFastTrack = new FastTrackWindow(this);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700134
Jeff Sharkey549aa162009-05-21 01:33:30 -0700135 if (USE_GESTURE) {
136 // Find and listen for edge triggers
137 mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
138 mEdgeTrigger.setOnEdgeTriggerListener(this);
139 }
140 }
141
142 /** {@inheritDoc} */
143 public void onClick(View v) {
144 // Clicked on photo, so show fast-track
Jeff Sharkey39261272009-06-03 19:15:09 -0700145 showFastTrack(v, (Long)v.getTag());
Jeff Sharkey3f177592009-05-18 15:23:12 -0700146 }
147
148 /** {@inheritDoc} */
149 public void onTrigger(float downX, float downY, int edge) {
150 // Find list item user triggered over
Jeff Sharkey549aa162009-05-21 01:33:30 -0700151 final int position = mListView.pointToPosition((int)downX, (int)downY);
152 if (position == ListView.INVALID_POSITION) return;
153
154 // Reverse to find the exact top of the triggered entry
155 final int index = position - mListView.getFirstVisiblePosition();
156 final View anchor = mListView.getChildAt(index);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700157
158 Cursor cursor = (Cursor)mAdapter.getItem(position);
159 long aggId = cursor.getLong(COL_AGGREGATE_ID);
160
Jeff Sharkey549aa162009-05-21 01:33:30 -0700161 showFastTrack(anchor, aggId);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700162
Jeff Sharkey549aa162009-05-21 01:33:30 -0700163 }
164
165 private int[] mLocation = new int[2];
Jeff Sharkey39261272009-06-03 19:15:09 -0700166 private Rect mRect = new Rect();
Jeff Sharkey549aa162009-05-21 01:33:30 -0700167
168 private void showFastTrack(View anchor, long aggId) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700169 Uri aggUri = ContentUris.withAppendedId(ContactsContract.Aggregates.CONTENT_URI, aggId);
170
Jeff Sharkey549aa162009-05-21 01:33:30 -0700171 anchor.getLocationInWindow(mLocation);
Jeff Sharkey39261272009-06-03 19:15:09 -0700172 mRect.left = mLocation[0];
173 mRect.top = mLocation[1];
174 mRect.right = mRect.left + anchor.getWidth();
175 mRect.bottom = mRect.top + anchor.getHeight();
Jeff Sharkey549aa162009-05-21 01:33:30 -0700176
177 mFastTrack.dismiss();
Jeff Sharkey39261272009-06-03 19:15:09 -0700178 mFastTrack.show(aggUri, mRect);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700179 }
180
181 /** {@inheritDoc} */
182 @Override
183 public boolean onKeyDown(int keyCode, KeyEvent event) {
184 // Back key dismisses fast-track when its visible
185 if (keyCode == KeyEvent.KEYCODE_BACK && mFastTrack.isShowing()) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700186 mFastTrack.dismiss();
Jeff Sharkey549aa162009-05-21 01:33:30 -0700187 return true;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700188 }
189
Jeff Sharkey549aa162009-05-21 01:33:30 -0700190 return super.onKeyDown(keyCode, event);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700191 }
192
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700193 @Override
194 protected void onListItemClick(ListView l, View v, int position, long id) {
195 Cursor cursor = (Cursor)getListAdapter().getItem(position);
196
197 // TODO check mime type and if it is supported, launch the corresponding app
198 String link = cursor.getString(COL_LINK);
199 if (link == null) {
200 return;
201 }
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700202 startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(link)));
203 }
204
Jeff Sharkey3f177592009-05-18 15:23:12 -0700205 /**
206 * List adapter for social stream data queried from
207 * {@link Activities#CONTENT_URI}.
208 */
209 private static class SocialAdapter extends CursorAdapter {
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700210 private final Context mContext;
211 private final LayoutInflater mInflater;
212 private final ContactsCache mContactsCache;
213 private final MappingCache mMappingCache;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700214 private final StyleSpan mTextStyleName;
Dmitri Plotnikov05f158f2009-05-21 11:37:15 -0700215 private final UnderlineSpan mTextStyleLink;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700216 private OnClickListener mPhotoListener;
217 private SpannableStringBuilder mBuilder = new SpannableStringBuilder();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700218
Jeff Sharkey39261272009-06-03 19:15:09 -0700219 public static final int TYPE_ACTIVITY = 0;
220 public static final int TYPE_REPLY = 1;
221
Jeff Sharkey3f177592009-05-18 15:23:12 -0700222 private static class SocialHolder {
223 ImageView photo;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700224 ImageView sourceIcon;
225 TextView content;
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700226 TextView summary;
227 ImageView thumbnail;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700228 TextView published;
229 }
230
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700231 public SocialAdapter(Context context, Cursor c, ContactsCache contactsCache,
232 MappingCache mappingCache) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700233 super(context, c, true);
234 mContext = context;
235 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
236 mContactsCache = contactsCache;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700237 mMappingCache = mappingCache;
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700238 mTextStyleName = new StyleSpan(android.graphics.Typeface.BOLD);
Dmitri Plotnikov05f158f2009-05-21 11:37:15 -0700239 mTextStyleLink = new UnderlineSpan();
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700240 }
241
Jeff Sharkey549aa162009-05-21 01:33:30 -0700242 public void setPhotoListener(OnClickListener listener) {
243 mPhotoListener = listener;
244 }
245
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700246 @Override
247 public int getViewTypeCount() {
248 return 2;
249 }
250
251 @Override
252 public int getItemViewType(int position) {
253 Cursor cursor = (Cursor) getItem(position);
Jeff Sharkey39261272009-06-03 19:15:09 -0700254 return isReply(cursor) ? TYPE_ACTIVITY : TYPE_REPLY;
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700255 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700256
257 @Override
258 public void bindView(View view, Context context, Cursor cursor) {
259 SocialHolder holder = (SocialHolder)view.getTag();
260
Jeff Sharkey549aa162009-05-21 01:33:30 -0700261 long aggId = cursor.getLong(COL_AGGREGATE_ID);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700262 long contactId = cursor.getLong(COL_AUTHOR_CONTACT_ID);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700263 String name = cursor.getString(COL_DISPLAY_NAME);
264 String title = cursor.getString(COL_TITLE);
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700265 String summary = cursor.getString(COL_SUMMARY);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700266 long published = cursor.getLong(COL_PUBLISHED);
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700267 byte[] thumbnailBlob = cursor.getBlob(COL_THUMBNAIL);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700268
269 // TODO: trigger async query to find actual name and photo instead
270 // of using this lazy caching mechanism
Dmitri Plotnikov9a41d432009-05-19 18:33:46 -0700271 Bitmap photo = mContactsCache.getPhoto(contactId);
272 if (photo != null) {
273 holder.photo.setImageBitmap(photo);
274 } else {
275 holder.photo.setImageResource(R.drawable.ic_contact_list_picture);
276 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700277 holder.photo.setTag(aggId);
278
279 mBuilder.clear();
280 mBuilder.append(name);
281 mBuilder.append(" ");
282 mBuilder.append(title);
283 mBuilder.setSpan(mTextStyleName, 0, name.length(), 0);
284 holder.content.setText(mBuilder);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700285
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700286 if (summary == null) {
287 holder.summary.setVisibility(View.GONE);
288 } else {
Jeff Sharkey39261272009-06-03 19:15:09 -0700289 mBuilder.clear();
290 mBuilder.append(summary);
291 mBuilder.setSpan(mTextStyleLink, 0, summary.length(), 0);
292 holder.summary.setText(mBuilder);
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700293 holder.summary.setVisibility(View.VISIBLE);
294 }
295
296 if (thumbnailBlob != null) {
297 Bitmap thumbnail =
298 BitmapFactory.decodeByteArray(thumbnailBlob, 0, thumbnailBlob.length);
299 holder.thumbnail.setImageBitmap(thumbnail);
300 holder.thumbnail.setVisibility(View.VISIBLE);
301 } else {
302 holder.thumbnail.setVisibility(View.GONE);
303 }
304
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700305 CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published,
306 System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700307 holder.published.setText(relativePublished);
308
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700309 if (holder.sourceIcon != null) {
310 String packageName = cursor.getString(COL_PACKAGE);
311 String mimeType = cursor.getString(COL_MIMETYPE);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700312 Mapping mapping = mMappingCache.findMapping(packageName, mimeType);
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700313 if (mapping != null && mapping.icon != null) {
314 holder.sourceIcon.setImageBitmap(mapping.icon);
315 } else {
316 holder.sourceIcon.setImageDrawable(null);
317 }
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700318 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700319 }
320
321 @Override
322 public View newView(Context context, Cursor cursor, ViewGroup parent) {
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700323 View view = mInflater.inflate(
324 isReply(cursor) ? R.layout.social_list_item_reply : R.layout.social_list_item,
325 parent, false);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700326
327 SocialHolder holder = new SocialHolder();
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700328 holder.photo = (ImageView) view.findViewById(R.id.photo);
329 holder.sourceIcon = (ImageView) view.findViewById(R.id.sourceIcon);
330 holder.content = (TextView) view.findViewById(R.id.content);
Dmitri Plotnikov672cbe62009-05-20 19:07:59 -0700331 holder.summary = (TextView) view.findViewById(R.id.summary);
332 holder.thumbnail = (ImageView) view.findViewById(R.id.thumbnail);
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700333 holder.published = (TextView) view.findViewById(R.id.published);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700334 view.setTag(holder);
335
Jeff Sharkey549aa162009-05-21 01:33:30 -0700336 if (!USE_GESTURE) {
337 holder.photo.setOnClickListener(mPhotoListener);
338 }
339
Jeff Sharkey3f177592009-05-18 15:23:12 -0700340 return view;
341 }
Dmitri Plotnikovf0eb9f52009-05-20 14:44:56 -0700342
343 private boolean isReply(Cursor cursor) {
344
345 /*
346 * Comparing the message timestamp to the thread timestamp rather than checking the
347 * in_reply_to field. The rationale for this approach is that in the case when the
348 * original message to which the reply was posted is missing, we want to display
349 * the message as if it was an original; otherwise it would appear to be a reply
350 * to whatever message preceded it in the list. In the case when the original message
351 * of the thread is missing, the two timestamps will be the same.
352 */
353 long published = cursor.getLong(COL_PUBLISHED);
354 long threadPublished = cursor.getLong(COL_THREAD_PUBLISHED);
355 return published != threadPublished;
356 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700357 }
358
359 /**
Dmitri Plotnikov39466592009-07-27 11:23:51 -0700360 * Keep a cache that maps from {@link RawContacts#_ID} to {@link Photo#PHOTO}
Jeff Sharkey3f177592009-05-18 15:23:12 -0700361 * values.
362 */
363 private static class ContactsCache {
364 private static final String TAG = "ContactsCache";
365
366 private static final String[] PROJ_DETAILS = new String[] {
367 Data.MIMETYPE,
368 Data.CONTACT_ID,
369 Photo.PHOTO,
370 };
371
372 private static final int COL_MIMETYPE = 0;
373 private static final int COL_CONTACT_ID = 1;
Jeff Sharkey8da253a2009-05-18 21:23:19 -0700374 private static final int COL_PHOTO = 2;
Jeff Sharkey3f177592009-05-18 15:23:12 -0700375
376 private HashMap<Long, Bitmap> mPhoto = new HashMap<Long, Bitmap>();
377
378 public ContactsCache(Context context) {
379 Log.d(TAG, "building ContactsCache...");
380
381 ContentResolver resolver = context.getContentResolver();
382 Cursor cursor = resolver.query(Data.CONTENT_URI, PROJ_DETAILS,
383 Data.MIMETYPE + "=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
384
385 while (cursor.moveToNext()) {
386 long contactId = cursor.getLong(COL_CONTACT_ID);
387 String mimeType = cursor.getString(COL_MIMETYPE);
388 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
389 byte[] photoBlob = cursor.getBlob(COL_PHOTO);
390 Bitmap photo = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
391 photo = Utilities.createBitmapThumbnail(photo, context, PHOTO_SIZE);
392
393 mPhoto.put(contactId, photo);
394 }
395 }
396
397 cursor.close();
398 Log.d(TAG, "done building ContactsCache");
399 }
400
401 public Bitmap getPhoto(long contactId) {
402 return mPhoto.get(contactId);
403 }
404 }
405
406 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700407 * Store a mapping from a package name and mime-type pair to a set of
408 * {@link RemoteViews}, default icon, and column to use from the
409 * {@link Data} table to use as a summary.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700410 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700411 public static class Mapping {
412 String packageName;
413 String mimeType;
414 String summaryColumn;
Jeff Sharkey39261272009-06-03 19:15:09 -0700415 String detailColumn;
Jeff Sharkey549aa162009-05-21 01:33:30 -0700416 int remoteViewsRes;
417 Bitmap icon;
418
419 public Mapping() {
420 }
421
422 public Mapping(String packageName, String mimeType) {
423 this.packageName = packageName;
424 this.mimeType = mimeType;
425 }
426 }
427
428 /**
429 * Store a parsed <code>Mapping</code> object, which maps package and
430 * mime-type combinations to {@link RemoteViews} XML resources, default
431 * icons, and summary columns in the {@link Data} table.
432 */
433 public static class MappingCache extends HashMap<String, Mapping> {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700434 private static final String TAG = "MappingCache";
435
436 private static final String TAG_MAPPINGSET = "MappingSet";
437 private static final String TAG_MAPPING = "Mapping";
438
439 private static final String MAPPING_METADATA = "com.android.contacts.stylemap";
440
Jeff Sharkey3f177592009-05-18 15:23:12 -0700441
Jeff Sharkey549aa162009-05-21 01:33:30 -0700442 /**
443 * Only allow inflating through
444 * {@link MappingCache#createAndFill(Context)}.
445 */
Jeff Sharkey3f177592009-05-18 15:23:12 -0700446 private MappingCache() {
447 }
448
Jeff Sharkey549aa162009-05-21 01:33:30 -0700449 /**
450 * Add a {@link Mapping} instance to this cache, correctly using
451 * {@link #generateKey(String, String)} when storing.
452 */
Jeff Sharkey3f177592009-05-18 15:23:12 -0700453 public void addMapping(Mapping mapping) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700454 String hashKey = generateKey(mapping.packageName, mapping.mimeType);
455 put(hashKey, mapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700456 }
457
458 /**
Jeff Sharkey549aa162009-05-21 01:33:30 -0700459 * Generate a key used internally for mapping a specific package name
460 * and mime-type to a {@link Mapping}.
Jeff Sharkey3f177592009-05-18 15:23:12 -0700461 */
Jeff Sharkey549aa162009-05-21 01:33:30 -0700462 private String generateKey(String packageName, String mimeType) {
463 return packageName + ";" + mimeType;
464 }
465
466 /**
467 * Find matching mapping for requested package and mime-type. Returns
468 * null if no mapping found.
469 */
470 public Mapping findMapping(String packageName, String mimeType) {
471 // Search for common mapping first
472 final String commonMapping = generateKey(CommonDataKinds.PACKAGE_COMMON, mimeType);
473 if (containsKey(commonMapping)) {
474 return get(commonMapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700475 }
Jeff Sharkey549aa162009-05-21 01:33:30 -0700476
477 // Otherwise search for package-specific mapping
478 final String specificMapping = generateKey(packageName, mimeType);
479 return get(specificMapping);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700480 }
481
482 /**
483 * Create a new {@link MappingCache} object and fill by walking across
484 * all packages to find those that provide mappings.
485 */
486 public static MappingCache createAndFill(Context context) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700487 Log.d(TAG, "building mime-type mapping cache...");
Jeff Sharkey3f177592009-05-18 15:23:12 -0700488 final PackageManager pm = context.getPackageManager();
489 MappingCache building = new MappingCache();
490 List<ApplicationInfo> installed = pm
491 .getInstalledApplications(PackageManager.GET_META_DATA);
492 for (ApplicationInfo info : installed) {
493 if (info.metaData != null && info.metaData.containsKey(MAPPING_METADATA)) {
494 try {
495 // Found metadata, so clone into their context to
496 // inflate reference
497 Context theirContext = context.createPackageContext(info.packageName, 0);
498 XmlPullParser mappingParser = info.loadXmlMetaData(pm, MAPPING_METADATA);
499 building.inflateMappings(theirContext, info.uid, info.packageName,
500 mappingParser);
501 } catch (NameNotFoundException e) {
502 Log.w(TAG, "Problem creating context for remote package", e);
503 } catch (InflateException e) {
504 Log.w(TAG, "Problem inflating MappingSet from remote package", e);
505 }
506 }
507 }
508 return building;
509 }
510
511 public static class InflateException extends Exception {
512 public InflateException(String message) {
513 super(message);
514 }
515
516 public InflateException(String message, Throwable throwable) {
517 super(message, throwable);
518 }
519 }
520
521 /**
522 * Inflate a <code>MappingSet</code> from an XML resource, assuming the
523 * given package name as the source.
524 */
525 public void inflateMappings(Context context, int uid, String packageName,
526 XmlPullParser parser) throws InflateException {
527 final AttributeSet attrs = Xml.asAttributeSet(parser);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700528 final Resources res = context.getResources();
Jeff Sharkey3f177592009-05-18 15:23:12 -0700529
530 try {
531 int type;
532 while ((type = parser.next()) != XmlPullParser.START_TAG
533 && type != XmlPullParser.END_DOCUMENT) {
534 // Drain comments and whitespace
535 }
536
537 if (type != XmlPullParser.START_TAG) {
538 throw new InflateException("No start tag found");
539 }
540
541 if (!TAG_MAPPINGSET.equals(parser.getName())) {
542 throw new InflateException("Top level element must be MappingSet");
543 }
544
545 // Parse all children actions
546 final int depth = parser.getDepth();
547 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
548 && type != XmlPullParser.END_DOCUMENT) {
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700549 if (type == XmlPullParser.END_TAG) {
Jeff Sharkey3f177592009-05-18 15:23:12 -0700550 continue;
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700551 }
Jeff Sharkey3f177592009-05-18 15:23:12 -0700552
553 if (!TAG_MAPPING.equals(parser.getName())) {
554 throw new InflateException("Expected Mapping tag");
555 }
556
557 // Parse kind, mime-type, and RemoteViews reference
558 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Mapping);
559
560 Mapping mapping = new Mapping();
561 mapping.packageName = packageName;
562 mapping.mimeType = a.getString(R.styleable.Mapping_mimeType);
Jeff Sharkey549aa162009-05-21 01:33:30 -0700563 mapping.summaryColumn = a.getString(R.styleable.Mapping_summaryColumn);
Jeff Sharkey39261272009-06-03 19:15:09 -0700564 mapping.detailColumn = a.getString(R.styleable.Mapping_detailColumn);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700565 mapping.remoteViewsRes = a.getResourceId(R.styleable.Mapping_remoteViews, -1);
566
567 // Read and resize icon if provided
568 int iconRes = a.getResourceId(R.styleable.Mapping_icon, -1);
569 if (iconRes != -1) {
Jeff Sharkey549aa162009-05-21 01:33:30 -0700570 mapping.icon = BitmapFactory.decodeResource(res, iconRes);
Jeff Sharkey3f177592009-05-18 15:23:12 -0700571 }
572
573 addMapping(mapping);
574 Log.d(TAG, "Added mapping for packageName=" + mapping.packageName
575 + ", mimetype=" + mapping.mimeType);
576 }
577 } catch (XmlPullParserException e) {
578 throw new InflateException("Problem reading XML", e);
579 } catch (IOException e) {
580 throw new InflateException("Problem reading XML", e);
581 }
582 }
583 }
584
585 /**
586 * Borrowed from Launcher for {@link Bitmap} resizing.
587 */
588 static final class Utilities {
589 private static final Paint sPaint = new Paint();
590 private static final Rect sBounds = new Rect();
591 private static final Rect sOldBounds = new Rect();
592 private static Canvas sCanvas = new Canvas();
593
594 static {
595 sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
596 Paint.FILTER_BITMAP_FLAG));
597 }
598
599 /**
600 * Returns a Bitmap representing the thumbnail of the specified Bitmap.
601 * The size of the thumbnail is defined by the dimension
602 * android.R.dimen.launcher_application_icon_size. This method is not
603 * thread-safe and should be invoked on the UI thread only.
Dmitri Plotnikov3c690ce2009-05-19 14:43:45 -0700604 *
Jeff Sharkey3f177592009-05-18 15:23:12 -0700605 * @param bitmap The bitmap to get a thumbnail of.
606 * @param context The application's context.
607 * @return A thumbnail for the specified bitmap or the bitmap itself if
608 * the thumbnail could not be created.
609 */
610 static Bitmap createBitmapThumbnail(Bitmap bitmap, Context context, int size) {
611 int width = size;
612 int height = size;
613
614 final int bitmapWidth = bitmap.getWidth();
615 final int bitmapHeight = bitmap.getHeight();
616
617 if (width > 0 && height > 0 && (width < bitmapWidth || height < bitmapHeight)) {
618 final float ratio = (float)bitmapWidth / bitmapHeight;
619
620 if (bitmapWidth > bitmapHeight) {
621 height = (int)(width / ratio);
622 } else if (bitmapHeight > bitmapWidth) {
623 width = (int)(height * ratio);
624 }
625
626 final Bitmap.Config c = (width == size && height == size) ? bitmap.getConfig()
627 : Bitmap.Config.ARGB_8888;
628 final Bitmap thumb = Bitmap.createBitmap(size, size, c);
629 final Canvas canvas = sCanvas;
630 final Paint paint = sPaint;
631 canvas.setBitmap(thumb);
632 paint.setDither(false);
633 paint.setFilterBitmap(true);
634 sBounds.set((size - width) / 2, (size - height) / 2, width, height);
635 sOldBounds.set(0, 0, bitmapWidth, bitmapHeight);
636 canvas.drawBitmap(bitmap, sOldBounds, sBounds, paint);
637 return thumb;
638 }
639
640 return bitmap;
641 }
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700642 }
643}