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