Add quick contact badge to MessageView.

- Added "quick contact badge" to MessageView
- Removed PresenceUpdater, and added new implementation based
  on Loader, which is much simpler.
- Changed some text color, so they're visible now.

Bug 2988625

Change-Id: I688a3217178ee8fd0b7245c0ab36a633687ea525
This commit is contained in:
Makoto Onuki 2010-09-07 13:30:59 -07:00
parent 02c0f95734
commit b1ea9c3c12
7 changed files with 474 additions and 443 deletions

View File

@ -43,6 +43,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="true" >
<android.widget.QuickContactBadge
android:id="@+id/badge"
android:layout_gravity="center_vertical"
android:layout_marginRight="8dip"
android:layout_marginLeft="2dip"
style="@*android:style/Widget.QuickContactBadge.WindowSmall" />
/>
<ImageView
android:id="@+id/presence"
android:src="@drawable/presence_inactive"
@ -55,7 +62,7 @@
android:id="@+id/from"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimaryInverse"
android:textColor="?android:attr/textColorPrimary"
android:layout_width="0dip"
android:layout_weight="1.0"
android:layout_height="wrap_content"
@ -72,7 +79,7 @@
<TextView
android:id="@+id/date"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimaryInverse"
android:textColor="?android:attr/textColorPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
@ -84,7 +91,7 @@
android:layout_height="wrap_content" >
<TextView
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textColor="?android:attr/textColorSecondary"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -92,7 +99,7 @@
<TextView
android:id="@+id/to"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textColor="?android:attr/textColorSecondary"
android:layout_width="0dip"
android:layout_weight="1.0"
android:layout_height="wrap_content"
@ -102,7 +109,7 @@
<TextView
android:id="@+id/time"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimaryInverse"
android:textColor="?android:attr/textColorPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
@ -114,7 +121,7 @@
android:layout_height="wrap_content" >
<TextView
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textColor="?android:attr/textColorSecondary"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -122,7 +129,7 @@
<TextView
android:id="@+id/cc"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textColor="?android:attr/textColorSecondary"
android:layout_width="0dip"
android:layout_weight="1.0"
android:layout_height="wrap_content"
@ -136,7 +143,7 @@
<TextView
android:id="@+id/subject"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textColor="?android:attr/textColorSecondary"
android:textStyle="bold"
android:layout_width="0dip"
android:layout_weight="1.0"
@ -166,7 +173,7 @@
android:visibility="gone">
<TextView
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textColor="?android:attr/textColorSecondary"
android:text="@string/message_view_show_pictures_instructions"
android:layout_gravity="center"
android:layout_width="0dip"

View File

@ -803,12 +803,14 @@ public class Utility {
Long defaultValue) {
Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs,
sortOrder);
try {
if (c.moveToFirst()) {
return c.getLong(column);
if (c != null) {
try {
if (c.moveToFirst()) {
return c.getLong(column);
}
} finally {
c.close();
}
} finally {
c.close();
}
return defaultValue;
}
@ -844,6 +846,23 @@ public class Utility {
sortOrder, column, null);
}
public static byte[] getFirstRowBlob(Context context, Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder, int column,
byte[] defaultValue) {
Cursor c = context.getContentResolver().query(uri, projection, selection, selectionArgs,
sortOrder);
if (c != null) {
try {
if (c.moveToFirst()) {
return c.getBlob(column);
}
} finally {
c.close();
}
}
return defaultValue;
}
/**
* A class used to restore ListView state (e.g. scroll position) when changing adapter.
*/

View File

