Implement Autodiscover for Exchange servers

* Autodiscover allows complete configuration using only email address
  and password
* Code handles the two standard autodiscover addresses and redirect
* Autodiscover process starts when the user chooses "Exchange" as the
  account type.  If the account is created via the AccountManager,
  autodiscover begins upon tapping "Next" for the first time
* If autodiscover fails due to anything other than auth failure for
  autodiscover-capable servers, the user is placed into the standard manual
  configuration screen

Bug: 2366019
Change-Id: I936712b924833d9a133e8da04e11c3ba45d92f92
This commit is contained in:
Marc Blank 2009-12-18 09:18:55 -08:00
parent a4d32a8fec
commit 17da1767e3
13 changed files with 585 additions and 80 deletions

View File

@ -139,6 +139,9 @@ public class AccountSetupAccountType extends Activity implements OnClickListener
mAccount.setSyncInterval(Account.CHECK_INTERVAL_PUSH);
mAccount.setSyncLookback(1);
AccountSetupExchange.actionIncomingSettings(this, mAccount, mMakeDefault, easFlowMode);
if (easFlowMode) {
finish();
}
}
/**

View File

@ -73,6 +73,9 @@ public class AccountSetupBasics extends Activity
private static final String ACTION_START_AT_MESSAGE_LIST =
"com.android.email.AccountSetupBasics.messageList";
private final static String EXTRA_USERNAME = "com.android.email.AccountSetupBasics.username";
private final static String EXTRA_PASSWORD = "com.android.email.AccountSetupBasics.password";
private final static int DIALOG_NOTE = 1;
private final static int DIALOG_DUPLICATE_ACCOUNT = 2;
@ -99,6 +102,15 @@ public class AccountSetupBasics extends Activity
fromActivity.startActivity(i);
}
public static void actionNewAccountWithCredentials(Activity fromActivity,
String username, String password, boolean easFlow) {
Intent i = new Intent(fromActivity, AccountSetupBasics.class);
i.putExtra(EXTRA_USERNAME, username);
i.putExtra(EXTRA_PASSWORD, password);
i.putExtra(EXTRA_EAS_FLOW, easFlow);
fromActivity.startActivity(i);
}
/**
* This creates an intent that can be used to start a self-contained account creation flow
* for exchange accounts.
@ -147,6 +159,7 @@ public class AccountSetupBasics extends Activity
}
setContentView(R.layout.account_setup_basics);
mEmailView = (EditText)findViewById(R.id.account_email);
mPasswordView = (EditText)findViewById(R.id.account_password);
mDefaultView = (CheckBox)findViewById(R.id.account_default);
@ -185,6 +198,13 @@ public class AccountSetupBasics extends Activity
welcomeView.setText(R.string.accounts_welcome_exchange);
}
if (intent.hasExtra(EXTRA_USERNAME)) {
mEmailView.setText(intent.getStringExtra(EXTRA_USERNAME));
}
if (intent.hasExtra(EXTRA_PASSWORD)) {
mPasswordView.setText(intent.getStringExtra(EXTRA_PASSWORD));
}
if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
mAccount = (EmailContent.Account)savedInstanceState.getParcelable(EXTRA_ACCOUNT);
}
@ -267,34 +287,34 @@ public class AccountSetupBasics extends Activity
if (id == DIALOG_NOTE) {
if (mProvider != null && mProvider.note != null) {
return new AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(android.R.string.dialog_alert_title)
.setMessage(mProvider.note)
.setPositiveButton(
getString(R.string.okay_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finishAutoSetup();
}
})
.setNegativeButton(
getString(R.string.cancel_action),
null)
.create();
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(android.R.string.dialog_alert_title)
.setMessage(mProvider.note)
.setPositiveButton(
getString(R.string.okay_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
finishAutoSetup();
}
})
.setNegativeButton(
getString(R.string.cancel_action),
null)
.create();
}
} else if (id == DIALOG_DUPLICATE_ACCOUNT) {
return new AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.account_duplicate_dlg_title)
.setMessage(getString(R.string.account_duplicate_dlg_message_fmt,
mDuplicateAccountName))
.setPositiveButton(R.string.okay_action,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dismissDialog(DIALOG_DUPLICATE_ACCOUNT);
}
})
.create();
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.account_duplicate_dlg_title)
.setMessage(getString(R.string.account_duplicate_dlg_message_fmt,
mDuplicateAccountName))
.setPositiveButton(R.string.okay_action,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dismissDialog(DIALOG_DUPLICATE_ACCOUNT);
}
})
.create();
}
return null;
}
@ -384,7 +404,7 @@ public class AccountSetupBasics extends Activity
mAccount.setDeletePolicy(EmailContent.Account.DELETE_POLICY_ON_DELETE);
}
mAccount.setSyncInterval(DEFAULT_ACCOUNT_CHECK_INTERVAL);
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, true);
AccountSetupCheckSettings.actionValidateSettings(this, mAccount, true, true);
}
private void onNext() {

View File

@ -23,6 +23,7 @@ import com.android.email.mail.MessagingException;
import com.android.email.mail.Sender;
import com.android.email.mail.Store;
import com.android.email.provider.EmailContent;
import com.android.email.service.EmailServiceProxy;
import android.app.Activity;
import android.app.AlertDialog;
@ -56,6 +57,15 @@ public class AccountSetupCheckSettings extends Activity implements OnClickListen
private static final String EXTRA_ACCOUNT = "account";
private static final String EXTRA_CHECK_INCOMING = "checkIncoming";
private static final String EXTRA_CHECK_OUTGOING = "checkOutgoing";
private static final String EXTRA_AUTO_DISCOVER = "autoDiscover";
private static final String EXTRA_AUTO_DISCOVER_USERNAME = "userName";
private static final String EXTRA_AUTO_DISCOVER_PASSWORD = "password";
public static final int REQUEST_CODE_VALIDATE = 1;
public static final int REQUEST_CODE_AUTO_DISCOVER = 2;
// We'll define a special result code for AutoDiscover auth failures
public static final int RESULT_AUTO_DISCOVER_AUTH_FAILED = Activity.RESULT_FIRST_USER;
private Handler mHandler = new Handler();
private ProgressBar mProgressBar;
@ -64,16 +74,39 @@ public class AccountSetupCheckSettings extends Activity implements OnClickListen
private EmailContent.Account mAccount;
private boolean mCheckIncoming;
private boolean mCheckOutgoing;
private boolean mAutoDiscover;
private boolean mCanceled;
private boolean mDestroyed;
public static void actionCheckSettings(Activity fromActivity, EmailContent.Account account,
public static void actionValidateSettings(Activity fromActivity, EmailContent.Account account,
boolean checkIncoming, boolean checkOutgoing) {
Intent i = new Intent(fromActivity, AccountSetupCheckSettings.class);
i.putExtra(EXTRA_ACCOUNT, account);
i.putExtra(EXTRA_CHECK_INCOMING, checkIncoming);
i.putExtra(EXTRA_CHECK_OUTGOING, checkOutgoing);
fromActivity.startActivityForResult(i, 1);
fromActivity.startActivityForResult(i, REQUEST_CODE_VALIDATE);
}
public static void actionAutoDiscover(Activity fromActivity, EmailContent.Account account,
String userName, String password) {
Intent i = new Intent(fromActivity, AccountSetupCheckSettings.class);
i.putExtra(EXTRA_ACCOUNT, account);
i.putExtra(EXTRA_AUTO_DISCOVER, true);
i.putExtra(EXTRA_AUTO_DISCOVER_USERNAME, userName);
i.putExtra(EXTRA_AUTO_DISCOVER_PASSWORD, password);
fromActivity.startActivityForResult(i, REQUEST_CODE_AUTO_DISCOVER);
}
/**
* We create this simple class so that showErrorDialog can differentiate between a regular
* auth error and an auth error during the autodiscover sequence and respond appropriately
*/
private class AutoDiscoverAuthenticationException extends AuthenticationFailedException {
private static final long serialVersionUID = 1L;
public AutoDiscoverAuthenticationException(String message) {
super(message);
}
}
@Override
@ -94,15 +127,52 @@ public class AccountSetupCheckSettings extends Activity implements OnClickListen
return;
}
mAccount = (EmailContent.Account) getIntent().getParcelableExtra(EXTRA_ACCOUNT);
mCheckIncoming = getIntent().getBooleanExtra(EXTRA_CHECK_INCOMING, false);
mCheckOutgoing = getIntent().getBooleanExtra(EXTRA_CHECK_OUTGOING, false);
final Intent intent = getIntent();
mAccount = (EmailContent.Account)intent.getParcelableExtra(EXTRA_ACCOUNT);
mCheckIncoming = intent.getBooleanExtra(EXTRA_CHECK_INCOMING, false);
mCheckOutgoing = intent.getBooleanExtra(EXTRA_CHECK_OUTGOING, false);
mAutoDiscover = intent.getBooleanExtra(EXTRA_AUTO_DISCOVER, false);
new Thread() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
if (mDestroyed) {
return;
}
if (mCanceled) {
finish();
return;
}
if (mAutoDiscover) {
String userName = intent.getStringExtra(EXTRA_AUTO_DISCOVER_USERNAME);
String password = intent.getStringExtra(EXTRA_AUTO_DISCOVER_PASSWORD);
Store store = Store.getInstance(
mAccount.getStoreUri(AccountSetupCheckSettings.this),
getApplication(), null);
Bundle result = store.autoDiscover(AccountSetupCheckSettings.this,
userName, password);
// Result will be null if there was a remote exception
// Otherwise, we can check the exception code and handle auth failed
// Other errors will be ignored, and the user will be taken to manual
// setup
if (result != null) {
int errorCode =
result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
if (errorCode == MessagingException.AUTHENTICATION_FAILED) {
throw new AutoDiscoverAuthenticationException(null);
} else if (errorCode != MessagingException.NO_ERROR) {
return;
}
// The success case is here
Intent resultIntent = new Intent();
resultIntent.putExtra("HostAuth", result.getParcelable(
EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH));
setResult(RESULT_OK, resultIntent);
finish();
}
}
if (mDestroyed) {
return;
}
@ -135,24 +205,23 @@ public class AccountSetupCheckSettings extends Activity implements OnClickListen
if (mDestroyed) {
return;
}
if (mCanceled) {
finish();
return;
}
setResult(RESULT_OK);
finish();
} catch (final AuthenticationFailedException afe) {
// Could be two separate blocks (one for AutoDiscover) but this way we save
// some code
String message = afe.getMessage();
int id = (message == null)
int id = (message == null)
? R.string.account_setup_failed_dlg_auth_message
: R.string.account_setup_failed_dlg_auth_message_fmt;
showErrorDialog(id, message);
showErrorDialog(afe instanceof AutoDiscoverAuthenticationException,
id, message);
} catch (final CertificateValidationException cve) {
String message = cve.getMessage();
int id = (message == null)
int id = (message == null)
? R.string.account_setup_failed_dlg_certificate_message
: R.string.account_setup_failed_dlg_certificate_message_fmt;
showErrorDialog(id, message);
showErrorDialog(false, id, message);
} catch (final MessagingException me) {
int id;
String message = me.getMessage();
@ -173,12 +242,12 @@ public class AccountSetupCheckSettings extends Activity implements OnClickListen
id = R.string.account_setup_failed_security;
break;
default:
id = (message == null)
id = (message == null)
? R.string.account_setup_failed_dlg_server_message
: R.string.account_setup_failed_dlg_server_message_fmt;
: R.string.account_setup_failed_dlg_server_message_fmt;
break;
}
showErrorDialog(id, message);
showErrorDialog(false, id, message);
}
}
}.start();
@ -202,7 +271,13 @@ public class AccountSetupCheckSettings extends Activity implements OnClickListen
});
}
private void showErrorDialog(final int msgResId, final Object... args) {
/**
* The first argument here indicates whether we return an OK result or a cancelled result
* An OK result is used by Exchange to indicate a failed authentication via AutoDiscover
* In that case, we'll end up returning to the AccountSetupBasic screen
*/
private void showErrorDialog(final boolean autoDiscoverAuthException, final int msgResId,
final Object... args) {
mHandler.post(new Runnable() {
public void run() {
if (mDestroyed) {
@ -218,9 +293,9 @@ public class AccountSetupCheckSettings extends Activity implements OnClickListen
getString(R.string.account_setup_failed_dlg_edit_details_action),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// while debugging connection logic, force a true result
// note, this will save possibly-bad settings
if (DBG_FORCE_RESULT_OK) {
if (autoDiscoverAuthException) {
setResult(RESULT_AUTO_DISCOVER_AUTH_FAILED);
} else if (DBG_FORCE_RESULT_OK) {
setResult(RESULT_OK);
}
finish();

View File

@ -20,6 +20,7 @@ import com.android.email.R;
import com.android.email.Utility;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.HostAuth;
import com.android.email.service.EmailServiceProxy;
import com.android.exchange.SyncManager;
@ -29,6 +30,7 @@ import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.RemoteException;
import android.text.Editable;
import android.text.TextWatcher;
@ -147,10 +149,11 @@ public class AccountSetupExchange extends Activity implements OnClickListener,
mAccount = (EmailContent.Account) savedInstanceState.getParcelable(EXTRA_ACCOUNT);
}
String username = null;
String password = null;
try {
URI uri = new URI(mAccount.getStoreUri(this));
String username = null;
String password = null;
if (uri.getUserInfo() != null) {
String[] userInfoParts = uri.getUserInfo().split(":", 2);
username = userInfoParts[0];
@ -195,6 +198,13 @@ public class AccountSetupExchange extends Activity implements OnClickListener,
}
validateFields();
// If we've got a username and password and we're NOT editing, try autodiscover
if (username != null && password != null &&
!Intent.ACTION_EDIT.equals(getIntent().getAction())) {
AccountSetupCheckSettings
.actionAutoDiscover(this, mAccount, mAccount.mEmailAddress, password);
}
}
@Override
@ -266,36 +276,63 @@ public class AccountSetupExchange extends Activity implements OnClickListener,
Utility.setCompoundDrawablesAlpha(mNextButton, enabled ? 255 : 128);
}
private void doOptions() {
boolean easFlowMode = getIntent().getBooleanExtra(EXTRA_EAS_FLOW, false);
AccountSetupOptions.actionOptions(this, mAccount, mMakeDefault, easFlowMode);
finish();
}
/**
* We can get here two ways, either by validate returning or by autodiscover returning.
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
if (Intent.ACTION_EDIT.equals(getIntent().getAction())) {
if (mAccount.isSaved()) {
// Account.update will NOT save the HostAuth's
mAccount.update(this, mAccount.toContentValues());
mAccount.mHostAuthRecv.update(this, mAccount.mHostAuthRecv.toContentValues());
mAccount.mHostAuthSend.update(this, mAccount.mHostAuthSend.toContentValues());
if (mAccount.mHostAuthRecv.mProtocol.equals("eas")) {
// For EAS, notify SyncManager that the password has changed
try {
new EmailServiceProxy(this, SyncManager.class)
.hostChanged(mAccount.mId);
} catch (RemoteException e) {
// Nothing to be done if this fails
if (requestCode == AccountSetupCheckSettings.REQUEST_CODE_VALIDATE) {
if (resultCode == RESULT_OK) {
if (Intent.ACTION_EDIT.equals(getIntent().getAction())) {
if (mAccount.isSaved()) {
// Account.update will NOT save the HostAuth's
mAccount.update(this, mAccount.toContentValues());
mAccount.mHostAuthRecv.update(this,
mAccount.mHostAuthRecv.toContentValues());
mAccount.mHostAuthSend.update(this,
mAccount.mHostAuthSend.toContentValues());
if (mAccount.mHostAuthRecv.mProtocol.equals("eas")) {
// For EAS, notify SyncManager that the password has changed
try {
new EmailServiceProxy(this, SyncManager.class)
.hostChanged(mAccount.mId);
} catch (RemoteException e) {
// Nothing to be done if this fails
}
}
} else {
// Account.save will save the HostAuth's
mAccount.save(this);
}
finish();
} else {
// Account.save will save the HostAuth's
mAccount.save(this);
// Go directly to end - there is no 2nd screen for incoming settings
doOptions();
}
finish();
} else {
// Go directly to end - there is no 2nd screen for incoming settings
boolean easFlowMode = getIntent().getBooleanExtra(EXTRA_EAS_FLOW, false);
AccountSetupOptions.actionOptions(this, mAccount, mMakeDefault, easFlowMode);
}
} else if (requestCode == AccountSetupCheckSettings.REQUEST_CODE_AUTO_DISCOVER) {
// The idea here is that it only matters if we've gotten a HostAuth back from the
// autodiscover service call. In all other cases, we can ignore the result
if (data != null) {
Parcelable p = data.getParcelableExtra("HostAuth");
if (p != null) {
HostAuth hostAuth = (HostAuth)p;
mAccount.mHostAuthSend = hostAuth;
mAccount.mHostAuthRecv = hostAuth;
doOptions();
}
// If we've got an auth failed, we need to go back to the basic screen
// Otherwise, we just continue on with the Exchange setup screen
} else if (resultCode == AccountSetupCheckSettings.RESULT_AUTO_DISCOVER_AUTH_FAILED) {
finish();
}
}
}
}
/**
@ -357,7 +394,7 @@ public class AccountSetupExchange extends Activity implements OnClickListener,
throw new Error(use);
}
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, false);
AccountSetupCheckSettings.actionValidateSettings(this, mAccount, true, false);
}
public void onClick(View v) {

View File

@ -412,7 +412,7 @@ public class AccountSetupIncoming extends Activity implements OnClickListener {
}
mAccount.setDeletePolicy((Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value);
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, false);
AccountSetupCheckSettings.actionValidateSettings(this, mAccount, true, false);
}
public void onClick(View v) {

View File

@ -297,7 +297,7 @@ public class AccountSetupOutgoing extends Activity implements OnClickListener,
*/
throw new Error(use);
}
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, false, true);
AccountSetupCheckSettings.actionValidateSettings(this, mAccount, false, true);
}
public void onClick(View v) {

View File

@ -23,6 +23,7 @@ import org.xmlpull.v1.XmlPullParserException;
import android.content.Context;
import android.content.res.XmlResourceParser;
import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
@ -284,4 +285,17 @@ public abstract class Store {
*/
public String getPersistentString(String key, String defaultValue);
}
/**
* Handle discovery of account settings using only the user's email address and password
* @param context the context of the caller
* @param emailAddress the email address of the exchange user
* @param password the password of the exchange user
* @return a Bundle containing an error code and a HostAuth (if successful)
* @throws MessagingException
*/
public Bundle autoDiscover(Context context, String emailAddress, String password)
throws MessagingException {
return null;
}
}

View File

@ -236,5 +236,20 @@ public class ExchangeStore extends Store {
}
}
}
/**
* We handle AutoDiscover for Exchange 2007 (and later) here, wrapping the EmailService call.
* The service call returns a HostAuth and we return null if there was a service issue
*/
@Override
public Bundle autoDiscover(Context context, String username, String password)
throws MessagingException {
try {
return new EmailServiceProxy(context, SyncManager.class)
.autoDiscover(username, password);
} catch (RemoteException e) {
return null;
}
}
}

