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;
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;
import android.widget.Toast;
import java.util.ArrayList;
+import java.util.HashMap;
/**
* Displays the details of a specific contact.
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
private FrameLayout mTabContentLayout;
private ListView mListView;
private boolean mShowSmsLinksForAllPhones;
- private ArrayList<Entity> mEntities = null;
@Override
protected void onCreate(Bundle icicle) {
// 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();
/** {@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() {
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;
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);
}
}
}
+ /**
+ * 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
Collapser.collapseList(mSmsEntries);
Collapser.collapseList(mEmailEntries);
Collapser.collapseList(mPostalEntries);
+ Collapser.collapseList(mImEntries);
if (mAdapter == null) {
mAdapter = new ViewAdapter(this, mSections);
//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.
*
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) {
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;
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)) {
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.
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 {
static class ViewCache {
public TextView label;
public TextView data;
+ public TextView footer;
public ImageView actionIcon;
public ImageView presenceIcon;
public ImageView primaryIcon;
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);
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);
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) {
}
}
}
+
+ 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;
+ }
}
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;
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) {
}
/**
- * 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) {
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);
--- /dev/null
+/*
+ * 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));
+ }
+}