OSDN Git Service

Include Google Talk presence, even when missing Im rows.
authorJeff Sharkey <jsharkey@android.com>
Fri, 2 Oct 2009 04:32:19 +0000 (21:32 -0700)
committerJeff Sharkey <jsharkey@android.com>
Tue, 6 Oct 2009 20:10:17 +0000 (13:10 -0700)
When inserting Google Talk presence updates, we match on
both Im and Email rows.  This change adds presence "dots"
to individual Im rows, and creates in-memory Im rows when
Email entries have presence.

This loads status details through a second query and holds
back building UI until both queries finish.  This change
also generalizes logic for building Im intents borrowed
from FastTrack code.

This change also fixes a regression that was dropping third-
party data rows.  The second-query approach above allows us
to remove a large chunk of code that was using old API.

Fixes http://b/2161796

res/layout-finger/contacts_list_item.xml
res/layout-finger/contacts_list_item_photo.xml
res/layout-finger/list_item_text_icons.xml
res/layout-finger/recent_calls_list_item.xml
src/com/android/contacts/ContactsUtils.java
src/com/android/contacts/ViewContactActivity.java
src/com/android/contacts/model/ContactsSource.java
src/com/android/contacts/model/ExternalSource.java
src/com/android/contacts/ui/QuickContactWindow.java
src/com/android/contacts/util/DataStatus.java [new file with mode: 0644]

index 74f5bdd..61cc839 100644 (file)
             android:layout_alignParentRight="true">
 
             <ImageView android:id="@+id/presence"
-                android:layout_width="32dip"
-                android:layout_height="32dip"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
                 android:layout_marginLeft="5dip"
                 android:layout_marginRight="5dip"
-
+                android:padding="7dip"
                 android:layout_gravity="center_vertical"
                 android:scaleType="centerInside"
             />
index b7fcd7a..c9b4c1c 100644 (file)
             android:layout_alignParentRight="true">
 
             <ImageView android:id="@+id/presence"
-                android:layout_width="32dip"
-                android:layout_height="32dip"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
                 android:layout_marginLeft="5dip"
                 android:layout_marginRight="5dip"
-
+                android:padding="7dip"
                 android:layout_gravity="center_vertical"
                 android:scaleType="centerInside"
             />
index 552d28a..8d4b7a8 100644 (file)
                 android:paddingLeft="3dip"
                 android:src="@drawable/ic_default_number"
             />
+
         </LinearLayout>
-        
+
+        <TextView
+            android:id="@+id/footer"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:visibility="gone" />
+
     </LinearLayout>
 
     <ImageView android:id="@+id/presence_icon"
-        android:layout_width="32dip"
-        android:layout_height="32dip"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         android:layout_marginLeft="5dip"
         android:gravity="center"
         android:scaleType="centerInside"
     />
 
     <ImageView android:id="@+id/action_icon"
-        android:layout_width="wrap_content"
-        android:layout_height="fill_parent"
+        android:layout_width="30dip"
+        android:layout_height="30dip"
         android:layout_marginLeft="14dip"
         android:layout_marginRight="14dip"
         android:gravity="center"
index 288892d..5439ff3 100644 (file)
     <View android:id="@+id/divider"
         android:layout_width="1px"
         android:layout_height="fill_parent"
+        android:layout_marginTop="5dip"
+        android:layout_marginBottom="5dip"
         android:layout_toLeftOf="@id/call_icon"
         android:layout_marginLeft="11dip"
-
-        android:background="@*android:drawable/divider_vertical_dark_opaque"
+        android:background="@drawable/divider_vertical_dark"
     />
 
     <ImageView android:id="@+id/call_type_icon"
index 2a3c22d..25da482 100644 (file)
@@ -19,6 +19,7 @@ package com.android.contacts;
 
 import android.content.ContentResolver;
 import android.content.ContentUris;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
@@ -222,6 +223,31 @@ public class ContactsUtils {
         return null;
     }
 