@ -0,0 +1,142 @@
/*
* Copyright (C) 2010 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.email.activity;
import com.android.email.R;
import com.android.email.Utility;
import android.content.AsyncTaskLoader;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.StatusUpdates;
/**
* Loader to load presence statuses and the contact photoes.
*/
public class ContactStatusLoader extends AsyncTaskLoader<ContactStatusLoader.Result> {
public static final int PRESENCE_UNKNOWN_RESOURCE_ID = R.drawable.presence_inactive;
/** email address -> photo id, presence */
/* package */ static final String[] PROJECTION_PHOTO_ID_PRESENCE = new String[] {
Contacts.PHOTO_ID,
Contacts.CONTACT_PRESENCE
};
private static final int COLUMN_PHOTO_ID = 0;
private static final int COLUMN_PRESENCE = 1;
/** photo id -> photo data */
/* package */ static final String[] PHOTO_PROJECTION = new String[] {
Photo.PHOTO
};
private static final int PHOTO_COLUMN = 0;
private final Context mContext;
private final String mEmailAddress;
/**
* Class that encapsulates the result.
*/
public static class Result {
public static final Result UNKNOWN = new Result(null, PRESENCE_UNKNOWN_RESOURCE_ID, null);
/** Contact photo. Null if unknown */
public final Bitmap mPhoto;
/** Presence image resource ID. Always has a valid value, even if unknown. */
public final int mPresenceResId;
/** URI for opening quick contact. Null if unknown. */
public final Uri mLookupUri;
public Result(Bitmap photo, int presenceResId, Uri lookupUri) {
mPhoto = photo;
mPresenceResId = presenceResId;
mLookupUri = lookupUri;
}
}
public ContactStatusLoader(Context context, String emailAddress) {
super(context);
mContext = context;
mEmailAddress = emailAddress;
}
@Override
public Result loadInBackground() {
// Load photo-id and presence status.
Uri uri = Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mEmailAddress));
Cursor c = mContext.getContentResolver().query(
uri,
PROJECTION_PHOTO_ID_PRESENCE, null, null, null);
if (c == null) {
return Result.UNKNOWN;
}
final long photoId;
final int presenceStatus;
try {
if (!c.moveToFirst()) {
return Result.UNKNOWN;
}
photoId = c.getLong(COLUMN_PHOTO_ID);
presenceStatus = c.getInt(COLUMN_PRESENCE);
} finally {
c.close();
}
// Convert presence status into the res id.
final int presenceStatusResId = StatusUpdates.getPresenceIconResourceId(presenceStatus);
// load photo from photo-id.
Bitmap photo = null;
if (photoId != -1) {
final byte[] photoData = Utility.getFirstRowBlob(mContext,
ContentUris.withAppendedId(Data.CONTENT_URI, photoId), PHOTO_PROJECTION,
null, null, null, PHOTO_COLUMN, null);
if (photoData != null) {
photo = BitmapFactory.decodeByteArray(photoData, 0, photoData.length, null);
}
}
// Get lookup URI
final Uri lookupUri = Data.getContactLookupUri(mContext.getContentResolver(), uri);
return new Result(photo, presenceStatusResId, lookupUri);
}
@Override
public void startLoading() {
cancelLoad();
forceLoad();
}
@Override
public void stopLoading() {
cancelLoad();
}
@Override
public void destroy() {
stopLoading();
}
}

View File

