Integration with Directory API for autocomplete

The UI changes a bit - there is no separator
between the local contacts and directories.
Will bring the separator back if asked, but
most likely simply as a thick line.

Change-Id: Idfc990deff41b30d63bd8289731694e3d9a00fb6
This commit is contained in:
Dmitri Plotnikov 2010-08-27 14:02:06 -07:00
parent 4772322cc9
commit 9d74207039
6 changed files with 61 additions and 447 deletions

View File

@ -24,7 +24,7 @@ LOCAL_SRC_FILES += \
src/com/android/email/service/IEmailServiceCallback.aidl
# EXCHANGE-REMOVE-SECTION-END
LOCAL_JAVA_STATIC_LIBRARIES := android-common
LOCAL_STATIC_JAVA_LIBRARIES := android-common
LOCAL_PACKAGE_NAME := Email

View File

@ -14,21 +14,20 @@
limitations under the License.
-->
<!-- TODO: proper style/theme based layout -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="20dip"
android:layout_height="50dip"
android:orientation="horizontal"
android:layout_centerVertical="true"
android:background="#FF777777" >
android:layout_gravity="center_vertical">
<TextView android:id="@+id/text1"
android:textColor="#FFFFFFFF"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimaryInverse"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2007 The Android Open Source Project
* 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.
@ -16,76 +16,65 @@
package com.android.email;
import com.android.email.mail.Address;
import com.android.common.contacts.BaseEmailAddressAdapter;
import com.android.email.provider.EmailContent.Account;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ResourceCursorAdapter;
import android.view.ViewGroup;
import android.widget.TextView;
public class EmailAddressAdapter extends ResourceCursorAdapter {
public static final int ID_INDEX = 0;
public static final int NAME_INDEX = 1;
public static final int DATA_INDEX = 2;
/**
* An adaptation of {@link BaseEmailAddressAdapter} for the Email app. The main
* purpose of the class is to bind the generic implementation to the resources
* defined locally: strings and layouts.
*/
public class EmailAddressAdapter extends BaseEmailAddressAdapter {
protected static final String SORT_ORDER =
Contacts.TIMES_CONTACTED + " DESC, " + Contacts.DISPLAY_NAME;
protected final ContentResolver mContentResolver;
protected static final String[] PROJECTION = {
Data._ID, // 0
Contacts.DISPLAY_NAME, // 1
Email.DATA // 2
};
private LayoutInflater mInflater;
public EmailAddressAdapter(Context context) {
super(context, R.layout.recipient_dropdown_item, null);
mContentResolver = context.getContentResolver();
super(context);
mInflater = LayoutInflater.from(context);
}
@Override
public final String convertToString(Cursor cursor) {
String name = cursor.getString(NAME_INDEX);
String address = cursor.getString(DATA_INDEX);
return new Address(address, name).toString();
protected View inflateItemView(ViewGroup parent) {
return mInflater.inflate(R.layout.recipient_dropdown_item, parent, false);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
protected View inflateItemViewLoading(ViewGroup parent) {
return mInflater.inflate(R.layout.recipient_dropdown_item_loading, parent, false);
}
@Override
protected void bindView(View view, String directoryType, String directoryName,
String displayName, String emailAddress) {
TextView text1 = (TextView)view.findViewById(R.id.text1);
TextView text2 = (TextView)view.findViewById(R.id.text2);
text1.setText(displayName);
text2.setText(emailAddress);
}
@Override
protected void bindViewLoading(View view, String directoryType, String directoryName) {
TextView text1 = (TextView)view.findViewById(R.id.text1);
TextView text2 = (TextView)view.findViewById(R.id.text2);
text1.setText(cursor.getString(NAME_INDEX));
text2.setText(cursor.getString(DATA_INDEX));
}
@Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
String filter = constraint == null ? "" : constraint.toString();
Uri uri = Uri.withAppendedPath(Email.CONTENT_FILTER_URI, Uri.encode(filter));
Cursor c = mContentResolver.query(uri, PROJECTION, null, null, SORT_ORDER);
// To prevent expensive execution in the UI thread
// Cursors get lazily executed, so if you don't call anything on the cursor before
// returning it from the background thread you'll have a complied program for the cursor,
// but it won't have been executed to generate the data yet. Often the execution is more
// expensive than the compilation...
if (c != null) {
c.getCount();
}
return c;
String text = getContext().getString(R.string.gal_searching_fmt,
TextUtils.isEmpty(directoryName) ? directoryType : directoryName);
text1.setText(text);
}
/**
* Set the account when known. Not used for generic contacts lookup; Use when
* linking lookup to specific account.
* Set the account when known. Causes the search to prioritize contacts
* from that account.
*/
public void setAccount(Account account) { }
public void setAccount(Account account) {
if (account != null) {
// TODO: figure out how to infer the contacts account type from the email account
super.setAccount(new android.accounts.Account(account.mEmailAddress, "unknown"));
}
}
}

View File

@ -32,7 +32,6 @@ import com.android.email.provider.EmailContent.Body;
import com.android.email.provider.EmailContent.BodyColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.MessageColumns;
import com.android.exchange.provider.GalEmailAddressAdapter;
import android.app.ActionBar;
import android.app.Activity;
@ -380,13 +379,13 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus
mLoadMessageTask = null;
if (mAddressAdapterTo != null) {
mAddressAdapterTo.changeCursor(null);
mAddressAdapterTo.close();
}
if (mAddressAdapterCc != null) {
mAddressAdapterCc.changeCursor(null);
mAddressAdapterCc.close();
}
if (mAddressAdapterBcc != null) {
mAddressAdapterBcc.changeCursor(null);
mAddressAdapterBcc.close();
}
}
@ -558,19 +557,9 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus
*/
@SuppressWarnings("all")
private void setupAddressAdapters() {
/* EXCHANGE-REMOVE-SECTION-START */
if (true) {
mAddressAdapterTo = new GalEmailAddressAdapter(this);
mAddressAdapterCc = new GalEmailAddressAdapter(this);
mAddressAdapterBcc = new GalEmailAddressAdapter(this);
} else {
/* EXCHANGE-REMOVE-SECTION-END */
mAddressAdapterTo = new EmailAddressAdapter(this);
mAddressAdapterCc = new EmailAddressAdapter(this);
mAddressAdapterBcc = new EmailAddressAdapter(this);
/* EXCHANGE-REMOVE-SECTION-START */
}
/* EXCHANGE-REMOVE-SECTION-END */
mAddressAdapterTo = new EmailAddressAdapter(this);
mAddressAdapterCc = new EmailAddressAdapter(this);
mAddressAdapterBcc = new EmailAddressAdapter(this);
}
// TODO: is there any way to unify this with MessageView.LoadMessageTask?

View File

@ -59,6 +59,7 @@ public class ExchangeDirectoryProvider extends ContentProvider {
private static final int GAL_FILTER = GAL_BASE + 1;
private static final int GAL_CONTACT = GAL_BASE + 2;
private static final int GAL_CONTACT_WITH_ID = GAL_BASE + 3;
private static final int GAL_EMAIL_FILTER = GAL_BASE + 4;
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
@ -68,6 +69,7 @@ public class ExchangeDirectoryProvider extends ContentProvider {
sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/entities", GAL_CONTACT);
sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "contacts/lookup/*/#/entities",
GAL_CONTACT_WITH_ID);
sURIMatcher.addURI(EXCHANGE_GAL_AUTHORITY, "data/emails/filter/*", GAL_EMAIL_FILTER);
}
@Override
@ -211,7 +213,8 @@ public class ExchangeDirectoryProvider extends ContentProvider {
return cursor;
}
case GAL_FILTER: {
case GAL_FILTER:
case GAL_EMAIL_FILTER: {
String filter = uri.getLastPathSegment();
// We should have at least two characters before doing a GAL search
if (filter == null || filter.length() < 2) {

View File

@ -1,366 +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.exchange.provider;
import com.android.email.Email;
import com.android.email.EmailAddressAdapter;
import com.android.email.R;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.HostAuth;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.net.Uri;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
/**
* Email Address adapter that performs asynchronous GAL lookups.
*/
public class GalEmailAddressAdapter extends EmailAddressAdapter {
// DO NOT CHECK IN SET TO TRUE
private static final boolean DEBUG_GAL_LOG = false;
// Don't run GAL query until there are 3 characters typed
private static final int MINIMUM_GAL_CONSTRAINT_LENGTH = 3;
private Activity mActivity;
private Account mAccount;
private boolean mAccountHasGal;
private String mAccountEmailDomain;
private LayoutInflater mInflater;
// Local variables to track status of the search
private int mSeparatorDisplayCount;
private int mSeparatorTotalCount;
public GalEmailAddressAdapter(Activity activity) {
super(activity);
mActivity = activity;
mAccount = null;
mAccountHasGal = false;
mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
/**
* Set the account ID when known. Not used for generic contacts lookup; Use when
* linking lookup to specific account.
*/
@Override
public void setAccount(Account account) {
mAccount = account;
mAccountHasGal = false;
int finalSplit = mAccount.mEmailAddress.lastIndexOf('@');
mAccountEmailDomain = mAccount.mEmailAddress.substring(finalSplit + 1);
}
/**
* Sniff the provided account and if it's EAS, record "mAccounthHasGal". If not,
* clear mAccount so we just ignore it.
*/
private void checkGalAccount(Account account) {
HostAuth ha = HostAuth.restoreHostAuthWithId(mActivity, account.mHostAuthKeyRecv);
if (ha != null) {
if ("eas".equalsIgnoreCase(ha.mProtocol)) {
mAccountHasGal = true;
return;
}
}
// for any reason, we could not identify a GAL account, so clear mAccount
// and we'll never check this again
mAccount = null;
mAccountHasGal = false;
}
@Override
public Cursor runQueryOnBackgroundThread(final CharSequence constraint) {
// One time (and not in the UI thread) - check the account and see if it support GAL
// If not, clear it so we never bother again
if (mAccount != null && mAccountHasGal == false) {
checkGalAccount(mAccount);
}
// Get the cursor from ContactsProvider, and set up to exit immediately, returning it
Cursor contactsCursor = super.runQueryOnBackgroundThread(constraint);
// If we don't have a GAL account or we don't have a constraint that's long enough,
// just return the raw contactsCursor
if (!mAccountHasGal || constraint == null) {
return contactsCursor;
}
final String constraintString = constraint.toString().trim();
if (constraintString.length() < MINIMUM_GAL_CONSTRAINT_LENGTH) {
return contactsCursor;
}
// Strategy for handling dynamic GAL lookup.
// 1. Create cursor that we can use now (and update later)
// 2. Return it immediately
// 3. Spawn a thread that will update the cursor when results arrive or search fails
final MatrixCursor matrixCursor = new MatrixCursor(ExchangeProvider.GAL_PROJECTION);
final MyMergeCursor mergedResultCursor =
new MyMergeCursor(new Cursor[] {contactsCursor, matrixCursor});
mergedResultCursor.setSeparatorPosition(contactsCursor.getCount());
mSeparatorDisplayCount = -1;
mSeparatorTotalCount = -1;
new Thread(new Runnable() {
public void run() {
// Uri format is account/constraint
Uri galUri =
ExchangeProvider.GAL_URI.buildUpon()
.appendPath(Long.toString(mAccount.mId))
.appendPath(constraintString).build();
if (DEBUG_GAL_LOG) {
Log.d(Email.LOG_TAG, "Query: " + galUri);
}
// Use ExchangeProvider to get the results of the GAL query
final Cursor galCursor =
mContentResolver.query(galUri, ExchangeProvider.GAL_PROJECTION,
null, null, null);
// There are three result cases to handle here.
// 1. matrixCursor is closed - this means the UI no longer cares about us
// 2. gal cursor is null or empty - remove separator and exit
// 3. gal cursor has results - update separator and add results to matrix cursor
// Case 1: The merged cursor has already been dropped, (e.g. results superceded)
if (mergedResultCursor.isClosed()) {
if (DEBUG_GAL_LOG) {
Log.d(Email.LOG_TAG, "Drop result (cursor closed, bg thread)");
}
return;
}
// Cases 2 & 3 have UI aspects, so do them in the UI thread
mActivity.runOnUiThread(new Runnable() {
public void run() {
// Case 1: (final re-check): Merged cursor already dropped
if (mergedResultCursor.isClosed()) {
if (DEBUG_GAL_LOG) {
Log.d(Email.LOG_TAG, "Drop result (cursor closed, ui thread)");
}
return;
}
// Case 2: Gal cursor is null or empty
if (galCursor == null || galCursor.getCount() == 0) {
if (DEBUG_GAL_LOG) {
Log.d(Email.LOG_TAG, "Drop empty result");
}
mergedResultCursor.setSeparatorPosition(ListView.INVALID_POSITION);
GalEmailAddressAdapter.this.notifyDataSetChanged();
return;
}
// Case 3: Real results
galCursor.moveToPosition(-1);
while (galCursor.moveToNext()) {
MatrixCursor.RowBuilder rb = matrixCursor.newRow();
rb.add(galCursor.getLong(ExchangeProvider.GAL_COLUMN_ID));
rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DISPLAYNAME));
rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DATA));
}
// Replace the separator text with "totals"
mSeparatorDisplayCount = galCursor.getCount();
mSeparatorTotalCount =
galCursor.getExtras().getInt(ExchangeProvider.EXTRAS_TOTAL_RESULTS);
// Notify UI that the cursor changed
if (DEBUG_GAL_LOG) {
Log.d(Email.LOG_TAG, "Notify result, added=" + mSeparatorDisplayCount);
}
GalEmailAddressAdapter.this.notifyDataSetChanged();
}});
}}).start();
return mergedResultCursor;
}
/*
* The following series of overrides insert the separator between contacts & GAL contacts
* TODO: extract most of this into a CursorAdapter superclass, and share with AccountFolderList
*/
/**
* Get the separator position, which is tucked into the cursor to deal with threading.
* Result is invalid for any other cursor types (e.g. the raw contacts cursor)
*/
private int getSeparatorPosition() {
Cursor c = this.getCursor();
if (c instanceof MyMergeCursor) {
return ((MyMergeCursor)c).getSeparatorPosition();
} else {
return ListView.INVALID_POSITION;
}
}
/**
* Prevents the separator view from recycling into the other views
*/
@Override
public int getItemViewType(int position) {
if (position == getSeparatorPosition()) {
return IGNORE_ITEM_VIEW_TYPE;
}
return super.getItemViewType(position);
}
/**
* Injects the separator view when required
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// The base class's getView() checks for mDataValid at the beginning, but we don't have
// to do that, because if the cursor is invalid getCount() returns 0, in which case this
// method wouldn't get called.
// Handle the separator here - create & bind
if (position == getSeparatorPosition()) {
View separator;
separator = mInflater.inflate(R.layout.recipient_dropdown_separator, parent, false);
TextView text1 = (TextView) separator.findViewById(R.id.text1);
View progress = separator.findViewById(R.id.progress);
String bannerText;
if (mSeparatorDisplayCount == -1) {
// Display "Searching <account>..."
bannerText = mContext.getString(R.string.gal_searching_fmt, mAccountEmailDomain);
progress.setVisibility(View.VISIBLE);
} else {
if (mSeparatorDisplayCount == mSeparatorTotalCount) {
// Display "x results from <account>"
bannerText = mContext.getResources().getQuantityString(
R.plurals.gal_completed_fmt, mSeparatorDisplayCount,
mSeparatorDisplayCount, mAccountEmailDomain);
} else {
// Display "First x results from <account>"
bannerText = mContext.getString(R.string.gal_completed_limited_fmt,
mSeparatorDisplayCount, mAccountEmailDomain);
}
progress.setVisibility(View.GONE);
}
text1.setText(bannerText);
return separator;
}
return super.getView(getRealPosition(position), convertView, parent);
}
/**
* Forces navigation to skip over the separator
*/
@Override
public boolean areAllItemsEnabled() {
return false;
}
/**
* Forces navigation to skip over the separator
*/
@Override
public boolean isEnabled(int position) {
return position != getSeparatorPosition();
}
/**
* Adjusts list count to include separator
*/
@Override
public int getCount() {
int count = super.getCount();
if (getSeparatorPosition() != ListView.INVALID_POSITION) {
// Increment for separator, if we have anything to show.
count += 1;
}
return count;
}
/**
* Converts list position to cursor position
*/
private int getRealPosition(int pos) {
int separatorPosition = getSeparatorPosition();
if (separatorPosition == ListView.INVALID_POSITION) {
// No separator, identity map
return pos;
} else if (pos <= separatorPosition) {
// Before or at the separator, identity map
return pos;
} else {
// After the separator, remove 1 from the pos to get the real underlying pos
return pos - 1;
}
}
/**
* Returns the item using external position numbering (no separator)
*/
@Override
public Object getItem(int pos) {
return super.getItem(getRealPosition(pos));
}
/**
* Returns the item id using external position numbering (no separator)
*/
@Override
public long getItemId(int pos) {
if (pos == getSeparatorPosition()) {
return View.NO_ID;
}
return super.getItemId(getRealPosition(pos));
}
/**
* Lightweight override of MergeCursor. Synchronizes "mClosed" / "isClosed()" so we
* can safely check if it has been closed, in the threading jumble of our adapter.
* Also holds the separator position, so it can be tracked with the cursor itself and avoid
* errors when multiple cursors are in flight.
*/
private static class MyMergeCursor extends MergeCursor {
private int mSeparatorPosition;
public MyMergeCursor(Cursor[] cursors) {
super(cursors);
mClosed = false;
mSeparatorPosition = ListView.INVALID_POSITION;
}
@Override
public synchronized void close() {
super.close();
if (DEBUG_GAL_LOG) {
Log.d(Email.LOG_TAG, "Closing MyMergeCursor");
}
}
@Override
public synchronized boolean isClosed() {
return super.isClosed();
}
void setSeparatorPosition(int newPos) {
mSeparatorPosition = newPos;
}
int getSeparatorPosition() {
return mSeparatorPosition;
}
}
}