+    /**
+     * Build {@link Intent} to launch an action for the given {@link Im} or
+     * {@link Email} row. Returns null when missing protocol or data.
+     */
+    public static Intent buildImIntent(ContentValues values) {
+        final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(values.getAsString(Data.MIMETYPE));
+        final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : values.getAsInteger(Im.PROTOCOL);
+
+        String host = values.getAsString(Im.CUSTOM_PROTOCOL);
+        String data = values.getAsString(isEmail ? Email.DATA : Im.DATA);
+        if (protocol != Im.PROTOCOL_CUSTOM) {
+            // Try bringing in a well-known host for specific protocols
+            host = ContactsUtils.lookupProviderNameFromId(protocol);
+        }
+
+        if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) {
+            final String authority = host.toLowerCase();
+            final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority(
+                    authority).appendPath(data).build();
+            return new Intent(Intent.ACTION_SENDTO, imUri);
+        } else {
+            return null;
+        }
+    }
+
     public static Intent getPhotoPickIntent() {
         Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
         intent.setType("image/*");
index d6ab83c..3d5ac85 100644 (file)
@@ -22,9 +22,12 @@ import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
 import com.android.contacts.ui.EditContactActivity;
 import com.android.contacts.util.Constants;
+import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.NotifyingAsyncQueryHandler;
 import com.android.internal.telephony.ITelephony;
 import com.android.internal.widget.ContactHeaderWidget;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
 
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -55,7 +58,13 @@ import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.util.Log;
@@ -77,6 +86,7 @@ import android.widget.TextView;
 import android.widget.Toast;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 
 /**
  * Displays the details of a specific contact.
@@ -129,7 +139,14 @@ public class ViewContactActivity extends Activity
     protected int mWritableSourcesCnt;
     protected ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
 
-    private static final int TOKEN_QUERY = 0;
+    private static final int TOKEN_ENTITIES = 0;
+    private static final int TOKEN_STATUSES = 1;
+
+    private boolean mHasEntities = false;
+    private boolean mHasStatuses = false;
+
+    private ArrayList<Entity> mEntities = Lists.newArrayList();
+    private HashMap<Long, DataStatus> mStatuses = Maps.newHashMap();
 
     private ContentObserver mObserver = new ContentObserver(new Handler()) {
         @Override
@@ -154,7 +171,6 @@ public class ViewContactActivity extends Activity
     private FrameLayout mTabContentLayout;
     private ListView mListView;
     private boolean mShowSmsLinksForAllPhones;
-    private ArrayList<Entity> mEntities = null;
 
     @Override
     protected void onCreate(Bundle icicle) {
@@ -273,11 +289,10 @@ public class ViewContactActivity extends Activity
     // QUERY CODE //
     /** {@inheritDoc} */
     public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
-        try{
-            if (token == TOKEN_QUERY) {
-                mEntities = readEntities(iterator);
-                bindData();
-            }
+        try {
+            // Read incoming entities and consider binding
+            readEntities(iterator);
+            considerBindData();
         } finally {
             if (iterator != null) {
                 iterator.close();
@@ -287,7 +302,15 @@ public class ViewContactActivity extends Activity
 
     /** {@inheritDoc} */
     public void onQueryComplete(int token, Object cookie, Cursor cursor) {
-        // Empty
+        try {
+            // Read available social rows and consider binding
+            readStatuses(cursor);
+            considerBindData();
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
     }
 
     private long getRefreshedContactId() {
@@ -298,19 +321,40 @@ public class ViewContactActivity extends Activity
         return -1;
     }
 
-    private ArrayList<Entity> readEntities(EntityIterator iterator) {
-        ArrayList<Entity> entities = new ArrayList<Entity>();
+    /**
+     * Read from the given {@link EntityIterator} to build internal set of
+     * {@link #mEntities} for data display.
+     */
+    private synchronized void readEntities(EntityIterator iterator) {
+        mEntities.clear();
         try {
             while (iterator.hasNext()) {
-                entities.add(iterator.next());
+                mEntities.add(iterator.next());
             }
+            mHasEntities = true;
         } catch (RemoteException e) {
+            Log.w(TAG, "Problem reading contact data: " + e.toString());
+        }
+    }
+
+    /**
+     * Read from the given {@link Cursor} and build a set of {@link DataStatus}
+     * objects to match any valid statuses found.
+     */
+    private synchronized void readStatuses(Cursor cursor) {
+        mStatuses.clear();
+
+        // Walk found statuses, creating internal row for each
+        while (cursor.moveToNext()) {
+            final DataStatus status = new DataStatus(cursor);
+            final long dataId = cursor.getLong(StatusQuery._ID);
+            mStatuses.put(dataId, status);
         }
 
-        return entities;
+        mHasStatuses = true;
     }
 
-    private void startEntityQuery() {
+    private synchronized void startEntityQuery() {
         closeCursor();
 
         Uri uri = null;
@@ -331,13 +375,24 @@ public class ViewContactActivity extends Activity
             return;
         }
 
-        mCursor = mResolver.query(Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY),
+        final Uri dataUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
+
+        // Keep stub cursor open on side to watch for change events
+        mCursor = mResolver.query(dataUri,
                 new String[] {Contacts.DISPLAY_NAME}, null, null, null);
         mCursor.registerContentObserver(mObserver);
 
-        long contactId = ContentUris.parseId(uri);
-        mHandler.startQueryEntities(TOKEN_QUERY, null,
-                RawContacts.CONTENT_URI, RawContacts.CONTACT_ID + "=" + contactId, null, null);
+        final long contactId = ContentUris.parseId(uri);
+
+        // Clear flags and start queries to data and status
+        mHasEntities = false;
+        mHasStatuses = false;
+
+        mHandler.startQueryEntities(TOKEN_ENTITIES, null, RawContacts.CONTENT_URI,
+                RawContacts.CONTACT_ID + "=" + contactId, null, null);
+        mHandler.startQuery(TOKEN_STATUSES, null, dataUri, StatusQuery.PROJECTION,
+                        StatusUpdates.PRESENCE + " IS NOT NULL OR " + StatusUpdates.STATUS
+                                + " IS NOT NULL", null, null);
 
         mContactHeaderWidget.bindFromContactLookupUri(mLookupUri);
     }
@@ -350,6 +405,17 @@ public class ViewContactActivity extends Activity
         }
     }
 
+    /**
+     * Consider binding views after any of several background queries has
+     * completed. We check internal flags and only bind when all data has
+     * arrived.
+     */
+    private void considerBindData() {
+        if (mHasEntities && mHasStatuses) {
+            bindData();
+        }
+    }
+
     private void bindData() {
 
         // Build up the contact entries
@@ -360,6 +426,7 @@ public class ViewContactActivity extends Activity
         Collapser.collapseList(mSmsEntries);
         Collapser.collapseList(mEmailEntries);
         Collapser.collapseList(mPostalEntries);
+        Collapser.collapseList(mImEntries);
 
         if (mAdapter == null) {
             mAdapter = new ViewAdapter(this, mSections);
@@ -689,15 +756,6 @@ public class ViewContactActivity extends Activity
         //TODO: implement this when we have the sonification APIs
     }
 
-    private Uri constructImToUrl(String host, String data) {
-        // don't encode the url, because the Activity Manager can't find using the encoded url
-        StringBuilder buf = new StringBuilder("imto://");
-        buf.append(host);
-        buf.append('/');
-        buf.append(data);
-        return Uri.parse(buf.toString());
-    }
-
     /**
      * Build up the entries to display on the screen.
      *
@@ -712,10 +770,11 @@ public class ViewContactActivity extends Activity
 
         mRawContactIds.clear();
         mReadOnlySourcesCnt = 0;
-       mWritableSourcesCnt = 0;
+        mWritableSourcesCnt = 0;
         mWritableRawContactIds.clear();
 
-        Sources sources = Sources.getInstance(this);
+        final Context context = this;
+        final Sources sources = Sources.getInstance(context);
 
         // Build up method entries
         if (mLookupUri != null) {
@@ -738,50 +797,32 @@ public class ViewContactActivity extends Activity
 
 
                 for (NamedContentValues subValue : entity.getSubValues()) {
-                    ViewEntry entry = new ViewEntry();
-
                     final ContentValues entryValues = subValue.values;
                     entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
 
-                    final String mimetype = entryValues.getAsString(Data.MIMETYPE);
-                    if (mimetype == null) continue;
+                    final long dataId = entryValues.getAsLong(Data._ID);
+                    final String mimeType = entryValues.getAsString(Data.MIMETYPE);
+                    if (mimeType == null) continue;
 
-                    final DataKind kind = sources.getKindOrFallback(accountType, mimetype, this,
+                    final DataKind kind = sources.getKindOrFallback(accountType, mimeType, this,
                             ContactsSource.LEVEL_MIMETYPES);
                     if (kind == null) continue;
 
-                    final long id = entryValues.getAsLong(Data._ID);
-                    final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
-                    entry.contactId = rawContactId;
-                    entry.id = id;
-                    entry.uri = uri;
-                    entry.mimetype = mimetype;
-                    entry.label = buildActionString(kind, entryValues, false);
-                    entry.data = buildDataString(kind, entryValues);
-                    if (kind.typeColumn != null && entryValues.containsKey(kind.typeColumn)) {
-                        entry.type = entryValues.getAsInteger(kind.typeColumn);
-                    }
-                    if (kind.iconRes > 0) {
-                        entry.resPackageName = kind.resPackageName;
-                        entry.actionIcon = kind.iconRes;
-                    }
-
-                    // Don't crash if the data is bogus
-                    if (TextUtils.isEmpty(entry.data)) {
-                        continue;
-                    }
+                    final ViewEntry entry = ViewEntry.fromValues(context, mimeType, kind,
+                            rawContactId, dataId, entryValues);
 
+                    final boolean hasData = !TextUtils.isEmpty(entry.data);
                     final boolean isSuperPrimary = entryValues.getAsInteger(
                             Data.IS_SUPER_PRIMARY) != 0;
 
-                    if (CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                    if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
                         // Build phone entries
                         mNumPhoneNumbers++;
 
                         entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
-                                Uri.fromParts("tel", entry.data, null));
+                                Uri.fromParts(Constants.SCHEME_TEL, entry.data, null));
                         entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO,
-                                Uri.fromParts("sms", entry.data, null));
+                                Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null));
                         entry.data = PhoneNumberUtils.stripSeparators(entry.data);
 
                         entry.isPrimary = isSuperPrimary;
@@ -794,107 +835,161 @@ public class ViewContactActivity extends Activity
                                 entry.secondaryActionIcon = kind.iconAltRes;
                             }
                         }
-                    } else if (CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                    } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
                         // Build email entries
                         entry.intent = new Intent(Intent.ACTION_SENDTO,
-                                Uri.fromParts("mailto", entry.data, null));
+                                Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
                         entry.isPrimary = isSuperPrimary;
                         mEmailEntries.add(entry);
-                    } else if (CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE.
-                            equals(mimetype)) {
+
+                        // When Email rows have status, create additional Im row
+                        final DataStatus status = mStatuses.get(entry.id);
+                        if (status != null) {
+                            final String imMime = Im.CONTENT_ITEM_TYPE;
+                            final DataKind imKind = sources.getKindOrFallback(accountType,
+                                    imMime, this, ContactsSource.LEVEL_MIMETYPES);
+                            final ViewEntry imEntry = ViewEntry.fromValues(context,
+                                    imMime, imKind, rawContactId, dataId, entryValues);
+                            imEntry.intent = ContactsUtils.buildImIntent(entryValues);
+                            imEntry.applyStatus(status, false);
+                            mImEntries.add(imEntry);
+                        }
+                    } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
                         // Build postal entries
                         entry.maxLines = 4;
-                        entry.intent = new Intent(Intent.ACTION_VIEW, uri);
+                        entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
                         mPostalEntries.add(entry);
-                    } else if (CommonDataKinds.Im.CONTENT_ITEM_TYPE.equals(mimetype)) {
-                        // Build im entries
-                        Object protocolObj = entryValues.getAsInteger(CommonDataKinds.Im.PROTOCOL);
-                        String host = null;
-
+                    } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
+                        // Build IM entries
+                        entry.intent = ContactsUtils.buildImIntent(entryValues);
                         if (TextUtils.isEmpty(entry.label)) {
                             entry.label = getString(R.string.chat).toLowerCase();
                         }
 
-                        if (protocolObj instanceof Number) {
-                            int protocol = ((Number) protocolObj).intValue();
-                            host = ContactsUtils.lookupProviderNameFromId(protocol);
-                            if (protocol == CommonDataKinds.Im.PROTOCOL_GOOGLE_TALK
-                                    || protocol == CommonDataKinds.Im.PROTOCOL_MSN) {
-                                entry.maxLabelLines = 2;
-                            }
-                        } else if (protocolObj != null) {
-                            String providerName = (String) protocolObj;
-                            host = providerName.toLowerCase();
-                        }
-
-                        // Only add the intent if there is a valid host
-                        //  host is null for CommonDataKinds.Im.PROTOCOL_CUSTOM
-                        if (!TextUtils.isEmpty(host)) {
-                            entry.intent = new Intent(Intent.ACTION_SENDTO,
-                                    constructImToUrl(host.toLowerCase(), entry.data));
+                        // Apply presence and status details when available
+                        final DataStatus status = mStatuses.get(entry.id);
+                        if (status != null) {
+                            entry.applyStatus(status, false);
                         }
-                        //TODO(emillar) Add in presence info
-                            /*if (!aggCursor.isNull(METHODS_STATUS_COLUMN)) {
-                            entry.presenceIcon = Presence.getPresenceIconResourceId(
-                                    aggCursor.getInt(METHODS_STATUS_COLUMN));
-                            entry.status = ...
-                        }*/
                         mImEntries.add(entry);
-                    } else if (CommonDataKinds.Organization.CONTENT_ITEM_TYPE.equals(mimetype)
-                            || CommonDataKinds.Nickname.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                    } else if ((Organization.CONTENT_ITEM_TYPE.equals(mimeType)
+                            || Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) && hasData) {
                         // Build organization and note entries
                         entry.uri = null;
                         mOrganizationEntries.add(entry);
-                    } else if (CommonDataKinds.Note.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                    } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
                         // Build note entries
                         entry.uri = null;
                         entry.maxLines = 10;
                         mOtherEntries.add(entry);
                     } else {
-                        // Handle showing custom
+                        // Handle showing custom rows
                         entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
-                        mOtherEntries.add(entry);
+
+                        // Use social summary when requested by external source
+                        final DataStatus status = mStatuses.get(entry.id);
+                        final boolean hasSocial = kind.actionBodySocial && status != null;
+                        if (hasSocial) {
+                            entry.applyStatus(status, true);
+                        }
+
+                        if (hasSocial || hasData) {
+                            mOtherEntries.add(entry);
+                        }
                     }
                 }
             }
         }
     }
 
