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:
parent
20225d5760
commit
e2c56fc88c
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) { }
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue