diff --git a/Android.mk b/Android.mk index 4e8239b56..da7fc5c81 100644 --- a/Android.mk +++ b/Android.mk @@ -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 diff --git a/res/layout/recipient_dropdown_separator.xml b/res/layout/recipient_dropdown_item_loading.xml similarity index 84% rename from res/layout/recipient_dropdown_separator.xml rename to res/layout/recipient_dropdown_item_loading.xml index 7235bbc3c..eca65af5e 100644 --- a/res/layout/recipient_dropdown_separator.xml +++ b/res/layout/recipient_dropdown_item_loading.xml @@ -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. --> - - + android:layout_gravity="center_vertical"> - \ No newline at end of file + diff --git a/src/com/android/email/EmailAddressAdapter.java b/src/com/android/email/EmailAddressAdapter.java index 047a95f79..93e49c76f 100644 --- a/src/com/android/email/EmailAddressAdapter.java +++ b/src/com/android/email/EmailAddressAdapter.java @@ -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")); + } + } } diff --git a/src/com/android/email/activity/MessageCompose.java b/src/com/android/email/activity/MessageCompose.java index 7245e3f47..9de1c06c1 100644 --- a/src/com/android/email/activity/MessageCompose.java +++ b/src/com/android/email/activity/MessageCompose.java @@ -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? diff --git a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java index 1afe6b173..f4c5d0397 100644 --- a/src/com/android/exchange/provider/ExchangeDirectoryProvider.java +++ b/src/com/android/exchange/provider/ExchangeDirectoryProvider.java @@ -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) { diff --git a/src/com/android/exchange/provider/GalEmailAddressAdapter.java b/src/com/android/exchange/provider/GalEmailAddressAdapter.java deleted file mode 100644 index e0e3f4fe1..000000000 --- a/src/com/android/exchange/provider/GalEmailAddressAdapter.java +++ /dev/null @@ -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 ..." - bannerText = mContext.getString(R.string.gal_searching_fmt, mAccountEmailDomain); - progress.setVisibility(View.VISIBLE); - } else { - if (mSeparatorDisplayCount == mSeparatorTotalCount) { - // Display "x results from " - bannerText = mContext.getResources().getQuantityString( - R.plurals.gal_completed_fmt, mSeparatorDisplayCount, - mSeparatorDisplayCount, mAccountEmailDomain); - } else { - // Display "First x results from " - 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; - } - } -}