-    String buildActionString(DataKind kind, ContentValues values, boolean lowerCase) {
+    static String buildActionString(DataKind kind, ContentValues values, boolean lowerCase,
+            Context context) {
         if (kind.actionHeader == null) {
             return null;
         }
-        CharSequence actionHeader = kind.actionHeader.inflateUsing(this, values);
+        CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
         if (actionHeader == null) {
             return null;
         }
         return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
     }
 
-    String buildDataString(DataKind kind, ContentValues values) {
+    static String buildDataString(DataKind kind, ContentValues values, Context context) {
         if (kind.actionBody == null) {
             return null;
         }
-        CharSequence actionBody = kind.actionBody.inflateUsing(this, values);
+        CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
         return actionBody == null ? null : actionBody.toString();
     }
 
     /**
      * A basic structure with the data for a contact entry in the list.
      */
-    class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
+    static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
+        public Context context = null;
         public String resPackageName = null;
         public int actionIcon = -1;
         public boolean isPrimary = false;
-        public int presenceIcon = -1;
         public int secondaryActionIcon = -1;
         public Intent intent;
         public Intent secondaryIntent = null;
-        public int status = -1;
         public int maxLabelLines = 1;
         public ArrayList<Long> ids = new ArrayList<Long>();
         public int collapseCount = 0;
 
+        public int presence = -1;
+        public int presenceIcon = -1;
+
+        public CharSequence footerLine = null;
+
+        private ViewEntry() {
+        }
+
+        /**
+         * Build new {@link ViewEntry} and populate from the given values.
+         */
+        public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
+                long rawContactId, long dataId, ContentValues values) {
+            final ViewEntry entry = new ViewEntry();
+            entry.context = context;
+            entry.contactId = rawContactId;
+            entry.id = dataId;
+            entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
+            entry.mimetype = mimeType;
+            entry.label = buildActionString(kind, values, false, context);
+            entry.data = buildDataString(kind, values, context);
+
+            if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
+                entry.type = values.getAsInteger(kind.typeColumn);
+            }
+            if (kind.iconRes > 0) {
+                entry.resPackageName = kind.resPackageName;
+                entry.actionIcon = kind.iconRes;
+            }
+
+            return entry;
+        }
+
+        /**
+         * Apply given {@link DataStatus} values over this {@link ViewEntry}
+         *
+         * @param fillData When true, the given status replaces {@link #data}
+         *            and {@link #footerLine}. Otherwise only {@link #presence}
+         *            is updated.
+         */
+        public ViewEntry applyStatus(DataStatus status, boolean fillData) {
+            presence = status.getPresence();
+            presenceIcon = (presence == -1) ? -1 :
+                    StatusUpdates.getPresenceIconResourceId(this.presence);
+
+            if (fillData && status.isValid()) {
+                this.data = status.getStatus().toString();
+                this.footerLine = status.getTimestampLabel(context);
+            }
+
+            return this;
+        }
+
         public boolean collapseWith(ViewEntry entry) {
             // assert equal collapse keys
             if (!shouldCollapseWith(entry)) {
@@ -913,9 +1008,9 @@ public class ViewContactActivity extends Activity
             maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
 
             // Choose the presence with the highest precedence.
-            if (StatusUpdates.getPresencePrecedence(status)
-                    < StatusUpdates.getPresencePrecedence(entry.status)) {
-                status = entry.status;
+            if (StatusUpdates.getPresencePrecedence(presence)
+                    < StatusUpdates.getPresencePrecedence(entry.presence)) {
+                presence = entry.presence;
             }
 
             // If any of the collapsed entries are primary make the whole thing primary.
@@ -936,7 +1031,7 @@ public class ViewContactActivity extends Activity
 
             if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)
                     && Phone.CONTENT_ITEM_TYPE.equals(entry.mimetype)) {
-                if (!PhoneNumberUtils.compare(ViewContactActivity.this, data, entry.data)) {
+                if (!PhoneNumberUtils.compare(this.context, data, entry.data)) {
                     return false;
                 }
             } else {
@@ -973,6 +1068,7 @@ public class ViewContactActivity extends Activity
     static class ViewCache {
         public TextView label;
         public TextView data;
+        public TextView footer;
         public ImageView actionIcon;
         public ImageView presenceIcon;
         public ImageView primaryIcon;
@@ -1015,6 +1111,7 @@ public class ViewContactActivity extends Activity
                 views = new ViewCache();
                 views.label = (TextView) v.findViewById(android.R.id.text1);
                 views.data = (TextView) v.findViewById(android.R.id.text2);
+                views.footer = (TextView) v.findViewById(R.id.footer);
                 views.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
                 views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
                 views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
@@ -1061,6 +1158,14 @@ public class ViewContactActivity extends Activity
                 setMaxLines(data, entry.maxLines);
             }
 
+            // Set the footer
+            if (!TextUtils.isEmpty(entry.footerLine)) {
+                views.footer.setText(entry.footerLine);
+                views.footer.setVisibility(View.VISIBLE);
+            } else {
+                views.footer.setVisibility(View.GONE);
+            }
+
             // Set the primary icon
             views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
 
@@ -1086,9 +1191,9 @@ public class ViewContactActivity extends Activity
             Drawable presenceIcon = null;
             if (entry.presenceIcon != -1) {
                 presenceIcon = resources.getDrawable(entry.presenceIcon);
-            } else if (entry.status != -1) {
+            } else if (entry.presence != -1) {
                 presenceIcon = resources.getDrawable(
-                        StatusUpdates.getPresenceIconResourceId(entry.status));
+                        StatusUpdates.getPresenceIconResourceId(entry.presence));
             }
             ImageView presenceIconView = views.presenceIcon;
             if (presenceIcon != null) {
@@ -1126,4 +1231,18 @@ public class ViewContactActivity extends Activity
             }
         }
     }