View File

@ -15,7 +15,7 @@
* limitations under the License.
*/
package com.android.exchange;
package com.android.email.provider;
parcelable EmailContent.Attachment;
parcelable EmailContent.HostAuth;

View File

@ -24,6 +24,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Debug;
import android.os.IBinder;
import android.os.RemoteException;
@ -48,6 +49,9 @@ public class EmailServiceProxy implements IEmailService {
private static final boolean DEBUG_PROXY = false; // DO NOT CHECK THIS IN SET TO TRUE
private static final String TAG = "EmailServiceProxy";
public static final String AUTO_DISCOVER_BUNDLE_ERROR_CODE = "autodiscover_error_code";
public static final String AUTO_DISCOVER_BUNDLE_HOST_AUTH = "autodiscover_host_auth";
private Context mContext;
private Class<?> mClass;
private IEmailServiceCallback mCallback;
@ -209,6 +213,27 @@ public class EmailServiceProxy implements IEmailService {
}
}
public Bundle autoDiscover(final String userName, final String password)
throws RemoteException {
setTask(new Runnable () {
public void run() {
try {
if (mCallback != null) mService.setCallback(mCallback);
mReturn = mService.autoDiscover(userName, password);
} catch (RemoteException e) {
}
}
});
waitForCompletion();
if (mReturn == null) {
return null;
} else {
Bundle bundle = (Bundle) mReturn;
Log.v(TAG, "autoDiscover returns " + bundle.getInt(AUTO_DISCOVER_BUNDLE_ERROR_CODE));
return bundle;
}
}
public void updateFolderList(final long accountId) throws RemoteException {
setTask(new Runnable () {
public void run() {

View File

@ -28,6 +28,7 @@ import com.android.email.provider.EmailContent.HostAuth;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.service.EmailServiceProxy;
import com.android.exchange.adapter.AbstractSyncAdapter;
import com.android.exchange.adapter.AccountSyncAdapter;
import com.android.exchange.adapter.ContactsSyncAdapter;
@ -42,25 +43,35 @@ import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import android.util.Xml;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -94,6 +105,11 @@ public class EasSyncService extends AbstractSyncService {
// Define our default protocol version as 2.5 (Exchange 2003)
static private final String DEFAULT_PROTOCOL_VERSION = "2.5";
static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
"http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
static private final int AUTO_DISCOVER_REDIRECT_CODE = 451;
/**
* We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time. There's
* no point having a timeout shorter than 5 minutes, I think; at that point, we can just let
@ -200,7 +216,7 @@ public class EasSyncService extends AbstractSyncService {
* @return whether or not the code represents an authentication error
*/
protected boolean isAuthError(int code) {
return ((code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN));
return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
}
@Override
@ -261,6 +277,300 @@ public class EasSyncService extends AbstractSyncService {
}
/**
* Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
* it can be reused
*
* @param resp the HttpResponse that indicates a redirect (451)
* @param post the HttpPost that was originally sent to the server
* @return the HttpPost, updated with the redirect location
*/
private HttpPost getRedirect(HttpResponse resp, HttpPost post) {
Header locHeader = resp.getFirstHeader("X-MS-Location");
if (locHeader != null) {
String loc = locHeader.getValue();
// If we've gotten one and it shows signs of looking like an address, we try
// sending our request there
if (loc != null && loc.startsWith("http")) {
post.setURI(URI.create(loc));
return post;
}
}
return null;
}
/**
* Send the POST command to the autodiscover server, handling a redirect, if necessary, and
* return the HttpResponse
*
* @param client the HttpClient to be used for the request
* @param post the HttpPost we're going to send
* @return an HttpResponse from the original or redirect server
* @throws IOException on any IOException within the HttpClient code
* @throws MessagingException
*/
private HttpResponse postAutodiscover(HttpClient client, HttpPost post)
throws IOException, MessagingException {
userLog("Posting autodiscover to: " + post.getURI());
HttpResponse resp = client.execute(post);
int code = resp.getStatusLine().getStatusCode();
// On a redirect, try the new location
if (code == AUTO_DISCOVER_REDIRECT_CODE) {
post = getRedirect(resp, post);
if (post != null) {
userLog("Posting autodiscover to redirect: " + post.getURI());
return client.execute(post);
}
} else if (isAuthError(code)) {
throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
} else if (code != HttpStatus.SC_OK) {
// We'll try the next address if this doesn't work
userLog("Code: " + code + ", throwing IOException");
throw new IOException();
}
return resp;
}
/**
* Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
* only an email address and the password
*
* @param userName the user's email address
* @param password the user's password
* @return a HostAuth ready to be saved in an Account or null (failure)
*/
public Bundle tryAutodiscover(String userName, String password) throws RemoteException {
XmlSerializer s = Xml.newSerializer();
ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
HostAuth hostAuth = new HostAuth();
Bundle bundle = new Bundle();
bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
MessagingException.NO_ERROR);
try {
// Build the XML document that's sent to the autodiscover server(s)
s.setOutput(os, "UTF-8");
s.startDocument("UTF-8", false);
s.startTag(null, "Autodiscover");
s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
s.startTag(null, "Request");
s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress");
s.startTag(null, "AcceptableResponseSchema");
s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
s.endTag(null, "AcceptableResponseSchema");
s.endTag(null, "Request");
s.endTag(null, "Autodiscover");
s.endDocument();
String req = os.toString();
// Initialize the user name and password
mUserName = userName;
mPassword = password;
// Make sure the authentication string is created (mAuthString)
makeUriString("foo", null);
// Split out the domain name
int amp = userName.indexOf('@');
// The UI ensures that userName is a valid email address
if (amp < 0) {
throw new RemoteException();
}
String domain = userName.substring(amp + 1);
// There are up to four attempts here; the two URLs that we're supposed to try per the
// specification, and up to one redirect for each (handled in postAutodiscover)
// Try the domain first and see if we can get a response
HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
setHeaders(post);
post.setHeader("Content-Type", "text/xml");
post.setEntity(new StringEntity(req));
HttpClient client = getHttpClient(COMMAND_TIMEOUT);
HttpResponse resp;
try {
resp = postAutodiscover(client, post);
} catch (ClientProtocolException e1) {
return null;
} catch (IOException e1) {
// We catch the IOException here because we have an alternate address to try
post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
// If we fail here, we're out of options, so we let the outer try catch the
// IOException and return null
resp = postAutodiscover(client, post);
}
// Get the "final" code; if it's not 200, just return null
int code = resp.getStatusLine().getStatusCode();
userLog("Code: " + code);
if (code != HttpStatus.SC_OK) return null;
// At this point, we have a 200 response (SC_OK)
HttpEntity e = resp.getEntity();
InputStream is = e.getContent();
try {
// The response to Autodiscover is regular XML (not WBXML)
// If we ever get an error in this process, we'll just punt and return null
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
parser.setInput(is, "UTF-8");
int type = parser.getEventType();
if (type == XmlPullParser.START_DOCUMENT) {
type = parser.next();
if (type == XmlPullParser.START_TAG) {
String name = parser.getName();
if (name.equals("Autodiscover")) {
hostAuth = new HostAuth();
parseAutodiscover(parser, hostAuth);
// On success, we'll have a server address and login
if (hostAuth.mAddress != null && hostAuth.mLogin != null) {
// Fill in the rest of the HostAuth
hostAuth.mPassword = password;
hostAuth.mPort = 443;
hostAuth.mProtocol = "eas";
hostAuth.mFlags =
HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
bundle.putParcelable(
EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
} else {
bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
MessagingException.UNSPECIFIED_EXCEPTION);
}
}
}
}
} catch (XmlPullParserException e1) {
// This would indicate an I/O error of some sort
// We will simply return null and user can configure manually
}
// There's no reason at all for exceptions to be thrown, and it's ok if so.
// We just won't do auto-discover; user can configure manually
} catch (IllegalArgumentException e) {
bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
MessagingException.UNSPECIFIED_EXCEPTION);
} catch (IllegalStateException e) {
bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
MessagingException.UNSPECIFIED_EXCEPTION);
} catch (IOException e) {
userLog("IOException in Autodiscover", e);
bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
MessagingException.IOERROR);
} catch (MessagingException e) {
bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
MessagingException.AUTHENTICATION_FAILED);
}
return bundle;
}
void parseServer(XmlPullParser parser, HostAuth hostAuth)
throws XmlPullParserException, IOException {
boolean mobileSync = false;
while (true) {
int type = parser.next();
if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
break;
} else if (type == XmlPullParser.START_TAG) {
String name = parser.getName();
if (name.equals("Type")) {
if (parser.nextText().equals("MobileSync")) {
mobileSync = true;
}
} else if (mobileSync && name.equals("Url")) {
String url = parser.nextText().toLowerCase();
// This will look like https://<server address>/Microsoft-Server-ActiveSync
// We need to extract the <server address>
if (url.startsWith("https://") &&
url.endsWith("/microsoft-server-activesync")) {
int lastSlash = url.lastIndexOf('/');
hostAuth.mAddress = url.substring(8, lastSlash);
userLog("Autodiscover, server: " + hostAuth.mAddress);
}
}
}
}
}
void parseSettings(XmlPullParser parser, HostAuth hostAuth)
throws XmlPullParserException, IOException {
while (true) {
int type = parser.next();
if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
break;
} else if (type == XmlPullParser.START_TAG) {
String name = parser.getName();
if (name.equals("Server")) {
parseServer(parser, hostAuth);
}
}
}
}
void parseAction(XmlPullParser parser, HostAuth hostAuth)
throws XmlPullParserException, IOException {
while (true) {
int type = parser.next();
if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
break;
} else if (type == XmlPullParser.START_TAG) {
String name = parser.getName();
if (name.equals("Error")) {
// Should parse the error
} else if (name.equals("Redirect")) {
Log.d(TAG, "Redirect: " + parser.nextText());
} else if (name.equals("Settings")) {
parseSettings(parser, hostAuth);
}
}
}
}
void parseUser(XmlPullParser parser, HostAuth hostAuth)
throws XmlPullParserException, IOException {
while (true) {
int type = parser.next();
if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
break;
} else if (type == XmlPullParser.START_TAG) {
String name = parser.getName();
if (name.equals("EMailAddress")) {
String addr = parser.nextText();
hostAuth.mLogin = addr;
userLog("Autodiscover, login: " + addr);
} else if (name.equals("DisplayName")) {
String dn = parser.nextText();
userLog("Autodiscover, user: " + dn);
}
}
}
}
void parseResponse(XmlPullParser parser, HostAuth hostAuth)
throws XmlPullParserException, IOException {
while (true) {
int type = parser.next();
if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
break;
} else if (type == XmlPullParser.START_TAG) {
String name = parser.getName();
if (name.equals("User")) {
parseUser(parser, hostAuth);
} else if (name.equals("Action")) {
parseAction(parser, hostAuth);
}
}
}
}
void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
throws XmlPullParserException, IOException {
while (true) {
int type = parser.nextTag();
if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
break;
} else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
parseResponse(parser, hostAuth);
}
}
}
private void doStatusCallback(long messageId, long attachmentId, int status) {
try {
SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
@ -372,7 +682,7 @@ public class EasSyncService extends AbstractSyncService {
errorLog("totalRead is greater than attachment length?");
break;
}
int pct = (totalRead * 100 / length);
int pct = (totalRead * 100) / length;
doProgressCallback(msg.mId, att.mId, pct);
}
}
@ -721,7 +1031,6 @@ public class EasSyncService extends AbstractSyncService {
int pingStatus = SyncManager.pingStatus(mailboxId);
String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
if (pingStatus == SyncManager.PING_STATUS_OK) {
String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
if ((syncKey == null) || syncKey.equals("0")) {
// We can't push until the initial sync is done
@ -1156,7 +1465,7 @@ public class EasSyncService extends AbstractSyncService {
}
} catch (IOException e) {
String message = e.getMessage();
userLog("Caught IOException: ", ((message == null) ? "No message" : message));
userLog("Caught IOException: ", (message == null) ? "No message" : message);
mExitStatus = EXIT_IO_ERROR;
} catch (Exception e) {
userLog("Uncaught exception in EasSyncService", e);

View File

@ -17,7 +17,8 @@
package com.android.exchange;
import com.android.exchange.IEmailServiceCallback;
import com.android.exchange.EmailContent;
import com.android.email.provider.EmailContent;
import android.os.Bundle;
interface IEmailService {
int validate(in String protocol, in String host, in String userName, in String password,
@ -40,4 +41,6 @@ interface IEmailService {
void setLogging(int on);
void hostChanged(long accountId);
Bundle autoDiscover(String userName, String password);
}

View File

@ -277,6 +277,10 @@ public class SyncManager extends Service implements Runnable {
}
}
public Bundle autoDiscover(String userName, String password) throws RemoteException {
return new EasSyncService().tryAutodiscover(userName, password);
}
public void startSync(long mailboxId) throws RemoteException {
checkSyncManagerServiceRunning();
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);