@ -35,10 +35,12 @@ import com.android.email.service.AttachmentDownloadService;
import org.apache.commons.io.IOUtils;
import android.app.Fragment;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.Loader;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
@ -61,6 +63,7 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.QuickContactBadge;
import android.widget.TextView;
import java.io.File;
@ -84,6 +87,7 @@ import java.util.regex.Pattern;
* directly. If you need, always load the latest value.
*/
public abstract class MessageViewFragmentBase extends Fragment implements View.OnClickListener {
private static final int PHOTO_LOADER_ID = 1;
private Context mContext;
// Regex that matches start of img tag. '<(?i)img\s+'.
@ -105,6 +109,7 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
private LinearLayout mAttachments;
private ImageView mAttachmentIcon;
private View mShowPicturesSection;
private QuickContactBadge mFromBadge;
private ImageView mSenderPresenceView;
private View mScrollView;
@ -115,7 +120,6 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
private LoadMessageTask mLoadMessageTask;
private LoadBodyTask mLoadBodyTask;
private LoadAttachmentsTask mLoadAttachmentsTask;
private PresenceUpdater mPresenceUpdater;
private java.text.DateFormat mDateFormat;
private java.text.DateFormat mTimeFormat;
@ -134,6 +138,13 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
private boolean mIsMessageLoadedForTest;
private static final int CONTACT_STATUS_STATE_UNLOADED = 0;
private static final int CONTACT_STATUS_STATE_UNLOADED_TRIGGERED = 1;
private static final int CONTACT_STATUS_STATE_LOADED = 2;
private int mContactStatusState;
private Uri mQuickContactLookupUri;
/**
* Encapsulates known information about a single attachment.
*/
@ -206,7 +217,6 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>(
new Handler(), new ControllerResults());
mPresenceUpdater = new PresenceUpdater(mContext);
mDateFormat = android.text.format.DateFormat.getDateFormat(mContext); // short format
mTimeFormat = android.text.format.DateFormat.getTimeFormat(mContext); // 12/24 date format
@ -232,10 +242,12 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
mAttachments = (LinearLayout) view.findViewById(R.id.attachments);
mAttachmentIcon = (ImageView) view.findViewById(R.id.attachment);
mShowPicturesSection = view.findViewById(R.id.show_pictures_section);
mFromBadge = (QuickContactBadge) view.findViewById(R.id.badge);
mSenderPresenceView = (ImageView) view.findViewById(R.id.presence);
mScrollView = view.findViewById(R.id.scrollview);
mFromView.setOnClickListener(this);
mFromBadge.setOnClickListener(this);
mSenderPresenceView.setOnClickListener(this);
view.findViewById(R.id.show_pictures).setOnClickListener(this);
@ -324,9 +336,6 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
mLoadBodyTask = null;
Utility.cancelTaskInterrupt(mLoadAttachmentsTask);
mLoadAttachmentsTask = null;
if (mPresenceUpdater != null) {
mPresenceUpdater.cancelAll();
}
}
/**
@ -358,11 +367,15 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
return mAccountId;
}
protected void openMessageIfStarted() {
protected final void openMessageIfStarted() {
if (!mStarted) {
return;
}
cancelAllTasks();
resetView();
}
protected void resetView() {
if (mMessageContentView != null) {
mMessageContentView.getSettings().setBlockNetworkLoads(true);
mMessageContentView.scrollTo(0, 0);
@ -374,32 +387,40 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
mAttachmentIcon.setVisibility(View.GONE);
mLoadMessageTask = new LoadMessageTask(true);
mLoadMessageTask.execute();
initContactStatusViews();
}
private void initContactStatusViews() {
mContactStatusState = CONTACT_STATUS_STATE_UNLOADED;
mQuickContactLookupUri = null;
mSenderPresenceView.setImageResource(ContactStatusLoader.PRESENCE_UNKNOWN_RESOURCE_ID);
mFromBadge.setImageToDefault();
mFromBadge.assignContactFromEmail("", false);
}
/**
* Handle clicks on sender, which shows {@link QuickContact} or prompts to add
* the sender as a contact.
*
* TODO Move DB lookup to a worker thread.
*/
private void onClickSender() {
final Address senderEmail = Address.unpackFirst(mMessage.mFrom);
if (senderEmail == null) return;
// First perform lookup query to find existing contact
final ContentResolver resolver = mContext.getContentResolver();
final String address = senderEmail.getAddress();
final Uri dataUri = Uri.withAppendedPath(CommonDataKinds.Email.CONTENT_FILTER_URI,
Uri.encode(address));
final Uri lookupUri = ContactsContract.Data.getContactLookupUri(resolver, dataUri);
if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED) {
// Status not loaded yet.
mContactStatusState = CONTACT_STATUS_STATE_UNLOADED_TRIGGERED;
return;
}
if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED) {
return; // Already clicked, and waiting for the data.
}
if (lookupUri != null) {
// Found matching contact, trigger QuickContact
QuickContact.showQuickContact(mContext, mSenderPresenceView, lookupUri,
QuickContact.MODE_LARGE, null);
if (mQuickContactLookupUri != null) {
QuickContact.showQuickContact(mContext, mFromBadge, mQuickContactLookupUri,
QuickContact.MODE_LARGE, null);
} else {
// No matching contact, ask user to create one
final Uri mailUri = Uri.fromParts("mailto", address, null);
final Uri mailUri = Uri.fromParts("mailto", senderEmail.getAddress(), null);
final Intent intent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT,
mailUri);
@ -418,6 +439,44 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
}
}
private static class ContactStatusLoaderCallbacks
implements LoaderCallbacks<ContactStatusLoader.Result> {
private static final String BUNDLE_EMAIL_ADDRESS = "email";
private final MessageViewFragmentBase mFragment;
public ContactStatusLoaderCallbacks(MessageViewFragmentBase fragment) {
mFragment = fragment;
}
public static Bundle createArguments(String emailAddress) {
Bundle b = new Bundle();
b.putString(BUNDLE_EMAIL_ADDRESS, emailAddress);
return b;
}
@Override
public Loader<ContactStatusLoader.Result> onCreateLoader(int id, Bundle args) {
return new ContactStatusLoader(mFragment.mContext,
args.getString(BUNDLE_EMAIL_ADDRESS));
}
@Override
public void onLoadFinished(Loader<ContactStatusLoader.Result> loader,
ContactStatusLoader.Result result) {
boolean triggered =
(mFragment.mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED);
mFragment.mContactStatusState = CONTACT_STATUS_STATE_LOADED;
mFragment.mQuickContactLookupUri = result.mLookupUri;
mFragment.mSenderPresenceView.setImageResource(result.mPresenceResId);
if (result.mPhoto != null) { // photo will be null if unknown.
mFragment.mFromBadge.setImageBitmap(result.mPhoto);
}
if (triggered) {
mFragment.onClickSender();
}
}
}
private void onSaveAttachment(AttachmentInfo info) {
if (!Utility.isExternalStorageMounted()) {
/*
@ -520,6 +579,7 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
}
switch (view.getId()) {
case R.id.from:
case R.id.badge:
case R.id.presence:
onClickSender();
break;
@ -542,29 +602,21 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
}
/**
* Start checking presence of the sender of the message.
*
* Note: This is just a polling operation. A more advanced solution would be to keep the
* cursor open and respond to presence status updates (in the form of content change
* notifications). However, because presence changes fairly slowly compared to the duration
* of viewing a single message, a simple poll at message load (and onResume) should be
* sufficient.
* Start loading contact photo and presence.
*/
private void startPresenceCheck() {
// Set "unknown" presence icon.
mSenderPresenceView.setImageResource(PresenceUpdater.getPresenceIconResourceId(null));
private void queryContactStatus() {
initContactStatusViews(); // Initialize the state, just in case.
// Find the sender email address, and start presence check.
if (mMessage != null) {
Address sender = Address.unpackFirst(mMessage.mFrom);
if (sender != null) {
String email = sender.getAddress();
if (email != null) {
mPresenceUpdater.checkPresence(email, new PresenceUpdater.Callback() {
@Override
public void onPresenceResult(String emailAddress, Integer presenceStatus) {
mSenderPresenceView.setImageResource(
PresenceUpdater.getPresenceIconResourceId(presenceStatus));
}
});
mFromBadge.assignContactFromEmail(email, false);
getLoaderManager().restartLoader(PHOTO_LOADER_ID,
ContactStatusLoaderCallbacks.createArguments(email),
new ContactStatusLoaderCallbacks(this));
}
}
}
@ -625,7 +677,7 @@ public abstract class MessageViewFragmentBase extends Fragment implements View.O
mMessageId = message.mId;
reloadUiFromMessage(message, mOkToFetch);
startPresenceCheck();
queryContactStatus();
mCallback.onMessageViewShown(mMailboxType);
}
}

View File

@ -1,172 +0,0 @@
/*
* Copyright (C) 2010 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.email.activity;
import com.android.email.R;
import com.android.email.Utility;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.StatusUpdates;
import java.util.ArrayList;
/**
* Class to check presence for email addresses and update icons.
*
* In this class, "presence status" is represented by an {@link Integer}, which can take one of:
* {@link StatusUpdates#OFFLINE},
* {@link StatusUpdates#INVISIBLE},
* {@link StatusUpdates#AWAY},
* {@link StatusUpdates#IDLE},
* {@link StatusUpdates#DO_NOT_DISTURB},
* {@link StatusUpdates#AVAILABLE},
* or null for unknown.
*/
public class PresenceUpdater {
/* package */ static final String[] PRESENCE_STATUS_PROJECTION =
new String[] { Contacts.CONTACT_PRESENCE };
private final Context mContext;
/** List of running {@link PresenceCheckTask}. Used in {@link #cancelAll()}. */
private final ArrayList<PresenceCheckTask> mTaskList = new ArrayList<PresenceCheckTask>();
/** Callback called when {@link #checkPresence} is done. */
public interface Callback {
public void onPresenceResult(String emailAddress, Integer presenceStatus);
}
public PresenceUpdater(Context context) {
mContext = context.getApplicationContext();
}
/**
* Start a task to check presence. Call {@code Callback#onPresenceResult} when done.
*
* Must be called on the UI thread, as it creates an AsyncTask.
*/
public void checkPresence(String emailAddress, Callback callback) {
PresenceCheckTask task = new PresenceCheckTask(emailAddress, callback);
task.execute();
synchronized (mTaskList) {
mTaskList.add(task);
}
}
private void removeTaskFromList(PresenceCheckTask task) {
synchronized (mTaskList) {
mTaskList.remove(task);
}
}
/**
* Cancel all running tasks.
*/
public void cancelAll() {
synchronized (mTaskList) {
try {
for (PresenceCheckTask task : mTaskList) {
Utility.cancelTaskInterrupt(task);
}
} finally {
mTaskList.clear();
}
}
}
/**
* @return the resourece ID for the presence icon for {@code presenceStatus}.
*/
/* package */ static int getPresenceIconResourceId(Integer presenceStatus) {
return (presenceStatus == null) ? R.drawable.presence_inactive
: StatusUpdates.getPresenceIconResourceId(presenceStatus);
}
/**
* The actual method to get presence status from the contacts provider.
* Called on a worker thread.
*
* Extracted from {@link PresenceCheckTask} for testing.
*
* @return presence status
*/
/* package */ Integer getPresenceStatus(String emailAddress) {
Cursor cursor = openPresenceCheckCursor(emailAddress);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
return cursor.getInt(0);
}
} finally {
cursor.close();
}
}
return null; // Unknown
}
/**
* Open cursor for presence.
*
* Unit tests override this to inject a mock cursor.
*/
/* package */ Cursor openPresenceCheckCursor(String emailAddress) {
return mContext.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
PRESENCE_STATUS_PROJECTION,
CommonDataKinds.Email.DATA + "=?", new String[] { emailAddress }, null);
}
private class PresenceCheckTask extends AsyncTask<Void, Void, Integer> {
private final String mEmailAddress;
private final Callback mCallback;
public PresenceCheckTask(String emailAddress, Callback callback) {
mEmailAddress = emailAddress;
mCallback = callback;
}
@Override
protected Integer doInBackground(Void... params) {
return getPresenceStatus(mEmailAddress);
}
@Override
protected void onCancelled() {
removeTaskFromList(this);
}
@Override
protected void onPostExecute(Integer status) {
try {
if (isCancelled()) {
return;
}
mCallback.onPresenceResult(mEmailAddress, status);
} finally {
removeTaskFromList(this);
}
}
}
/* package */ int getTaskListSizeForTest() {
return mTaskList.size();
}
}