+
+    private interface StatusQuery {
+        final String[] PROJECTION = new String[] {
+                Data._ID,
+                Data.STATUS,
+                Data.STATUS_RES_PACKAGE,
+                Data.STATUS_ICON,
+                Data.STATUS_LABEL,
+                Data.STATUS_TIMESTAMP,
+                Data.PRESENCE,
+        };
+
+        final int _ID = 0;
+    }
 }
index f82f8a1..1198837 100644 (file)
@@ -199,9 +199,8 @@ public abstract class ContactsSource {
         public StringInflater actionHeader;
         public StringInflater actionAltHeader;
         public StringInflater actionBody;
-        public StringInflater actionFooter;
-        public boolean actionBodySocial;
-        public boolean actionBodyCombine;
+
+        public boolean actionBodySocial = false;
 
         public String typeColumn;
         public int typeOverallMax;
index 11e06c3..d2f14dc 100644 (file)
@@ -19,18 +19,12 @@ package com.android.contacts.model;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
-import android.database.Cursor;
-import android.provider.ContactsContract.Data;
-import android.provider.SocialContract.Activities;
-import android.text.format.DateUtils;
 import android.util.AttributeSet;
 import android.util.Xml;
 
@@ -172,8 +166,7 @@ public class ExternalSource extends FallbackSource {
                         false);
                 if (detailSocialSummary) {
                     // Inflate social summary when requested
-                    kind.actionBody = new SocialInflater(false);
-                    kind.actionFooter = new SocialInflater(true);
+                    kind.actionBodySocial = true;
                 } else {
                     // Otherwise inflate specific column as summary
                     kind.actionBody = new FallbackSource.SimpleInflater(detailColumn);
@@ -188,83 +181,6 @@ public class ExternalSource extends FallbackSource {
         }
     }
 
-    /**
-     * Temporary cache to hold recent social data.
-     */
-    private static class SocialCache {
-        private static Status sLastStatus = null;
-
-        public static class Status {
-            public long rawContactId;
-            public CharSequence title;
-            public long published;
-        }
-
-        public static synchronized Status getLatestStatus(Context context, long rawContactId) {
-            if (sLastStatus == null || sLastStatus.rawContactId != rawContactId) {
-                // Cache missing, or miss, so query directly
-                sLastStatus = queryLatestStatus(context, rawContactId);
-            }
-            return sLastStatus;
-        }
-
-        private static Status queryLatestStatus(Context context, long rawContactId) {
-            // Find latest social update by this person, filtering to show only
-            // original content and avoid replies.
-            final ContentResolver resolver = context.getContentResolver();
-            final Cursor cursor = resolver.query(Activities.CONTENT_URI, new String[] {
-                Activities.TITLE, Activities.PUBLISHED
-            }, Activities.AUTHOR_CONTACT_ID + "=" + rawContactId + " AND "
-                    + Activities.IN_REPLY_TO + " IS NULL", null, Activities.PUBLISHED + " DESC");
-
-            final Status status = new Status();
-            try {
-                if (cursor != null && cursor.moveToFirst()) {
-                    status.title = cursor.getString(0);
-                    status.published = cursor.getLong(1);
-                }
-            } finally {
-                if (cursor != null) cursor.close();
-            }
-            return status;
-        }
-    }
-
-    /**
-     * Inflater that will return the latest {@link Activities#TITLE} and
-     * {@link Activities#PUBLISHED} for the given {@link Data#RAW_CONTACT_ID}.
-     */
-    protected static class SocialInflater implements StringInflater {
-        private final boolean mPublishedMode;
-
-        public SocialInflater(boolean publishedMode) {
-            mPublishedMode = publishedMode;
-        }
-
-        protected CharSequence inflatePublished(long published) {
-            return DateUtils.getRelativeTimeSpanString(published, System.currentTimeMillis(),
-                    DateUtils.MINUTE_IN_MILLIS);
-        }
-
-        /** {@inheritDoc} */
-        public CharSequence inflateUsing(Context context, Cursor cursor) {
-            final Long rawContactId = cursor.getLong(cursor.getColumnIndex(Data.RAW_CONTACT_ID));
-            if (rawContactId == null) return null;
-
-            final SocialCache.Status status = SocialCache.getLatestStatus(context, rawContactId);
-            return mPublishedMode ? inflatePublished(status.published) : status.title;
-        }
-
-        /** {@inheritDoc} */
-        public CharSequence inflateUsing(Context context, ContentValues values) {
-            final Long rawContactId = values.getAsLong(Data.RAW_CONTACT_ID);
-            if (rawContactId == null) return null;
-
-            final SocialCache.Status status = SocialCache.getLatestStatus(context, rawContactId);
-            return mPublishedMode ? inflatePublished(status.published) : status.title;
-        }
-    }
-
     @Override
     public int getHeaderColor(Context context) {
         return 0xff7f93bc;
index 747c204..45cb73f 100644 (file)
@@ -23,6 +23,7 @@ import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
 import com.android.contacts.ui.widget.CheckableImageView;
 import com.android.contacts.util.Constants;
+import com.android.contacts.util.DataStatus;
 import com.android.contacts.util.NotifyingAsyncQueryHandler;
 import com.android.internal.policy.PolicyManager;
 import com.google.android.collect.Lists;
@@ -712,6 +713,13 @@ public class QuickContactWindow implements Window.Callback,
                 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK :
                         getAsInt(cursor, Im.PROTOCOL);
 
+                if (isEmail) {
+                    // Use Google Talk string when using Email, and clear data
+                    // Uri so we don't try saving Email as primary.
+                    mHeader = context.getText(R.string.chat_gtalk);
+                    mDataUri = null;
+                }
+
                 String host = getAsString(cursor, Im.CUSTOM_PROTOCOL);
                 String data = getAsString(cursor, isEmail ? Email.DATA : Im.DATA);
                 if (protocol != Im.PROTOCOL_CUSTOM) {
@@ -997,99 +1005,6 @@ public class QuickContactWindow implements Window.Callback,
     }
 
     /**
-     * Internal storage for the latest social status, as built when walking
-     * across a {@Link DataQuery} query. Will always keep record of at
-     * least the first status it encounters, but will replace it with newer
-     * statuses, as determined by timestamps.
-     */
-    private static class LatestStatus {
-        private String mStatus = null;
-        private long mTimestamp = -1;
-
-        private String mResPackage = null;
-        private int mIconRes = -1;
-        private int mLabelRes = -1;
-
-        private int getCursorInt(Cursor cursor, int columnIndex, int missingValue) {
-            if (cursor.isNull(columnIndex)) return missingValue;
-            return cursor.getInt(columnIndex);
-        }
-
-        /**
-         * Attempt updating this {@link LatestStatus} based on values at the
-         * current row of the given {@link Cursor}. Assumes that query
-         * projection was {@link DataQuery#PROJECTION}.
-         */
-        public void possibleUpdate(Cursor cursor) {
-            final boolean hasStatus = !cursor.isNull(DataQuery.STATUS);
-            final boolean hasTimestamp = !cursor.isNull(DataQuery.STATUS_TIMESTAMP);
-
-            // Bail early when not valid status, or when previous status was
-            // found and we can't compare this one.
-            if (!hasStatus) return;
-            if (isValid() && !hasTimestamp) return;
-
-            if (hasTimestamp) {
-                // Compare timestamps and bail if older status
-                final long newTimestamp = cursor.getLong(DataQuery.STATUS_TIMESTAMP);
-                if (newTimestamp < mTimestamp) return;
-
-                mTimestamp = newTimestamp;
-            }
-
-            // Fill in remaining details from cursor
-            mStatus = cursor.getString(DataQuery.STATUS);
-            mResPackage = cursor.getString(DataQuery.STATUS_RES_PACKAGE);
-            mIconRes = getCursorInt(cursor, DataQuery.STATUS_ICON, -1);
-            mLabelRes = getCursorInt(cursor, DataQuery.STATUS_LABEL, -1);
-        }
-
-        public boolean isValid() {
-            return !TextUtils.isEmpty(mStatus);
-        }
-
-        public CharSequence getStatus() {
-            return mStatus;
-        }
-
-        /**
-         * Build any timestamp and label into a single string.
-         */
-        public CharSequence getTimestampLabel(Context context) {
-            final PackageManager pm = context.getPackageManager();
-
-            final boolean validTimestamp = mTimestamp > 0;
-            final boolean validLabel = mResPackage != null && mLabelRes != -1;
-
-            final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
-                    mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
-                    DateUtils.FORMAT_ABBREV_RELATIVE) : null;
-            final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
-                    null) : null;
-
-            if (validTimestamp && validLabel) {
-                return context.getString(
-                        com.android.internal.R.string.contact_status_update_attribution_with_date,
-                        timeClause, labelClause);
-            } else if (validLabel) {
-                return context.getString(
-                        com.android.internal.R.string.contact_status_update_attribution,
-                        labelClause);
-            } else if (validTimestamp) {
-                return timeClause;
-            } else {
-                return null;
-            }
-        }
-
-        public Drawable getIcon(Context context) {
-            final PackageManager pm = context.getPackageManager();
-            final boolean validIcon = mResPackage != null && mIconRes != -1;
-            return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
-        }
-    }
-
-    /**
      * Handle the result from the {@link #TOKEN_DATA} query.
      */
     private void handleData(Cursor cursor) {
@@ -1101,7 +1016,7 @@ public class QuickContactWindow implements Window.Callback,
             mActions.collect(Contacts.CONTENT_ITEM_TYPE, action);
         }
 
