Include Google Talk presence, even when missing Im rows.
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
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 2a3c22d..25da482 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -19,6 +19,7 @@
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 @@
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/*");
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index d6ab83c..3d5ac85 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -22,9 +22,12 @@
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.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.Toast;
import java.util.ArrayList;
+import java.util.HashMap;
/**
* Displays the details of a specific contact.
@@ -129,7 +139,14 @@
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 @@
private FrameLayout mTabContentLayout;
private ListView mListView;
private boolean mShowSmsLinksForAllPhones;
- private ArrayList<Entity> mEntities = null;
@Override
protected void onCreate(Bundle icicle) {
@@ -273,11 +289,10 @@
// 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 @@
/** {@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 @@
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());
}
-
- return entities;
}
- private void startEntityQuery() {
+ /**
+ * 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);
+ }
+
+ mHasStatuses = true;
+ }
+
+ private synchronized void startEntityQuery() {
closeCursor();
Uri uri = null;
@@ -331,13 +375,24 @@
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 @@
}
}
+ /**
+ * 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 @@
Collapser.collapseList(mSmsEntries);
Collapser.collapseList(mEmailEntries);
Collapser.collapseList(mPostalEntries);
+ Collapser.collapseList(mImEntries);
if (mAdapter == null) {
mAdapter = new ViewAdapter(this, mSections);
@@ -689,15 +756,6 @@
//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 @@
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 @@
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;
- }
+ final ViewEntry entry = ViewEntry.fromValues(context, mimeType, kind,
+ rawContactId, dataId, entryValues);
- // Don't crash if the data is bogus
- if (TextUtils.isEmpty(entry.data)) {
- continue;
- }
-
+ 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 @@
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();
+ // Apply presence and status details when available
+ final DataStatus status = mStatuses.get(entry.id);
+ if (status != null) {
+ entry.applyStatus(status, false);
}
-
- // 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));
- }
- //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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
}
+
+ 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;
+ }
}
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index f82f8a1..1198837 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -199,9 +199,8 @@
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;
diff --git a/src/com/android/contacts/model/ExternalSource.java b/src/com/android/contacts/model/ExternalSource.java
index 11e06c3..d2f14dc 100644
--- a/src/com/android/contacts/model/ExternalSource.java
+++ b/src/com/android/contacts/model/ExternalSource.java
@@ -19,18 +19,12 @@
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 @@
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 @@
}
}
- /**
- * 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;
diff --git a/src/com/android/contacts/ui/QuickContactWindow.java b/src/com/android/contacts/ui/QuickContactWindow.java
index 747c204..45cb73f 100644
--- a/src/com/android/contacts/ui/QuickContactWindow.java
+++ b/src/com/android/contacts/ui/QuickContactWindow.java
@@ -23,6 +23,7 @@
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 @@
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 @@
}
/**
- * 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 @@
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
index 0000000..9d12894
--- /dev/null
+++ b/src/com/android/contacts/util/DataStatus.java
@@ -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));
+ }
+}