View File

@ -0,0 +1,203 @@
/*
* Copyright (C) 2010 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.email.activity;
import com.android.email.activity.ContactStatusLoader.Result;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.StatusUpdates;
import android.test.ProviderTestCase2;
import android.test.mock.MockContentProvider;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import junit.framework.Assert;
/**
* Test for {@link ContactStatusLoader}
*
* Unfortunately this doesn't check {@link ContactStatusLoader.Result#mLookupUri}, because we don't
* (shouldn't) know how {@link android.provider.ContactsContract.Data#getContactLookupUri} is
* implemented.
*/
public class ContactStatusLoaderTest
extends ProviderTestCase2<ContactStatusLoaderTest.MockContactProvider> {
private static final String EMAIL = "a@b.c";
private MockContactProvider mProvider;
public ContactStatusLoaderTest() {
super(MockContactProvider.class, ContactsContract.AUTHORITY);
}
@Override
protected void setUp() throws Exception {
super.setUp();
mProvider = getProvider();
}
// Contact doesn't exist
public void testContactNotFound() {
// Insert empty cursor
mProvider.mCursors.offer(
new MatrixCursor(ContactStatusLoader.PROJECTION_PHOTO_ID_PRESENCE));
// Load!
ContactStatusLoader l = new ContactStatusLoader(getMockContext(), EMAIL);
Result r = l.loadInBackground();
// Check input to the provider
assertEquals(1, mProvider.mUris.size());
assertEquals("content://com.android.contacts/data/emails/lookup/a%40b.c",
mProvider.mUris.get(0));
// Check result
assertNull(r.mPhoto);
assertEquals(ContactStatusLoader.PRESENCE_UNKNOWN_RESOURCE_ID, r.mPresenceResId);
}
// Contact doesn't exist -- provider returns null for the first query
public void testNull() {
// No cursor prepared. (Mock provider will return null)
// Load!
ContactStatusLoader l = new ContactStatusLoader(getMockContext(), EMAIL);
Result r = l.loadInBackground();
// Check result
assertNull(r.mPhoto);
assertEquals(ContactStatusLoader.PRESENCE_UNKNOWN_RESOURCE_ID, r.mPresenceResId);
}
// Contact exists, but no photo
public void testNoPhoto() {
// Result for the first query (the one for photo-id)
MatrixCursor cursor1 = new MatrixCursor(ContactStatusLoader.PROJECTION_PHOTO_ID_PRESENCE);
cursor1.addRow(new Object[]{12345, StatusUpdates.AWAY});
mProvider.mCursors.offer(cursor1);
// Empty cursor for the second query
mProvider.mCursors.offer(new MatrixCursor(ContactStatusLoader.PHOTO_PROJECTION));
// Load!
ContactStatusLoader l = new ContactStatusLoader(getMockContext(), EMAIL);
Result r = l.loadInBackground();
// Check input to the provider
// We should have had at least two queries from loadInBackground.
// There can be extra queries from getContactLookupUri(), but this test shouldn't know
// the details, so use ">= 2".
assertTrue(mProvider.mUris.size() >= 2);
assertEquals("content://com.android.contacts/data/emails/lookup/a%40b.c",
mProvider.mUris.get(0));
assertEquals("content://com.android.contacts/data/12345",
mProvider.mUris.get(1));
// Check result
assertNull(r.mPhoto); // no photo
assertEquals(android.R.drawable.presence_away, r.mPresenceResId);
}
// Contact exists, but no photo (provider returns null for the second query)
public void testNull2() {
// Result for the first query (the one for photo-id)
MatrixCursor cursor1 = new MatrixCursor(ContactStatusLoader.PROJECTION_PHOTO_ID_PRESENCE);
cursor1.addRow(new Object[]{12345, StatusUpdates.AWAY});
mProvider.mCursors.offer(cursor1);
// No cursor for the second query
// Load!
ContactStatusLoader l = new ContactStatusLoader(getMockContext(), EMAIL);
Result r = l.loadInBackground();
// Check result
assertNull(r.mPhoto); // no photo
assertEquals(android.R.drawable.presence_away, r.mPresenceResId);
}
// Contact exists, with a photo
public void testWithPhoto() {
// Result for the first query (the one for photo-id)
MatrixCursor cursor1 = new MatrixCursor(ContactStatusLoader.PROJECTION_PHOTO_ID_PRESENCE);
cursor1.addRow(new Object[]{12345, StatusUpdates.AWAY});
mProvider.mCursors.offer(cursor1);
// Prepare for the second query.
MatrixCursor cursor2 = new PhotoCursor(createJpegData(10, 20));
mProvider.mCursors.offer(cursor2);
// Load!
ContactStatusLoader l = new ContactStatusLoader(getMockContext(), EMAIL);
Result r = l.loadInBackground();
// Check result
assertNotNull(r.mPhoto);
assertEquals(10, r.mPhoto.getWidth());
assertEquals(android.R.drawable.presence_away, r.mPresenceResId);
}
private static byte[] createJpegData(int width, int height) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
ByteArrayOutputStream out = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, out);
return out.toByteArray();
}
// MatrixCursor doesn't support getBlob, so use this...
private static class PhotoCursor extends MatrixCursor {
private final byte[] mBlob;
public PhotoCursor(byte[] blob) {
super(ContactStatusLoader.PHOTO_PROJECTION);
mBlob = blob;
addRow(new Object[] {null}); // Add dummy row
}
@Override
public byte[] getBlob(int column) {
Assert.assertEquals(0, column);
return mBlob;
}
}
public static class MockContactProvider extends MockContentProvider {
public ArrayList<String> mUris = new ArrayList<String>();
public final Queue<Cursor> mCursors = new LinkedBlockingQueue<Cursor>();
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
mUris.add(uri.toString());
return mCursors.poll();
}
@Override
public void attachInfo(Context context, ProviderInfo info) {
}
}
}