-        final LatestStatus status = new LatestStatus();
+        final DataStatus status = new DataStatus();
         final Sources sources = Sources.getInstance(mContext);
         final ImageView photoView = (ImageView)mHeader.findViewById(R.id.photo);
 
diff --git a/src/com/android/contacts/util/DataStatus.java b/src/com/android/contacts/util/DataStatus.java
new file mode 100644 (file)
index 0000000..9d12894
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.Data;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+
+/**
+ * Storage for a social status update. Holds a single update, but can use
+ * {@link #possibleUpdate(Cursor)} to consider updating when a better status
+ * exists. Statuses with timestamps, or with newer timestamps win.
+ */
+public class DataStatus {
+    private int mPresence = -1;
+    private String mStatus = null;
+    private long mTimestamp = -1;
+
+    private String mResPackage = null;
+    private int mIconRes = -1;
+    private int mLabelRes = -1;
+
+    public DataStatus() {
+    }
+
+    public DataStatus(Cursor cursor) {
+        // When creating from cursor row, fill normally
+        fromCursor(cursor);
+    }
+
+    /**
+     * Attempt updating this {@link DataStatus} based on values at the
+     * current row of the given {@link Cursor}.
+     */
+    public void possibleUpdate(Cursor cursor) {
+        final boolean hasStatus = !isNull(cursor, Data.STATUS);
+        final boolean hasTimestamp = !isNull(cursor, Data.STATUS_TIMESTAMP);
+
+        // Bail early when not valid status, or when previous status was
+        // found and we can't compare this one.
+        if (!hasStatus) return;
+        if (isValid() && !hasTimestamp) return;
+
+        if (hasTimestamp) {
+            // Compare timestamps and bail if older status
+            final long newTimestamp = getLong(cursor, Data.STATUS_TIMESTAMP, -1);
+            if (newTimestamp < mTimestamp) return;
+
+            mTimestamp = newTimestamp;
+        }
+
+        // Fill in remaining details from cursor
+        fromCursor(cursor);
+    }
+
+    private void fromCursor(Cursor cursor) {
+        mPresence = getInt(cursor, Data.PRESENCE, -1);
+        mStatus = getString(cursor, Data.STATUS);
+        mTimestamp = getLong(cursor, Data.STATUS_TIMESTAMP, -1);
+        mResPackage = getString(cursor, Data.STATUS_RES_PACKAGE);
+        mIconRes = getInt(cursor, Data.STATUS_ICON, -1);
+        mLabelRes = getInt(cursor, Data.STATUS_LABEL, -1);
+    }
+
+    public boolean isValid() {
+        return !TextUtils.isEmpty(mStatus);
+    }
+
+    public int getPresence() {
+        return mPresence;
+    }
+
+    public CharSequence getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Build any timestamp and label into a single string.
+     */
+    public CharSequence getTimestampLabel(Context context) {
+        final PackageManager pm = context.getPackageManager();
+
+        final boolean validTimestamp = mTimestamp > 0;
+        final boolean validLabel = mResPackage != null && mLabelRes != -1;
+
+        final CharSequence timeClause = validTimestamp ? DateUtils.getRelativeTimeSpanString(
+                mTimestamp, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS,
+                DateUtils.FORMAT_ABBREV_RELATIVE) : null;
+        final CharSequence labelClause = validLabel ? pm.getText(mResPackage, mLabelRes,
+                null) : null;
+
+        if (validTimestamp && validLabel) {
+            return context.getString(
+                    com.android.internal.R.string.contact_status_update_attribution_with_date,
+                    timeClause, labelClause);
+        } else if (validLabel) {
+            return context.getString(
+                    com.android.internal.R.string.contact_status_update_attribution,
+                    labelClause);
+        } else if (validTimestamp) {
+            return timeClause;
+        } else {
+            return null;
+        }
+    }
+
+    public Drawable getIcon(Context context) {
+        final PackageManager pm = context.getPackageManager();
+        final boolean validIcon = mResPackage != null && mIconRes != -1;
+        return validIcon ? pm.getDrawable(mResPackage, mIconRes, null) : null;
+    }
+
+    private static String getString(Cursor cursor, String columnName) {
+        return cursor.getString(cursor.getColumnIndex(columnName));
+    }
+
+    private static int getInt(Cursor cursor, String columnName) {
+        return cursor.getInt(cursor.getColumnIndex(columnName));
+    }
+
+    private static int getInt(Cursor cursor, String columnName, int missingValue) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.isNull(columnIndex) ? missingValue : cursor.getInt(columnIndex);
+    }
+
+    private static long getLong(Cursor cursor, String columnName, long missingValue) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.isNull(columnIndex) ? missingValue : cursor.getLong(columnIndex);
+    }
+
+    private static boolean isNull(Cursor cursor, String columnName) {
+        return cursor.isNull(cursor.getColumnIndex(columnName));
+    }
+}