GAL support

* Provides GAL autocomplete in email address fields in message composition.

General TODO list:
UI: Implement divider in adapter, not in GAL provider
UI: Use listview_separator for divider
UI: Clean up strings, move all to resources
UI: Only have one GAL lookup in flight at any time
UI: Unit tests

GAL: Use side channel for status, not a row
GAL: Shorten timeout for interactive GAL lookup
GAL: Make watchdogs work
GAL: Figure out why some calls never return (conn pool exhaustion?)
GAL: Unit tests

Bug: 2249514
Change-Id: I513e25628bc2f5ed0920e0ee509cd598b1817b3a
This commit is contained in:
Andrew Stadler 2010-03-13 00:38:47 -08:00
parent 20225d5760
commit e2c56fc88c
11 changed files with 685 additions and 40 deletions

View File

@ -328,5 +328,17 @@
android:multiprocess="true"
android:permission="com.android.email.permission.ACCESS_PROVIDER"
/>
<!--EXCHANGE-REMOVE-SECTION-START-->
<!-- In this release, GAL information is used locally only, so we used the same
strict permissions. -->
<provider
android:name="com.android.exchange.provider.ExchangeProvider"
android:authorities="com.android.exchange.provider"
android:multiprocess="true"
android:permission="com.android.email.permission.ACCESS_PROVIDER"
/>
<!--EXCHANGE-REMOVE-SECTION-END-->
</application>
</manifest>

View File

@ -14,30 +14,69 @@
limitations under the License.
-->
<!-- TODO: Remove separator, return to original simple layout -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dip"
android:orientation="horizontal">
<TextView android:id="@+id/text1"
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_height="wrap_content"
android:orientation="vertical"
android:layout_centerVertical="true">
<RelativeLayout android:id="@+id/address"
android:layout_width="match_parent"
android:layout_height="50dip"
android:orientation="horizontal"
android:layout_centerVertical="true"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
<TextView android:id="@+id/text2"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/text1"
android:layout_gravity="center_vertical">
<TextView android:id="@+id/text1"
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"
/>
<TextView android:id="@+id/text2"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/text1"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
</RelativeLayout>
<RelativeLayout android:id="@+id/status_divider"
android:layout_width="match_parent"
android:layout_height="20dip"
android:orientation="horizontal"
android:layout_centerVertical="true"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
</RelativeLayout>
android:background="#FF777777">
<TextView android:id="@+id/account"
android:textColor="#FFFFFFFF"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
<ProgressBar android:id="@+id/progress"
style="?android:attr/progressBarStyleSmallTitle"
android:minWidth="10dip"
android:paddingRight="4dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
/>
</RelativeLayout>
</RelativeLayout>

View File

@ -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) { }
}

View File

@ -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 {

View File

@ -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<Long, Void, Object[]> {
@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);
}

View File

@ -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);

View File

@ -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();
}
}
}
}

View File

@ -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

View File

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

View File

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

View File

@ -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> galData = new ArrayList<GalData>();
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;
}
}
}