View File

@ -1,220 +0,0 @@
/*
* Copyright (C) 2010 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.email.activity;
import com.android.email.R;
import com.android.email.TestUtils;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.provider.ContactsContract.StatusUpdates;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.LargeTest;
/**
* Test case for {@link PresenceUpdater}.
*
* We need to use {@link InstrumentationTestCase} so that we can create AsyncTasks on the UI thread
* using {@link InstrumentationTestCase#runTestOnUiThread}.
*/
@LargeTest
public class PresenceUpdaterTest extends InstrumentationTestCase {
/**
* Email address that's (most probably) not in Contacts.
*/
private static final String NON_EXISTENT_EMAIL_ADDRESS = "no.such.email.address@a.a";
/**
* Timeout used for async tests.
*/
private static final int TIMEOUT_SECONDS = 10;
private Context getContext() {
return getInstrumentation().getTargetContext();
}
public void testSetPresenceIcon() {
assertEquals(StatusUpdates.getPresenceIconResourceId(StatusUpdates.AWAY),
PresenceUpdater.getPresenceIconResourceId(StatusUpdates.AWAY));
// Special case: unknown
assertEquals(R.drawable.presence_inactive, PresenceUpdater.getPresenceIconResourceId(null));
}
/** Call {@link PresenceUpdater#checkPresence} on the UI thread. */
private void checkPresenceOnUiThread(final PresenceUpdater pu, final String emailAddress,
final PresenceUpdater.Callback callback) throws Throwable {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
pu.checkPresence(emailAddress, callback);
}
});
}
private static void waitForAllTasksToFinish(String message, final PresenceUpdater pu) {
TestUtils.waitUntil(message, new TestUtils.Condition() {
@Override public boolean isMet() {
return pu.getTaskListSizeForTest() == 0;
}
}, TIMEOUT_SECONDS);
}
/**
* Verify that:
* - {@link PresenceUpdater#checkPresence} starts an AsyncTask.
* - {@link PresenceUpdater#cancelAll} cancels all AsyncTasks.
*
* It uses {@link PresenceUpdaterBlocking} to test cancellation.
*/
public void testQueueTasksAndCancelAll() throws Throwable {
// Use blocking one.
PresenceUpdaterBlocking pu = new PresenceUpdaterBlocking(getContext());
MockCallback callback = new MockCallback();
// Start presence check.
checkPresenceOnUiThread(pu, "dummy@dummy.com", callback);
// There should be 1 task running.
assertEquals(1, pu.getTaskListSizeForTest());
// Start another presence check.
checkPresenceOnUiThread(pu, "dummy2@dummy.com", callback);
// There should be 2 tasks running.
assertEquals(2, pu.getTaskListSizeForTest());
assertFalse(callback.mCalled);
// === Test for cancelAll() ===
// Cancel all tasks. Callback shouldn't get called.
callback.reset();
pu.cancelAll();
waitForAllTasksToFinish("testQueueTaskAndCancelAll", pu);
assertFalse(callback.mCalled);
}
/**
* Verify that
* - {@link PresenceUpdater#checkPresence} calls {@link PresenceUpdater.Callback} within
* timeout.
*
* It uses the actual contacts provider.
*/
public void testUpdateImageUnknownEmailAddress() throws Throwable {
PresenceUpdater pu = new PresenceUpdater(getContext());
MockCallback callback = new MockCallback();
// Start presence check.
checkPresenceOnUiThread(pu, NON_EXISTENT_EMAIL_ADDRESS, callback);
waitForAllTasksToFinish("testUpdateImageUnknownEmailAddress", pu);
// Check status
assertTrue(callback.mCalled);
assertNull(callback.mPresenceStatus);
// There should be no running tasks.
assertEquals(0, pu.getTaskListSizeForTest());
}
/**
* Verify that
* - startUpdate really updates image's resource ID before timeout for *known* email address.
*
* It uses {@link PresenceUpdaterWithMockCursor} to inject a mock cursor with a dummy presence
* information.
*/
public void testUpdateImage() throws Throwable {
PresenceUpdaterWithMockCursor pu = new PresenceUpdaterWithMockCursor(getContext(),
StatusUpdates.AVAILABLE);
MockCallback callback = new MockCallback();
// Start presence check.
checkPresenceOnUiThread(pu, NON_EXISTENT_EMAIL_ADDRESS, callback);
waitForAllTasksToFinish("testUpdateImage", pu);
// Check status
assertTrue(callback.mCalled);
assertEquals((Integer) StatusUpdates.AVAILABLE, callback.mPresenceStatus);
}
private static class MockCallback implements PresenceUpdater.Callback {
public boolean mCalled;
public Integer mPresenceStatus;
public void reset() {
mPresenceStatus = null;
mCalled = false;
}
@Override
public void onPresenceResult(String emailAddress, Integer presenceStatus) {
mPresenceStatus = presenceStatus;
mCalled = true;
}
}
/**
* A subclass of {@link PresenceUpdater} whose async task waits for an Object to be notified.
*/
private static class PresenceUpdaterBlocking extends PresenceUpdater {
public final Object mWaitForObject = new Object();
public PresenceUpdaterBlocking(Context context) {
super(context);
}
@Override Integer getPresenceStatus(String emailAddress) {
synchronized (mWaitForObject) {
try {
mWaitForObject.wait();
} catch (InterruptedException ignore) {
// Canceled
return null;
}
}
return super.getPresenceStatus(emailAddress);
}
}
/**
* A subclass of {@link PresenceUpdater} that injects a MatrixCursor as a mock.
*/
private static class PresenceUpdaterWithMockCursor extends PresenceUpdater {
public final int mPresenceStatus;
public PresenceUpdaterWithMockCursor(Context context, int presenceStatus) {
super(context);
mPresenceStatus = presenceStatus;
}
/**
* Override to inject a mock cursor.
*/
@Override Cursor openPresenceCheckCursor(String emailAddress) {
MatrixCursor c = new MatrixCursor(PresenceUpdater.PRESENCE_STATUS_PROJECTION);
c.addRow(new Object[] {mPresenceStatus});
return c;
}
}
}