diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 3a9f2a30f..198c9ddf2 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -328,5 +328,17 @@ android:multiprocess="true" android:permission="com.android.email.permission.ACCESS_PROVIDER" /> + + + + + + diff --git a/res/layout/recipient_dropdown_item.xml b/res/layout/recipient_dropdown_item.xml index 1cb34520c..1fb163c3a 100644 --- a/res/layout/recipient_dropdown_item.xml +++ b/res/layout/recipient_dropdown_item.xml @@ -14,30 +14,69 @@ limitations under the License. --> + - + - + + + + + - + android:background="#FF777777"> + + + + diff --git a/src/com/android/email/EmailAddressAdapter.java b/src/com/android/email/EmailAddressAdapter.java index e4d68707c..ec11717bc 100644 --- a/src/com/android/email/EmailAddressAdapter.java +++ b/src/com/android/email/EmailAddressAdapter.java @@ -17,6 +17,7 @@ package com.android.email; import com.android.email.mail.Address; +import com.android.email.provider.EmailContent.Account; import android.content.ContentResolver; import android.content.Context; @@ -30,15 +31,16 @@ import android.widget.ResourceCursorAdapter; 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; - private static final String SORT_ORDER = + protected static final String SORT_ORDER = Contacts.TIMES_CONTACTED + " DESC, " + Contacts.DISPLAY_NAME; - private ContentResolver mContentResolver; + protected ContentResolver mContentResolver; - private static final String[] PROJECTION = { + protected static final String[] PROJECTION = { Data._ID, // 0 Contacts.DISPLAY_NAME, // 1 Email.DATA // 2 @@ -58,7 +60,7 @@ public class EmailAddressAdapter extends ResourceCursorAdapter { } @Override - public final void bindView(View view, Context context, Cursor cursor) { + public void bindView(View view, Context context, Cursor cursor) { TextView text1 = (TextView)view.findViewById(R.id.text1); TextView text2 = (TextView)view.findViewById(R.id.text2); text1.setText(cursor.getString(NAME_INDEX)); @@ -80,4 +82,10 @@ public class EmailAddressAdapter extends ResourceCursorAdapter { } return c; } + + /** + * Set the account when known. Not used for generic contacts lookup; Use when + * linking lookup to specific account. + */ + public void setAccount(Account account) { } } diff --git a/src/com/android/email/EmailAddressValidator.java b/src/com/android/email/EmailAddressValidator.java index e6aab2f96..eca6faf28 100644 --- a/src/com/android/email/EmailAddressValidator.java +++ b/src/com/android/email/EmailAddressValidator.java @@ -18,8 +18,6 @@ package com.android.email; import com.android.email.mail.Address; -import android.util.Config; -import android.util.Log; import android.widget.AutoCompleteTextView.Validator; public class EmailAddressValidator implements Validator { diff --git a/src/com/android/email/activity/MessageCompose.java b/src/com/android/email/activity/MessageCompose.java index bf1b9db76..2d9a68f21 100644 --- a/src/com/android/email/activity/MessageCompose.java +++ b/src/com/android/email/activity/MessageCompose.java @@ -33,6 +33,7 @@ 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.Activity; import android.content.ActivityNotFoundException; @@ -156,7 +157,9 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus private AsyncTask mSaveMessageTask; private AsyncTask mLoadMessageTask; - private EmailAddressAdapter mAddressAdapter; + private EmailAddressAdapter mAddressAdapterTo; + private EmailAddressAdapter mAddressAdapterCc; + private EmailAddressAdapter mAddressAdapterBcc; private Handler mHandler = new Handler() { @Override @@ -278,6 +281,9 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus mAccount = account; if (account != null) { mRightTitle.setText(account.mDisplayName); + mAddressAdapterTo.setAccount(account); + mAddressAdapterCc.setAccount(account); + mAddressAdapterBcc.setAccount(account); } } @@ -391,9 +397,15 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus mLoadMessageTask = null; // don't cancel mSaveMessageTask, let it do its job to the end. - // Make sure the adapter doesn't leak its cursor - if (mAddressAdapter != null) { - mAddressAdapter.changeCursor(null); + // TODO Make sure the three adapters don't leak their internal cursors + if (mAddressAdapterTo != null) { + mAddressAdapterTo.changeCursor(null); + } + if (mAddressAdapterCc != null) { + mAddressAdapterCc.changeCursor(null); + } + if (mAddressAdapterBcc != null) { + mAddressAdapterBcc.changeCursor(null); } } @@ -538,18 +550,18 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus mQuotedTextDelete.setOnClickListener(this); - mAddressAdapter = new EmailAddressAdapter(this); EmailAddressValidator addressValidator = new EmailAddressValidator(); - mToView.setAdapter(mAddressAdapter); + setupAddressAdapters(); + mToView.setAdapter(mAddressAdapterTo); mToView.setTokenizer(new Rfc822Tokenizer()); mToView.setValidator(addressValidator); - mCcView.setAdapter(mAddressAdapter); + mCcView.setAdapter(mAddressAdapterCc); mCcView.setTokenizer(new Rfc822Tokenizer()); mCcView.setValidator(addressValidator); - mBccView.setAdapter(mAddressAdapter); + mBccView.setAdapter(mAddressAdapterBcc); mBccView.setTokenizer(new Rfc822Tokenizer()); mBccView.setValidator(addressValidator); @@ -561,6 +573,26 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus mMessageContentView.setOnFocusChangeListener(this); } + /** + * Set up address auto-completion adapters. + */ + @SuppressWarnings("all") + private void setupAddressAdapters() { + /* EXCHANGE-REMOVE-SECTION-START */ + if (true) { + mAddressAdapterTo = new GalEmailAddressAdapter(this, mToView); + mAddressAdapterCc = new GalEmailAddressAdapter(this, mCcView); + mAddressAdapterBcc = new GalEmailAddressAdapter(this, mBccView); + } 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 */ + } + // TODO: is there any way to unify this with MessageView.LoadMessageTask? private class LoadMessageTask extends AsyncTask { @Override @@ -817,7 +849,7 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus if (mDraft.mId > 0) { return mDraft.mId; } - // don't save draft if the source message did not load yet + // don't save draft if the source message did not load yet if (!mMessageLoaded) { return -1; } @@ -1248,7 +1280,7 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus Uri uri = (Uri) parcelable; if (uri != null) { Attachment attachment = loadAttachmentInfo(uri); - if (MimeUtility.mimeTypeMatches(attachment.mMimeType, + if (MimeUtility.mimeTypeMatches(attachment.mMimeType, Email.ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES)) { addAttachment(attachment); } diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java index d4c1bbb2e..15b4b684e 100644 --- a/src/com/android/exchange/EasSyncService.java +++ b/src/com/android/exchange/EasSyncService.java @@ -41,12 +41,14 @@ import com.android.exchange.adapter.CalendarSyncAdapter; import com.android.exchange.adapter.ContactsSyncAdapter; import com.android.exchange.adapter.EmailSyncAdapter; import com.android.exchange.adapter.FolderSyncParser; +import com.android.exchange.adapter.GalParser; import com.android.exchange.adapter.MeetingResponseParser; import com.android.exchange.adapter.PingParser; import com.android.exchange.adapter.ProvisionParser; import com.android.exchange.adapter.Serializer; import com.android.exchange.adapter.Tags; import com.android.exchange.adapter.Parser.EasParserException; +import com.android.exchange.provider.GalResult; import com.android.exchange.utility.CalendarUtilities; import org.apache.http.Header; @@ -657,6 +659,56 @@ public class EasSyncService extends AbstractSyncService { } } + /** + * Contact the GAL and obtain a list of matching accounts + * @param context caller's context + * @param accountId the account Id to search + * @param filter the characters entered so far + * @return a result record + * + * TODO: shorter timeout for interactive lookup + * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0) + * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion + */ + static public GalResult searchGal(Context context, long accountId, String filter) + { + Account acct = SyncManager.getAccountList().getById(accountId); + if (acct != null) { + HostAuth ha = HostAuth.restoreHostAuthWithId(context, acct.mHostAuthKeyRecv); + try { + EasSyncService svc = new EasSyncService("%GalLookupk%"); + svc.mContext = context; + svc.mHostAddress = ha.mAddress; + svc.mUserName = ha.mLogin; + svc.mPassword = ha.mPassword; + svc.mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0; + svc.mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0; + svc.mDeviceId = SyncManager.getDeviceId(); + Serializer s = new Serializer(); + s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE); + s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter); + s.start(Tags.SEARCH_OPTIONS); + s.data(Tags.SEARCH_RANGE, "0-19"); + s.end().end().end().done(); + svc.userLog("GAL lookup starting for " + ha.mAddress); + HttpResponse resp = svc.sendHttpClientPost("Search", s.toByteArray()); + int code = resp.getStatusLine().getStatusCode(); + svc.userLog("GAL lookup returned " + code); + if (code == HttpStatus.SC_OK) { + InputStream is = resp.getEntity().getContent(); + GalParser gp = new GalParser(is, svc); + if (gp.parse()) { + svc.userLog("GAL lookup successful for " + ha.mAddress); + return gp.getGalResult(); + } + } + } catch (IOException e) { + // GAL is non-critical; we'll just go on + } + } + return null; + } + private void doStatusCallback(long messageId, long attachmentId, int status) { try { SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0); diff --git a/src/com/android/exchange/adapter/GalParser.java b/src/com/android/exchange/adapter/GalParser.java new file mode 100644 index 000000000..90cba4c8a --- /dev/null +++ b/src/com/android/exchange/adapter/GalParser.java @@ -0,0 +1,106 @@ +/* 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.adapter; + +import com.android.exchange.EasSyncService; +import com.android.exchange.provider.GalResult; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Parse the result of a GAL command. + */ +public class GalParser extends Parser { + private EasSyncService mService; + GalResult mGalResult = new GalResult(); + + public GalParser(InputStream in, EasSyncService service) throws IOException { + super(in); + mService = service; + } + + public GalResult getGalResult() { + return mGalResult; + } + + @Override + public boolean parse() throws IOException { + if (nextTag(START_DOCUMENT) != Tags.SEARCH_SEARCH) { + throw new IOException(); + } + while (nextTag(START_DOCUMENT) != END_DOCUMENT) { + if (tag == Tags.SEARCH_RESPONSE) { + parseResponse(mGalResult); + } else { + skipTag(); + } + } + return mGalResult.total > 0; + } + + public void parseProperties(GalResult galResult) throws IOException { + String displayName = null; + String email = null; + while (nextTag(Tags.SEARCH_STORE) != END) { + if (tag == Tags.GAL_DISPLAY_NAME) { + displayName = getValue(); + } else if (tag == Tags.GAL_EMAIL_ADDRESS) { + email = getValue(); + } else { + skipTag(); + } + } + if (displayName != null && email != null) { + galResult.addGalData(0, displayName, email); + } + } + + public void parseResult(GalResult galResult) throws IOException { + while (nextTag(Tags.SEARCH_STORE) != END) { + if (tag == Tags.SEARCH_PROPERTIES) { + parseProperties(galResult); + } else { + skipTag(); + } + } + } + + public void parseResponse(GalResult galResult) throws IOException { + while (nextTag(Tags.SEARCH_RESPONSE) != END) { + if (tag == Tags.SEARCH_STORE) { + parseStore(galResult); + } else { + skipTag(); + } + } + } + + public void parseStore(GalResult galResult) throws IOException { + while (nextTag(Tags.SEARCH_STORE) != END) { + if (tag == Tags.SEARCH_RESULT) { + parseResult(galResult); + } else if (tag == Tags.SEARCH_RANGE) { + mService.userLog("GAL result range: " + getValue()); + } else if (tag == Tags.SEARCH_TOTAL) { + galResult.total = getValueInt(); + } else { + skipTag(); + } + } + } +} + diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java index c2385d53e..ef709815c 100644 --- a/src/com/android/exchange/adapter/Tags.java +++ b/src/com/android/exchange/adapter/Tags.java @@ -44,6 +44,7 @@ public class Tags { public static final int CONTACTS2 = 0x0C; public static final int PING = 0x0D; public static final int PROVISION = 0x0E; + public static final int SEARCH = 0x0F; public static final int GAL = 0x10; public static final int BASE = 0x11; @@ -87,7 +88,6 @@ public class Tags { public static final int SYNC_LIMIT = SYNC_PAGE + 0x25; public static final int SYNC_PARTIAL = SYNC_PAGE + 0x26; - public static final int GIE_PAGE = GIE << PAGE_SHIFT; public static final int GIE_GET_ITEM_ESTIMATE = GIE_PAGE + 5; public static final int GIE_VERSION = GIE_PAGE + 6; @@ -357,6 +357,46 @@ public class Tags { public static final int PING_CLASS = PING_PAGE + 0xC; public static final int PING_MAX_FOLDERS = PING_PAGE + 0xD; + public static final int SEARCH_PAGE = SEARCH << PAGE_SHIFT; + public static final int SEARCH_SEARCH = SEARCH_PAGE + 5; + public static final int SEARCH_STORES = SEARCH_PAGE + 6; + public static final int SEARCH_STORE = SEARCH_PAGE + 7; + public static final int SEARCH_NAME = SEARCH_PAGE + 8; + public static final int SEARCH_QUERY = SEARCH_PAGE + 9; + public static final int SEARCH_OPTIONS = SEARCH_PAGE + 0xA; + public static final int SEARCH_RANGE = SEARCH_PAGE + 0xB; + public static final int SEARCH_STATUS = SEARCH_PAGE + 0xC; + public static final int SEARCH_RESPONSE = SEARCH_PAGE + 0xD; + public static final int SEARCH_RESULT = SEARCH_PAGE + 0xE; + public static final int SEARCH_PROPERTIES = SEARCH_PAGE + 0xF; + public static final int SEARCH_TOTAL = SEARCH_PAGE + 0x10; + public static final int SEARCH_EQUAL_TO = SEARCH_PAGE + 0x11; + public static final int SEARCH_VALUE = SEARCH_PAGE + 0x12; + public static final int SEARCH_AND = SEARCH_PAGE + 0x13; + public static final int SEARCH_OR = SEARCH_PAGE + 0x14; + public static final int SEARCH_FREE_TEXT = SEARCH_PAGE + 0x15; + public static final int SEARCH_SUBSTRING_OP = SEARCH_PAGE + 0x16; + public static final int SEARCH_DEEP_TRAVERSAL = SEARCH_PAGE + 0x17; + public static final int SEARCH_LONG_ID = SEARCH_PAGE + 0x18; + public static final int SEARCH_REBUILD_RESULTS = SEARCH_PAGE + 0x19; + public static final int SEARCH_LESS_THAN = SEARCH_PAGE + 0x1A; + public static final int SEARCH_GREATER_THAN = SEARCH_PAGE + 0x1B; + public static final int SEARCH_SCHEMA = SEARCH_PAGE + 0x1C; + public static final int SEARCH_SUPPORTED = SEARCH_PAGE + 0x1D; + + public static final int GAL_PAGE = GAL << PAGE_SHIFT; + public static final int GAL_DISPLAY_NAME = GAL_PAGE + 5; + public static final int GAL_PHONE = GAL_PAGE + 6; + public static final int GAL_OFFICE = GAL_PAGE + 7; + public static final int GAL_TITLE = GAL_PAGE + 8; + public static final int GAL_COMPANY = GAL_PAGE + 9; + public static final int GAL_ALIAS = GAL_PAGE + 0xA; + public static final int GAL_FIRST_NAME = GAL_PAGE + 0xB; + public static final int GAL_LAST_NAME = GAL_PAGE + 0xC; + public static final int GAL_HOME_PHONE = GAL_PAGE + 0xD; + public static final int GAL_MOBILE_PHONE = GAL_PAGE + 0xE; + public static final int GAL_EMAIL_ADDRESS = GAL_PAGE + 0xF; + public static final int PROVISION_PAGE = PROVISION << PAGE_SHIFT; // EAS 2.5 public static final int PROVISION_PROVISION = PROVISION_PAGE + 5; @@ -566,6 +606,11 @@ public class Tags { }, { // 0x0F Search + "Search", "Stores", "Store", "Name", "Query", + "Options", "Range", "Status", "Response", "Result", + "Properties", "Total", "EqualTo", "Value", "And", + "Or", "FreeText", "SubstringOp", "DeepTraversal", "LongId", + "RebuildResults", "LessThan", "GreateerThan", "Schema", "Supported" }, { // 0x10 Gal diff --git a/src/com/android/exchange/provider/ExchangeProvider.java b/src/com/android/exchange/provider/ExchangeProvider.java new file mode 100644 index 000000000..936773925 --- /dev/null +++ b/src/com/android/exchange/provider/ExchangeProvider.java @@ -0,0 +1,140 @@ +/* + * 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.exchange.EasSyncService; +import com.android.exchange.provider.GalResult.GalData; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.util.Log; + +/** + * ExchangeProvider provides real-time data from the Exchange server; at the moment, it is used + * solely to provide GAL (Global Address Lookup) service to email address adapters + */ +public class ExchangeProvider extends ContentProvider { + public static final String TAG = "ExchangeProvider"; + + public static final String EXCHANGE_AUTHORITY = "com.android.exchange.provider"; + public static final Uri GAL_URI = Uri.parse("content://" + EXCHANGE_AUTHORITY + "/gal/"); + + public static final long GAL_START_ID = 0x1000000L; + + private static final int GAL_BASE = 0; + private static final int GAL_FILTER = GAL_BASE; + public static final String[] GAL_PROJECTION = new String[] {"_id", "displayName", "data"}; + + private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); + // We use the time stamp to suppress GAL results for queries that have been superceded by newer + // ones (i.e. we only respond to the most recent query) + public static long sQueryTimeStamp = 0; + + static { + // Exchange URI matching table + UriMatcher matcher = sURIMatcher; + // The URI for GAL lookup contains three user-supplied parameters in the path: + // 1) the account id of the Exchange account + // 2) the constraint (filter) text + // 3) a time stamp for the request + matcher.addURI(EXCHANGE_AUTHORITY, "gal/*/*/*", GAL_FILTER); + } + + private static void addGalDataRow(MatrixCursor mc, long id, String name, String address) { + mc.newRow().add(id).add(name).add(address); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + int match = sURIMatcher.match(uri); + switch (match) { + case GAL_FILTER: + long accountId = -1; + // Pull out our parameters + MatrixCursor c = new MatrixCursor(GAL_PROJECTION); + String accountIdString = uri.getPathSegments().get(1); + String filter = uri.getPathSegments().get(2); + String time = uri.getPathSegments().get(3); + // Make sure we get a valid time; otherwise throw an exception + try { + accountId = Long.parseLong(accountIdString); + sQueryTimeStamp = Long.parseLong(time); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Illegal value in URI"); + } + // Get results from the Exchange account + long timeStamp = sQueryTimeStamp; + GalResult galResult = EasSyncService.searchGal(getContext(), accountId, filter); + // If we have a more recent query in process, ignore the result + if (timeStamp != sQueryTimeStamp) { + Log.d(TAG, "Ignoring result from query: " + uri); + return null; + } else if (galResult != null) { + // TODO: None of the UI row should be communicated here- use + // cursor metadata or other method. + int count = galResult.galData.size(); + Log.d(TAG, "Query returned " + count + " result(s)"); + String header = (count == 0) ? "No results" : (count == 1) ? "1 result" : + count + " results"; + if (galResult.total != count) { + header += " (of " + galResult.total + ")"; + } + addGalDataRow(c, GAL_START_ID, header, ""); + int i = 1; + for (GalData data : galResult.galData) { + // TODO Don't get the constant from Email app... + addGalDataRow(c, data._id | GAL_START_ID + i, data.displayName, + data.emailAddress); + i++; + } + } + return c; + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return -1; + } + + @Override + public String getType(Uri uri) { + return "vnd.android.cursor.dir/gal-entry"; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public boolean onCreate() { + return false; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return -1; + } +} diff --git a/src/com/android/exchange/provider/GalEmailAddressAdapter.java b/src/com/android/exchange/provider/GalEmailAddressAdapter.java new file mode 100644 index 000000000..ad8e36963 --- /dev/null +++ b/src/com/android/exchange/provider/GalEmailAddressAdapter.java @@ -0,0 +1,166 @@ +/* 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.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.View; +import android.widget.AutoCompleteTextView; +import android.widget.TextView; + +public class GalEmailAddressAdapter extends EmailAddressAdapter { + private static final String TAG = "GalAdapter"; + // Don't run GAL query until there are 3 characters typed + private static final int MINIMUM_GAL_CONSTRAINT_LENGTH = 3; + // Tag in the placeholder + public static final String SEARCHING_TAG = "_SEARCHING_"; + + Activity mActivity; + AutoCompleteTextView mAutoCompleteTextView; + Account mAccount; + boolean mAccountHasGal; + + public GalEmailAddressAdapter(Activity activity, AutoCompleteTextView actv) { + super(activity); + mActivity = activity; + mAutoCompleteTextView = actv; + mAccount = null; + mAccountHasGal = false; + } + + /** + * 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; + } + + /** + * TODO: This should be the general purpose placeholder + * TODO: String (possibly with account name) + */ + public static Cursor getProgressCursor() { + MatrixCursor c = new MatrixCursor(ExchangeProvider.GAL_PROJECTION); + c.newRow().add(ExchangeProvider.GAL_START_ID).add("Searching ").add(SEARCHING_TAG); + return c; + } + + /** + * 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 + Cursor initialCursor = super.runQueryOnBackgroundThread(constraint); + // If we don't have an account or we don't have a constraint that's long enough, just return + if (mAccountHasGal && + constraint != null && constraint.length() >= MINIMUM_GAL_CONSTRAINT_LENGTH) { + // Note that the "progress" line could (should) be implemented as a header rather than + // as a row in the list. The current implementation is placeholder. + MergeCursor mc = + new MergeCursor(new Cursor[] {initialCursor, getProgressCursor()}); + // We need another copy of the original cursor for our MergeCursor + // because changeCursor closes the original! + // TODO: Avoid this - getting the contacts cursor twice is bad news. + // We're probably not handling the filter UI properly. + final Cursor contactsCursor = super.runQueryOnBackgroundThread(constraint); + new Thread(new Runnable() { + public void run() { + // Uri format is account/constraint/timestamp + Uri galUri = + ExchangeProvider.GAL_URI.buildUpon() + .appendPath(Long.toString(mAccount.mId)) + .appendPath(constraint.toString()) + .appendPath(((Long)System.currentTimeMillis()).toString()).build(); + Log.d(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); + // A null cursor means that our query has been superceded by a later one + if (galCursor == null) return; + // We need to change cursors on the UI thread + mActivity.runOnUiThread(new Runnable() { + public void run() { + // Create a new cursor putting together local and GAL results and + // use it in the adapter + MergeCursor mergeCursor = + new MergeCursor(new Cursor[] {contactsCursor, galCursor}); + changeCursor(mergeCursor); + // Call AutoCompleteTextView's onFilterComplete method with count + mAutoCompleteTextView.onFilterComplete(mergeCursor.getCount()); + }}); + }}).start(); + return mc; + } + // Return right away with the ContactsProvider result + return initialCursor; + } + + // TODO - we cannot assume that contacts ID's will not overlap with GAL_START_ID + // need to do this in a different way, based on more direct knowledge of our cursor + @Override + public final void bindView(View view, Context context, Cursor cursor) { + if (cursor.getLong(ID_INDEX) == ExchangeProvider.GAL_START_ID) { + ((TextView)view.findViewById(R.id.account)).setText(cursor.getString(NAME_INDEX)); + view.findViewById(R.id.status_divider).setVisibility(View.VISIBLE); + view.findViewById(R.id.address).setVisibility(View.GONE); + view.findViewById(R.id.progress).setVisibility( + cursor.getString(DATA_INDEX).equals(SEARCHING_TAG) ? + View.VISIBLE : View.GONE); + } else { + ((TextView)view.findViewById(R.id.text1)).setText(cursor.getString(NAME_INDEX)); + ((TextView)view.findViewById(R.id.text2)).setText(cursor.getString(DATA_INDEX)); + view.findViewById(R.id.address).setVisibility(View.VISIBLE); + view.findViewById(R.id.status_divider).setVisibility(View.GONE); + } + } + + +} diff --git a/src/com/android/exchange/provider/GalResult.java b/src/com/android/exchange/provider/GalResult.java new file mode 100644 index 000000000..3700622bf --- /dev/null +++ b/src/com/android/exchange/provider/GalResult.java @@ -0,0 +1,47 @@ +/* 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 java.util.ArrayList; + +/** + * A container for GAL results from EAS + * Each element of the galData array becomes an element of the list used by autocomplete + */ +public class GalResult { + // Total number of matches in this result + public int total; + public ArrayList galData = new ArrayList(); + + public GalResult() { + } + + public void addGalData(long id, String displayName, String emailAddress) { + galData.add(new GalData(id, displayName, emailAddress)); + } + + public static class GalData { + final long _id; + final String displayName; + final String emailAddress; + + private GalData(long id, String _displayName, String _emailAddress) { + _id = id; + displayName = _displayName; + emailAddress = _emailAddress; + } + } +}