blob: e04128f717c79335af5197603cf62c895bee147f [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;
20import com.android.providers.contacts2.ContactsContract;
21import com.android.providers.contacts2.ContactsContract.Aggregates;
22import com.android.providers.contacts2.ContactsContract.Contacts;
23import com.android.providers.contacts2.ContactsContract.Data;
24import com.android.providers.contacts2.ContactsContract.CommonDataKinds.Photo;
25import com.android.providers.contacts2.ContactsContract.CommonDataKinds.StructuredName;
26import com.android.providers.contacts2.SocialContract.Activities;
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -070027
Jeff Sharkey3f177592009-05-18 15:23:12 -070028import org.xmlpull.v1.XmlPullParser;
29import org.xmlpull.v1.XmlPullParserException;
30
31import android.app.ListActivity;
32import android.content.ContentResolver;
33import android.content.ContentUris;
34import android.content.Context;
35import android.content.pm.ApplicationInfo;
36import android.content.pm.PackageManager;
37import android.content.pm.PackageManager.NameNotFoundException;
38import android.content.res.Resources;
39import android.content.res.TypedArray;
40import android.database.Cursor;
41import android.graphics.Bitmap;
42import android.graphics.BitmapFactory;
43import android.graphics.Canvas;
44import android.graphics.Paint;
45import android.graphics.PaintFlagsDrawFilter;
46import android.graphics.PixelFormat;
47import android.graphics.Rect;
48import android.graphics.drawable.BitmapDrawable;
49import android.graphics.drawable.Drawable;
50import android.graphics.drawable.PaintDrawable;
51import android.net.Uri;
52import android.os.Bundle;
53import android.text.format.DateUtils;
54import android.util.AttributeSet;
55import android.util.Log;
56import android.util.Xml;
57import android.view.LayoutInflater;
58import android.view.View;
59import android.view.ViewGroup;
60import android.widget.BaseAdapter;
61import android.widget.CursorAdapter;
62import android.widget.ImageView;
63import android.widget.ListAdapter;
64import android.widget.ListView;
65import android.widget.SimpleCursorAdapter;
66import android.widget.TextView;
67
68import java.io.IOException;
69import java.util.HashMap;
70import java.util.LinkedList;
71import java.util.List;
72
73public class SocialStreamActivity extends ListActivity implements EdgeTriggerListener {
74 private static final String TAG = "SocialStreamActivity";
75
76 private static final String[] PROJ_ACTIVITIES = new String[] {
77 Activities._ID,
78 Activities.PACKAGE,
79 Activities.MIMETYPE,
80 Activities.AUTHOR_CONTACT_ID,
81 Contacts.AGGREGATE_ID,
82 Aggregates.DISPLAY_NAME,
83 Activities.PUBLISHED,
84 Activities.TITLE,
85 Activities.SUMMARY,
86 };
87
88 private static final int COL_ID = 0;
89 private static final int COL_PACKAGE = 1;
90 private static final int COL_MIMETYPE = 2;
91 private static final int COL_AUTHOR_CONTACT_ID = 3;
92 private static final int COL_AGGREGATE_ID = 4;
93 private static final int COL_DISPLAY_NAME = 5;
94 private static final int COL_PUBLISHED = 6;
95 private static final int COL_TITLE = 7;
96 private static final int COL_SUMMARY = 8;
97
98 public static final int PHOTO_SIZE = 58;
99
100 private ListAdapter mAdapter;
101
102 private FloatyListView mListView;
103 private EdgeTriggerView mEdgeTrigger;
104 private FastTrackWindow mFastTrack;
105
106 private ContactsCache mContactsCache;
107 private MappingCache mMappingCache;
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700108
109 @Override
110 protected void onCreate(Bundle icicle) {
111 super.onCreate(icicle);
112
Jeff Sharkey3f177592009-05-18 15:23:12 -0700113 setContentView(R.layout.social_list);
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700114
Jeff Sharkey3f177592009-05-18 15:23:12 -0700115 mContactsCache = new ContactsCache(this);
116 mMappingCache = MappingCache.createAndFill(this);
117
118 Cursor cursor = managedQuery(Activities.CONTENT_URI, PROJ_ACTIVITIES, null, null);
119 mAdapter = new SocialAdapter(this, cursor, mContactsCache);
120
121// mAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, cursor,
122// new String[] { Activities.AUTHOR_CONTACT_ID, Activities.TITLE },
123// new int[] { android.R.id.text1, android.R.id.text2 });
124
125 setListAdapter(mAdapter);
126
127 mListView = (FloatyListView)findViewById(android.R.id.list);
128
129 // Find and listen for edge triggers
130 mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
131 mEdgeTrigger.setOnEdgeTriggerListener(this);
132 }
133
134 /** {@inheritDoc} */
135 public void onTrigger(float downX, float downY, int edge) {
136 // Find list item user triggered over
137 int position = mListView.pointToPosition((int)downX, (int)downY);
138
139 Cursor cursor = (Cursor)mAdapter.getItem(position);
140 long aggId = cursor.getLong(COL_AGGREGATE_ID);
141
142 Log.d(TAG, "onTrigger found position=" + position + ", contactId=" + aggId);
143
144 Uri aggUri = ContentUris.withAppendedId(ContactsContract.Aggregates.CONTENT_URI, aggId);
145
146 // Dismiss any existing window first
147 if (mFastTrack != null) {
148 mFastTrack.dismiss();
149 }
150
151 mFastTrack = new FastTrackWindow(this, mListView, aggUri, mMappingCache);
152 mListView.setFloatyWindow(mFastTrack, position);
153
154 }
155
156 /**
157 * List adapter for social stream data queried from
158 * {@link Activities#CONTENT_URI}.
159 */
160 private static class SocialAdapter extends CursorAdapter {
161 private Context mContext;
162 private LayoutInflater mInflater;
163 private ContactsCache mContactsCache;
164
165 private static class SocialHolder {
166 ImageView photo;
167 TextView displayname;
168 TextView title;
169 TextView published;
170 }
171
172 public SocialAdapter(Context context, Cursor c, ContactsCache contactsCache) {
173 super(context, c, true);
174 mContext = context;
175 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
176 mContactsCache = contactsCache;
177 }
178
179 @Override
180 public void bindView(View view, Context context, Cursor cursor) {
181 SocialHolder holder = (SocialHolder)view.getTag();
182
183 long contactId = cursor.getLong(COL_AUTHOR_CONTACT_ID);
184
185 // TODO: trigger async query to find actual name and photo instead
186 // of using this lazy caching mechanism
187 holder.photo.setImageBitmap(mContactsCache.getPhoto(contactId));
188
189 holder.displayname.setText(cursor.getString(COL_DISPLAY_NAME));
190 holder.title.setText(cursor.getString(COL_TITLE));
191
192 long published = cursor.getLong(COL_PUBLISHED);
193 CharSequence relativePublished = DateUtils.getRelativeTimeSpanString(published, System
194 .currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
195 holder.published.setText(relativePublished);
196
197 }
198
199 @Override
200 public View newView(Context context, Cursor cursor, ViewGroup parent) {
201 View view = mInflater.inflate(R.layout.social_list_item, parent, false);
202
203 SocialHolder holder = new SocialHolder();
204 holder.photo = (ImageView)view.findViewById(R.id.photo);
205 holder.displayname = (TextView)view.findViewById(R.id.displayname);
206 holder.title = (TextView)view.findViewById(R.id.title);
207 holder.published = (TextView)view.findViewById(R.id.published);
208 view.setTag(holder);
209
210 return view;
211 }
212 }
213
214 /**
215 * Keep a cache that maps from {@link Contacts#_ID} to {@link Photo#PHOTO}
216 * values.
217 */
218 private static class ContactsCache {
219 private static final String TAG = "ContactsCache";
220
221 private static final String[] PROJ_DETAILS = new String[] {
222 Data.MIMETYPE,
223 Data.CONTACT_ID,
224 Photo.PHOTO,
225 };
226
227 private static final int COL_MIMETYPE = 0;
228 private static final int COL_CONTACT_ID = 1;
229 private static final int COL_PHOTO = 3;
230
231 private HashMap<Long, Bitmap> mPhoto = new HashMap<Long, Bitmap>();
232
233 public ContactsCache(Context context) {
234 Log.d(TAG, "building ContactsCache...");
235
236 ContentResolver resolver = context.getContentResolver();
237 Cursor cursor = resolver.query(Data.CONTENT_URI, PROJ_DETAILS,
238 Data.MIMETYPE + "=?", new String[] { Photo.CONTENT_ITEM_TYPE }, null);
239
240 while (cursor.moveToNext()) {
241 long contactId = cursor.getLong(COL_CONTACT_ID);
242 String mimeType = cursor.getString(COL_MIMETYPE);
243 if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
244 byte[] photoBlob = cursor.getBlob(COL_PHOTO);
245 Bitmap photo = BitmapFactory.decodeByteArray(photoBlob, 0, photoBlob.length);
246 photo = Utilities.createBitmapThumbnail(photo, context, PHOTO_SIZE);
247
248 mPhoto.put(contactId, photo);
249 }
250 }
251
252 cursor.close();
253 Log.d(TAG, "done building ContactsCache");
254 }
255
256 public Bitmap getPhoto(long contactId) {
257 return mPhoto.get(contactId);
258 }
259 }
260
261 /**
262 * Store a parsed <code>RemoteViewsMapping</code> object, which maps
263 * mime-types to <code>RemoteViews</code> XML resources and possible icons.
264 */
265 public static class MappingCache {
266 private static final String TAG = "MappingCache";
267
268 private static final String TAG_MAPPINGSET = "MappingSet";
269 private static final String TAG_MAPPING = "Mapping";
270
271 private static final String MAPPING_METADATA = "com.android.contacts.stylemap";
272
273 private LinkedList<Mapping> mappings = new LinkedList<Mapping>();
274
275 private MappingCache() {
276 }
277
278 public static class Mapping {
279 String packageName;
280 String mimeType;
281 int remoteViewsRes;
282 Bitmap icon;
283 }
284
285 public void addMapping(Mapping mapping) {
286 mappings.add(mapping);
287 }
288
289 /**
290 * Find matching <code>RemoteViews</code> XML resource for requested
291 * package and mime-type. Returns -1 if no mapping found.
292 */
293 public Mapping getMapping(String packageName, String mimeType) {
294 for (Mapping mapping : mappings) {
295 if (mapping.packageName.equals(packageName) && mapping.mimeType.equals(mimeType)) {
296 return mapping;
297 }
298 }
299 return null;
300 }
301
302 /**
303 * Create a new {@link MappingCache} object and fill by walking across
304 * all packages to find those that provide mappings.
305 */
306 public static MappingCache createAndFill(Context context) {
307 Log.d(TAG, "searching for mimetype mappings...");
308 final PackageManager pm = context.getPackageManager();
309 MappingCache building = new MappingCache();
310 List<ApplicationInfo> installed = pm
311 .getInstalledApplications(PackageManager.GET_META_DATA);
312 for (ApplicationInfo info : installed) {
313 if (info.metaData != null && info.metaData.containsKey(MAPPING_METADATA)) {
314 try {
315 // Found metadata, so clone into their context to
316 // inflate reference
317 Context theirContext = context.createPackageContext(info.packageName, 0);
318 XmlPullParser mappingParser = info.loadXmlMetaData(pm, MAPPING_METADATA);
319 building.inflateMappings(theirContext, info.uid, info.packageName,
320 mappingParser);
321 } catch (NameNotFoundException e) {
322 Log.w(TAG, "Problem creating context for remote package", e);
323 } catch (InflateException e) {
324 Log.w(TAG, "Problem inflating MappingSet from remote package", e);
325 }
326 }
327 }
328 return building;
329 }
330
331 public static class InflateException extends Exception {
332 public InflateException(String message) {
333 super(message);
334 }
335
336 public InflateException(String message, Throwable throwable) {
337 super(message, throwable);
338 }
339 }
340
341 /**
342 * Inflate a <code>MappingSet</code> from an XML resource, assuming the
343 * given package name as the source.
344 */
345 public void inflateMappings(Context context, int uid, String packageName,
346 XmlPullParser parser) throws InflateException {
347 final AttributeSet attrs = Xml.asAttributeSet(parser);
348
349 try {
350 int type;
351 while ((type = parser.next()) != XmlPullParser.START_TAG
352 && type != XmlPullParser.END_DOCUMENT) {
353 // Drain comments and whitespace
354 }
355
356 if (type != XmlPullParser.START_TAG) {
357 throw new InflateException("No start tag found");
358 }
359
360 if (!TAG_MAPPINGSET.equals(parser.getName())) {
361 throw new InflateException("Top level element must be MappingSet");
362 }
363
364 // Parse all children actions
365 final int depth = parser.getDepth();
366 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
367 && type != XmlPullParser.END_DOCUMENT) {
368 if (type == XmlPullParser.END_TAG)
369 continue;
370
371 if (!TAG_MAPPING.equals(parser.getName())) {
372 throw new InflateException("Expected Mapping tag");
373 }
374
375 // Parse kind, mime-type, and RemoteViews reference
376 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Mapping);
377
378 Mapping mapping = new Mapping();
379 mapping.packageName = packageName;
380 mapping.mimeType = a.getString(R.styleable.Mapping_mimeType);
381 mapping.remoteViewsRes = a.getResourceId(R.styleable.Mapping_remoteViews, -1);
382
383 // Read and resize icon if provided
384 int iconRes = a.getResourceId(R.styleable.Mapping_icon, -1);
385 if (iconRes != -1) {
386 mapping.icon = BitmapFactory
387 .decodeResource(context.getResources(), iconRes);
388 mapping.icon = Utilities.createBitmapThumbnail(mapping.icon, context,
389 FastTrackWindow.ICON_SIZE);
390 }
391
392 addMapping(mapping);
393 Log.d(TAG, "Added mapping for packageName=" + mapping.packageName
394 + ", mimetype=" + mapping.mimeType);
395 }
396 } catch (XmlPullParserException e) {
397 throw new InflateException("Problem reading XML", e);
398 } catch (IOException e) {
399 throw new InflateException("Problem reading XML", e);
400 }
401 }
402 }
403
404 /**
405 * Borrowed from Launcher for {@link Bitmap} resizing.
406 */
407 static final class Utilities {
408 private static final Paint sPaint = new Paint();
409 private static final Rect sBounds = new Rect();
410 private static final Rect sOldBounds = new Rect();
411 private static Canvas sCanvas = new Canvas();
412
413 static {
414 sCanvas.setDrawFilter(new PaintFlagsDrawFilter(Paint.DITHER_FLAG,
415 Paint.FILTER_BITMAP_FLAG));
416 }
417
418 /**
419 * Returns a Bitmap representing the thumbnail of the specified Bitmap.
420 * The size of the thumbnail is defined by the dimension
421 * android.R.dimen.launcher_application_icon_size. This method is not
422 * thread-safe and should be invoked on the UI thread only.
423 *
424 * @param bitmap The bitmap to get a thumbnail of.
425 * @param context The application's context.
426 * @return A thumbnail for the specified bitmap or the bitmap itself if
427 * the thumbnail could not be created.
428 */
429 static Bitmap createBitmapThumbnail(Bitmap bitmap, Context context, int size) {
430 int width = size;
431 int height = size;
432
433 final int bitmapWidth = bitmap.getWidth();
434 final int bitmapHeight = bitmap.getHeight();
435
436 if (width > 0 && height > 0 && (width < bitmapWidth || height < bitmapHeight)) {
437 final float ratio = (float)bitmapWidth / bitmapHeight;
438
439 if (bitmapWidth > bitmapHeight) {
440 height = (int)(width / ratio);
441 } else if (bitmapHeight > bitmapWidth) {
442 width = (int)(height * ratio);
443 }
444
445 final Bitmap.Config c = (width == size && height == size) ? bitmap.getConfig()
446 : Bitmap.Config.ARGB_8888;
447 final Bitmap thumb = Bitmap.createBitmap(size, size, c);
448 final Canvas canvas = sCanvas;
449 final Paint paint = sPaint;
450 canvas.setBitmap(thumb);
451 paint.setDither(false);
452 paint.setFilterBitmap(true);
453 sBounds.set((size - width) / 2, (size - height) / 2, width, height);
454 sOldBounds.set(0, 0, bitmapWidth, bitmapHeight);
455 canvas.drawBitmap(bitmap, sOldBounds, sBounds, paint);
456 return thumb;
457 }
458
459 return bitmap;
460 }
Dmitri Plotnikov06191cd2009-05-07 14:11:52 -0700461 }
462}