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

@ -4,9 +4,9 @@
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.
@ -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"
@ -42,4 +41,4 @@
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
/>
</RelativeLayout>
</RelativeLayout>

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;
}
}
}