blob: 47335ca4d21028da1d1b95ef97bfb7a17a21ae7f [file] [log] [blame]
/*
* Copyright (C) 2008-2009 Marc Blank
* Licensed to 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;
import com.android.email.R;
import com.android.email.activity.AccountFolderList;
import com.android.email.mail.AuthenticationFailedException;
import com.android.email.mail.MessagingException;
import com.android.email.provider.EmailContent.Account;
import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.Attachment;
import com.android.email.provider.EmailContent.AttachmentColumns;
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.exchange.adapter.AbstractSyncAdapter;
import com.android.exchange.adapter.ContactsSyncAdapter;
import com.android.exchange.adapter.EmailSyncAdapter;
import com.android.exchange.adapter.FolderSyncParser;
import com.android.exchange.adapter.PingParser;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
import com.android.exchange.adapter.Parser.EasParserException;
import com.android.exchange.utility.Base64;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
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.entity.ByteArrayEntity;
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 android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.RemoteException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
public class EasSyncService extends AbstractSyncService {
private static final String EMAIL_WINDOW_SIZE = "5";
public static final String PIM_WINDOW_SIZE = "20";
private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING =
MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
'=' + Account.CHECK_INTERVAL_PING;
private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
MailboxColumns.SYNC_INTERVAL + " IN (" + Account.CHECK_INTERVAL_PING +
',' + Account.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" +
Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"';
private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX =
MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
'=' + Account.CHECK_INTERVAL_PUSH_HOLD;
static private final int CHUNK_SIZE = 16*1024;
static private final String PING_COMMAND = "Ping";
static private final int COMMAND_TIMEOUT = 20*SECS;
static private final int PING_COMMAND_TIMEOUT = 20*MINS;
// Reasonable default
String mProtocolVersion = "2.5";
public Double mProtocolVersionDouble;
private String mDeviceId = null;
private String mDeviceType = "Android";
AbstractSyncAdapter mTarget;
String mAuthString = null;
String mCmdString = null;
public String mHostAddress;
public String mUserName;
public String mPassword;
String mDomain = null;
boolean mSentCommands;
boolean mIsIdle = false;
boolean mSsl = true;
public Context mContext;
public ContentResolver mContentResolver;
String[] mBindArguments = new String[2];
InputStream mPendingPartInputStream = null;
private boolean mTriedReloadFolderList = false;
public EasSyncService(Context _context, Mailbox _mailbox) {
super(_context, _mailbox);
mContext = _context;
mContentResolver = _context.getContentResolver();
HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
}
private EasSyncService(String prefix) {
super(prefix);
}
public EasSyncService() {
this("EAS Validation");
}
@Override
public void ping() {
userLog("We've been pinged!");
Object synchronizer = getSynchronizer();
synchronized (synchronizer) {
synchronizer.notify();
}
}
@Override
public void stop() {
mStop = true;
}
@Override
public int getSyncStatus() {
return 0;
}
private boolean isAuthError(int code) {
return (code == HttpURLConnection.HTTP_UNAUTHORIZED || code == HttpURLConnection.HTTP_FORBIDDEN
|| code == HttpURLConnection.HTTP_INTERNAL_ERROR);
}
/* (non-Javadoc)
* @see com.android.exchange.SyncService#validateAccount(java.lang.String, java.lang.String, java.lang.String, int, boolean, android.content.Context)
*/
@Override
public void validateAccount(String hostAddress, String userName, String password, int port,
boolean ssl, Context context) throws MessagingException {
try {
userLog("Testing EAS: " + hostAddress + ", " + userName + ", ssl = " + ssl);
EasSyncService svc = new EasSyncService("%TestAccount%");
svc.mContext = context;
svc.mHostAddress = hostAddress;
svc.mUserName = userName;
svc.mPassword = password;
svc.mSsl = ssl;
svc.mDeviceId = SyncManager.getDeviceId();
HttpResponse resp = svc.sendHttpClientOptions();
int code = resp.getStatusLine().getStatusCode();
userLog("Validation (OPTIONS) response: " + code);
if (code == HttpURLConnection.HTTP_OK) {
// No exception means successful validation
userLog("Validation successful");
return;
}
if (isAuthError(code)) {
userLog("Authentication failed");
throw new AuthenticationFailedException("Validation failed");
} else {
// TODO Need to catch other kinds of errors (e.g. policy) For now, report the code.
userLog("Validation failed, reporting I/O error: " + code);
throw new MessagingException(MessagingException.IOERROR);
}
} catch (IOException e) {
userLog("IOException caught, reporting I/O error: " + e.getMessage());
throw new MessagingException(MessagingException.IOERROR);
}
}
private void doStatusCallback(long messageId, long attachmentId, int status) {
try {
SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
} catch (RemoteException e) {
// No danger if the client is no longer around
}
}
private void doProgressCallback(long messageId, long attachmentId, int progress) {
try {
SyncManager.callback().loadAttachmentStatus(messageId, attachmentId,
EmailServiceStatus.IN_PROGRESS, progress);
} catch (RemoteException e) {
// No danger if the client is no longer around
}
}
public File createUniqueFileInternal(String dir, String filename) {
File directory;
if (dir == null) {
directory = mContext.getFilesDir();
} else {
directory = new File(dir);
}
if (!directory.exists()) {
directory.mkdirs();
}
File file = new File(directory, filename);
if (!file.exists()) {
return file;
}
// Get the extension of the file, if any.
int index = filename.lastIndexOf('.');
String name = filename;
String extension = "";
if (index != -1) {
name = filename.substring(0, index);
extension = filename.substring(index);
}
for (int i = 2; i < Integer.MAX_VALUE; i++) {
file = new File(directory, name + '-' + i + extension);
if (!file.exists()) {
return file;
}
}
return null;
}
/**
* Loads an attachment, based on the PartRequest passed in. The PartRequest is basically our
* wrapper for Attachment
* @param req the part (attachment) to be retrieved
* @throws IOException
*/
protected void getAttachment(PartRequest req) throws IOException {
Attachment att = req.att;
Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
doProgressCallback(msg.mId, att.mId, 0);
DefaultHttpClient client = new DefaultHttpClient();
String us = makeUriString("GetAttachment", "&AttachmentName=" + att.mLocation);
HttpPost method = new HttpPost(URI.create(us));
method.setHeader("Authorization", mAuthString);
HttpResponse res = client.execute(method);
int status = res.getStatusLine().getStatusCode();
if (status == HttpURLConnection.HTTP_OK) {
HttpEntity e = res.getEntity();
int len = (int)e.getContentLength();
String type = e.getContentType().getValue();
InputStream is = res.getEntity().getContent();
File f = (req.destination != null)
? new File(req.destination)
: createUniqueFileInternal(req.destination, att.mFileName);
if (f != null) {
// Ensure that the target directory exists
File destDir = f.getParentFile();
if (!destDir.exists()) {
destDir.mkdirs();
}
FileOutputStream os = new FileOutputStream(f);
if (len > 0) {
try {
mPendingPartRequest = req;
mPendingPartInputStream = is;
byte[] bytes = new byte[CHUNK_SIZE];
int length = len;
while (len > 0) {
int n = (len > CHUNK_SIZE ? CHUNK_SIZE : len);
int read = is.read(bytes, 0, n);
os.write(bytes, 0, read);
len -= read;
int pct = ((length - len) * 100 / length);
doProgressCallback(msg.mId, att.mId, pct);
}
} finally {
mPendingPartRequest = null;
mPendingPartInputStream = null;
}
}
os.flush();
os.close();
// EmailProvider will throw an exception if we try to update an unsaved attachment
if (att.isSaved()) {
String contentUriString = (req.contentUriString != null)
? req.contentUriString
: "file://" + f.getAbsolutePath();
ContentValues cv = new ContentValues();
cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
cv.put(AttachmentColumns.MIME_TYPE, type);
att.update(mContext, cv);
doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS);
}
}
} else {
doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
}
}
@SuppressWarnings("deprecation")
private String makeUriString(String cmd, String extra) throws IOException {
// Cache the authentication string and the command string
String safeUserName = URLEncoder.encode(mUserName);
if (mAuthString == null) {
String cs = mUserName + ':' + mPassword;
mAuthString = "Basic " + Base64.encodeBytes(cs.getBytes());
mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType="
+ mDeviceType;
}
String us = (mSsl ? "https" : "http") + "://" + mHostAddress +
"/Microsoft-Server-ActiveSync";
if (cmd != null) {
us += "?Cmd=" + cmd + mCmdString;
}
if (extra != null) {
us += extra;
}
return us;
}
private void setHeaders(HttpRequestBase method) {
method.setHeader("Authorization", mAuthString);
method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
method.setHeader("Connection", "keep-alive");
method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
}
private HttpClient getHttpClient(int timeout) {
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, 10*SECS);
HttpConnectionParams.setSoTimeout(params, timeout);
return new DefaultHttpClient(params);
}
protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
HttpClient client =
getHttpClient(cmd.equals(PING_COMMAND) ? PING_COMMAND_TIMEOUT : COMMAND_TIMEOUT);
String us = makeUriString(cmd, null);
HttpPost method = new HttpPost(URI.create(us));
if (cmd.startsWith("SendMail&")) {
method.setHeader("Content-Type", "message/rfc822");
} else {
method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
}
setHeaders(method);
method.setEntity(new ByteArrayEntity(bytes));
return client.execute(method);
}
protected HttpResponse sendHttpClientOptions() throws IOException {
HttpClient client = getHttpClient(COMMAND_TIMEOUT);
String us = makeUriString("OPTIONS", null);
HttpOptions method = new HttpOptions(URI.create(us));
setHeaders(method);
return client.execute(method);
}
String getTargetCollectionClassFromCursor(Cursor c) {
int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
if (type == Mailbox.TYPE_CONTACTS) {
return "Contacts";
} else if (type == Mailbox.TYPE_CALENDAR) {
return "Calendar";
} else {
return "Email";
}
}
/**
* Performs FolderSync
*
* @throws IOException
* @throws EasParserException
*/
public void runAccountMailbox() throws IOException, EasParserException {
// Initialize exit status to success
mExitStatus = EmailServiceStatus.SUCCESS;
try {
try {
SyncManager.callback()
.syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
} catch (RemoteException e1) {
// Don't care if this fails
}
if (mAccount.mSyncKey == null) {
mAccount.mSyncKey = "0";
userLog("Account syncKey INIT to 0");
ContentValues cv = new ContentValues();
cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
mAccount.update(mContext, cv);
}
boolean firstSync = mAccount.mSyncKey.equals("0");
if (firstSync) {
userLog("Initial FolderSync");
}
// When we first start up, change all ping mailboxes to push.
ContentValues cv = new ContentValues();
cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING,
new String[] {Long.toString(mAccount.mId)}) > 0) {
SyncManager.kick("change ping boxes to push");
}
// Determine our protocol version, if we haven't already
if (mAccount.mProtocolVersion == null) {
userLog("Determine EAS protocol version");
HttpResponse resp = sendHttpClientOptions();
int code = resp.getStatusLine().getStatusCode();
userLog("OPTIONS response: " + code);
if (code == HttpURLConnection.HTTP_OK) {
Header header = resp.getFirstHeader("ms-asprotocolversions");
String versions = header.getValue();
if (versions != null) {
if (versions.contains("12.0")) {
mProtocolVersion = "12.0";
}
mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
mAccount.mProtocolVersion = mProtocolVersion;
userLog(versions);
userLog("Using version " + mProtocolVersion);
} else {
errorLog("No protocol versions in OPTIONS response");
throw new IOException();
}
} else {
errorLog("OPTIONS command failed; throwing IOException");
throw new IOException();
}
}
while (!mStop) {
userLog("Sending Account syncKey: " + mAccount.mSyncKey);
Serializer s = new Serializer();
s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
.text(mAccount.mSyncKey).end().end().done();
HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
if (mStop) break;
int code = resp.getStatusLine().getStatusCode();
if (code == HttpURLConnection.HTTP_OK) {
HttpEntity entity = resp.getEntity();
int len = (int)entity.getContentLength();
if (len > 0) {
InputStream is = entity.getContent();
// Returns true if we need to sync again
userLog("FolderSync, deviceId = " + mDeviceId);
if (new FolderSyncParser(is, this).parse()) {
continue;
}
}
} else if (code == HttpURLConnection.HTTP_UNAUTHORIZED ||
code == HttpURLConnection.HTTP_FORBIDDEN) {
mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
} else {
userLog("FolderSync response error: " + code);
}
// Change all push/hold boxes to push
cv = new ContentValues();
cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
new String[] {Long.toString(mAccount.mId)}) > 0) {
userLog("Set push/hold boxes to push...");
}
try {
SyncManager.callback()
.syncMailboxListStatus(mAccount.mId, mExitStatus, 0);
} catch (RemoteException e1) {
// Don't care if this fails
}
// Wait for push notifications.
String threadName = Thread.currentThread().getName();
try {
runPingLoop();
} catch (StaleFolderListException e) {
// We break out if we get told about a stale folder list
userLog("Ping interrupted; folder list requires sync...");
} finally {
Thread.currentThread().setName(threadName);
}
}
} catch (IOException e) {
// We catch this here to send the folder sync status callback
// A folder sync failed callback will get sent from run()
try {
if (!mStop) {
SyncManager.callback()
.syncMailboxListStatus(mAccount.mId,
EmailServiceStatus.CONNECTION_ERROR, 0);
}
} catch (RemoteException e1) {
// Don't care if this fails
}
throw new IOException();
}
}
void pushFallback() {
// We'll try reloading folders first; this has been observed to work in some cases
if (!mTriedReloadFolderList) {
errorLog("*** PING LOOP: Trying to reload folder list...");
SyncManager.reloadFolderList(mContext, mAccount.mId, true);
mTriedReloadFolderList = true;
// If we've tried that, set all mailboxes (except the account mailbox) to 5 minute sync
} else {
errorLog("*** PING LOOP: Turning off push due to ping loop...");
ContentValues cv = new ContentValues();
cv.put(Mailbox.SYNC_INTERVAL, 5);
mContentResolver.update(Mailbox.CONTENT_URI, cv,
MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId
+ AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null);
// Now, change the account as well
cv.clear();
cv.put(Account.SYNC_INTERVAL, 5);
mContentResolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId),
cv, null, null);
// TODO Discuss the best way to alert the user
// Alert the user about what we've done
NotificationManager nm = (NotificationManager)mContext
.getSystemService(Context.NOTIFICATION_SERVICE);
Notification note =
new Notification(R.drawable.stat_notify_email_generic,
mContext.getString(R.string.notification_ping_loop_title),
System.currentTimeMillis());
Intent i = new Intent(mContext, AccountFolderList.class);
PendingIntent pi = PendingIntent.getActivity(mContext, 0, i, 0);
note.setLatestEventInfo(mContext,
mContext.getString(R.string.notification_ping_loop_title),
mContext.getString(R.string.notification_ping_loop_text), pi);
nm.notify(Eas.EXCHANGE_ERROR_NOTIFICATION, note);
}
}
void runPingLoop() throws IOException, StaleFolderListException {
// Do push for all sync services here
ArrayList<Mailbox> pushBoxes = new ArrayList<Mailbox>();
long endTime = System.currentTimeMillis() + (30*MINS);
HashMap<Long, Integer> pingFailureMap = new HashMap<Long, Integer>();
while (System.currentTimeMillis() < endTime) {
// Count of pushable mailboxes
int pushCount = 0;
// Count of mailboxes that can be pushed right now
int canPushCount = 0;
Serializer s = new Serializer();
int code;
Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
pushBoxes.clear();
try {
// Loop through our pushed boxes seeing what is available to push
while (c.moveToNext()) {
pushCount++;
// Two requirements for push:
// 1) SyncManager tells us the mailbox is syncable (not running, not stopped)
// 2) The syncKey isn't "0" (i.e. it's synced at least once)
long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
if (SyncManager.canSync(mailboxId)) {
String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
if (syncKey == null || syncKey.equals("0")) {
continue;
}
// Take a peek at this box's behavior last sync
// We do this because some Exchange 2003 servers put themselves (and
// therefore our client) into a "ping loop" in which the client is
// continuously told of server changes, only to find that there aren't any.
// This behavior is seemingly random, and we must code defensively by
// backing off of push behavior when this is detected.
// The server fix is at http://support.microsoft.com/kb/923282
// Sync status is encoded as S<type>:<exitstatus>:<changes>
String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
int type = SyncManager.getStatusType(status);
if (type == SyncManager.SYNC_PING) {
int changeCount = SyncManager.getStatusChangeCount(status);
if (changeCount == 0) {
// This means that a ping failed; we'll keep track of this
Integer failures = pingFailureMap.get(mailboxId);
if (failures == null) {
pingFailureMap.put(mailboxId, 1);
} else if (failures > 4) {
// Change all push/ping boxes (except account) to 5 minute sync
pushFallback();
return;
} else {
pingFailureMap.put(mailboxId, failures + 1);
}
} else {
pingFailureMap.put(mailboxId, 0);
}
}
if (canPushCount++ == 0) {
// Initialize the Ping command
s.start(Tags.PING_PING).data(Tags.PING_HEARTBEAT_INTERVAL, "900")
.start(Tags.PING_FOLDERS);
}
// When we're ready for Calendar/Contacts, we will check folder type
// TODO Save Calendar and Contacts!! Mark as not visible!
String folderClass = getTargetCollectionClassFromCursor(c);
s.start(Tags.PING_FOLDER)
.data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
.data(Tags.PING_CLASS, folderClass)
.end();
userLog("Ping ready for: " + folderClass + ", " +
c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN) + " (" +
c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ')');
pushBoxes.add(new Mailbox().restore(c));
} else {
userLog(c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) +
" not ready for ping");
}
}
} finally {
c.close();
}
if (canPushCount > 0 && (canPushCount == pushCount)) {
// If we have some number that are ready for push, send Ping to the server
s.end().end().done();
Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
userLog("Sending ping, timeout: " + PING_COMMAND_TIMEOUT / MINS + "m");
SyncManager.runAsleep(mMailboxId, PING_COMMAND_TIMEOUT);
HttpResponse res = sendHttpClientPost(PING_COMMAND, s.toByteArray());
SyncManager.runAwake(mMailboxId);
// Don't send request if we've been asked to stop
if (mStop) return;
long time = System.currentTimeMillis();
code = res.getStatusLine().getStatusCode();
// Return immediately if we've been asked to stop
if (mStop) {
userLog("Stopping pingLoop");
return;
}
// Get elapsed time
time = System.currentTimeMillis() - time;
userLog("Ping response: " + code + " in " + time + "ms");
if (code == HttpURLConnection.HTTP_OK) {
HttpEntity e = res.getEntity();
int len = (int)e.getContentLength();
InputStream is = res.getEntity().getContent();
if (len > 0) {
parsePingResult(is, mContentResolver);
} else {
throw new IOException();
}
} else if (isAuthError(code)) {
mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
userLog("Authorization error during Ping: " + code);
throw new IOException();
}
} else if (pushCount > 0) {
// If we want to Ping, but can't just yet, wait 10 seconds and try again
userLog("pingLoop waiting for " + (pushCount - canPushCount) + " box(es)");
sleep(10*SECS);
} else {
// We've got nothing to do, so let's hang out for a while
sleep(20*MINS);
}
}
}
void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
// Doesn't matter whether we stop early; it's the thought that counts
}
}
private int parsePingResult(InputStream is, ContentResolver cr)
throws IOException, StaleFolderListException {
PingParser pp = new PingParser(is, this);
if (pp.parse()) {
// True indicates some mailboxes need syncing...
// syncList has the serverId's of the mailboxes...
mBindArguments[0] = Long.toString(mAccount.mId);
ArrayList<String> syncList = pp.getSyncList();
for (String serverId: syncList) {
mBindArguments[1] = serverId;
Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
try {
if (c.moveToFirst()) {
SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
SyncManager.SYNC_PING, null);
}
} finally {
c.close();
}
}
}
return pp.getSyncList().size();
}
ByteArrayInputStream readResponse(HttpURLConnection uc) throws IOException {
String encoding = uc.getHeaderField("Transfer-Encoding");
if (encoding == null) {
int len = uc.getHeaderFieldInt("Content-Length", 0);
if (len > 0) {
InputStream in = uc.getInputStream();
byte[] bytes = new byte[len];
int remain = len;
int offs = 0;
while (remain > 0) {
int read = in.read(bytes, offs, remain);
remain -= read;
offs += read;
}
return new ByteArrayInputStream(bytes);
}
} else if (encoding.equalsIgnoreCase("chunked")) {
// TODO We don't handle this yet
return null;
}
return null;
}
String readResponseString(HttpURLConnection uc) throws IOException {
String encoding = uc.getHeaderField("Transfer-Encoding");
if (encoding == null) {
int len = uc.getHeaderFieldInt("Content-Length", 0);
if (len > 0) {
InputStream in = uc.getInputStream();
byte[] bytes = new byte[len];
int remain = len;
int offs = 0;
while (remain > 0) {
int read = in.read(bytes, offs, remain);
remain -= read;
offs += read;
}
return new String(bytes);
}
} else if (encoding.equalsIgnoreCase("chunked")) {
// TODO We don't handle this yet
return null;
}
return null;
}
private String getFilterType() {
String filter = Eas.FILTER_1_WEEK;
switch (mAccount.mSyncLookback) {
case com.android.email.Account.SYNC_WINDOW_1_DAY: {
filter = Eas.FILTER_1_DAY;
break;
}
case com.android.email.Account.SYNC_WINDOW_3_DAYS: {
filter = Eas.FILTER_3_DAYS;
break;
}
case com.android.email.Account.SYNC_WINDOW_1_WEEK: {
filter = Eas.FILTER_1_WEEK;
break;
}
case com.android.email.Account.SYNC_WINDOW_2_WEEKS: {
filter = Eas.FILTER_2_WEEKS;
break;
}
case com.android.email.Account.SYNC_WINDOW_1_MONTH: {
filter = Eas.FILTER_1_MONTH;
break;
}
case com.android.email.Account.SYNC_WINDOW_ALL: {
filter = Eas.FILTER_ALL;
break;
}
}
return filter;
}
/**
* Common code to sync E+PIM data
*
* @param target, an EasMailbox, EasContacts, or EasCalendar object
*/
public void sync(AbstractSyncAdapter target) throws IOException {
mTarget = target;
Mailbox mailbox = target.mMailbox;
boolean moreAvailable = true;
while (!mStop && moreAvailable) {
waitForConnectivity();
while (true) {
PartRequest req = null;
synchronized (mPartRequests) {
if (mPartRequests.isEmpty()) {
break;
} else {
req = mPartRequests.get(0);
}
}
getAttachment(req);
synchronized(mPartRequests) {
mPartRequests.remove(req);
}
}
Serializer s = new Serializer();
if (mailbox.mSyncKey == null) {
userLog("Mailbox syncKey RESET");
mailbox.mSyncKey = "0";
}
String className = target.getCollectionName();
userLog("Sending " + className + " syncKey: " + mailbox.mSyncKey);
s.start(Tags.SYNC_SYNC)
.start(Tags.SYNC_COLLECTIONS)
.start(Tags.SYNC_COLLECTION)
.data(Tags.SYNC_CLASS, className)
.data(Tags.SYNC_SYNC_KEY, mailbox.mSyncKey)
.data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId)
.tag(Tags.SYNC_DELETES_AS_MOVES);
// EAS doesn't like GetChanges if the syncKey is "0"; not documented
if (!mailbox.mSyncKey.equals("0")) {
s.tag(Tags.SYNC_GET_CHANGES);
}
s.data(Tags.SYNC_WINDOW_SIZE,
className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE);
boolean options = false;
if (!className.equals("Contacts")) {
// Set the lookback appropriately (EAS calls this a "filter")
s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, getFilterType());
// No truncation in this version
//if (mProtocolVersionDouble < 12.0) {
// s.data(Tags.SYNC_TRUNCATION, "7");
//}
options = true;
}
if (mProtocolVersionDouble >= 12.0) {
if (!options) {
options = true;
s.start(Tags.SYNC_OPTIONS);
}
s.start(Tags.BASE_BODY_PREFERENCE)
// HTML for email; plain text for everything else
.data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML
: Eas.BODY_PREFERENCE_TEXT))
// No truncation in this version
//.data(Tags.BASE_TRUNCATION_SIZE, Eas.DEFAULT_BODY_TRUNCATION_SIZE)
.end();
}
if (options) {
s.end();
}
// Send our changes up to the server
target.sendLocalChanges(s, this);
s.end().end().end().done();
HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray());
int code = resp.getStatusLine().getStatusCode();
if (code == HttpURLConnection.HTTP_OK) {
InputStream is = resp.getEntity().getContent();
if (is != null) {
moreAvailable = target.parse(is, this);
target.cleanup(this);
}
} else {
userLog("Sync response error: " + code);
if (isAuthError(code)) {
mExitStatus = AbstractSyncService.EXIT_LOGIN_FAILURE;
}
return;
}
}
}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
mThread = Thread.currentThread();
TAG = mThread.getName();
HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
mHostAddress = ha.mAddress;
mUserName = ha.mLogin;
mPassword = ha.mPassword;
try {
SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0);
} catch (RemoteException e1) {
// Don't care if this fails
}
// Make sure account and mailbox are always the latest from the database
mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
// Whether or not we're the account mailbox
boolean accountMailbox = false;
try {
mDeviceId = SyncManager.getDeviceId();
if (mMailbox == null || mAccount == null) {
return;
} else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
accountMailbox = true;
runAccountMailbox();
} else {
AbstractSyncAdapter target;
mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
mProtocolVersion = mAccount.mProtocolVersion;
mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
if (mMailbox.mType == Mailbox.TYPE_CONTACTS)
target = new ContactsSyncAdapter(mMailbox, this);
else {
target = new EmailSyncAdapter(mMailbox, this);
}
// We loop here because someone might have put a request in while we were syncing
// and we've missed that opportunity...
do {
if (mRequestTime != 0) {
userLog("Looping for user request...");
mRequestTime = 0;
}
sync(target);
} while (mRequestTime != 0);
}
mExitStatus = EXIT_DONE;
} catch (IOException e) {
userLog("Caught IOException");
mExitStatus = EXIT_IO_ERROR;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (!mStop) {
userLog(mMailbox.mDisplayName + ": sync finished");
SyncManager.done(this);
// If this is the account mailbox, wake up SyncManager
// Because this box has a "push" interval, it will be restarted immediately
// which will cause the folder list to be reloaded...
try {
int status;
switch (mExitStatus) {
case EXIT_IO_ERROR:
status = EmailServiceStatus.CONNECTION_ERROR;
break;
case EXIT_DONE:
status = EmailServiceStatus.SUCCESS;
break;
case EXIT_LOGIN_FAILURE:
status = EmailServiceStatus.LOGIN_FAILED;
break;
default:
status = EmailServiceStatus.REMOTE_EXCEPTION;
break;
}
SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
// Save the sync time and status
ContentValues cv = new ContentValues();
cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
cv.put(Mailbox.SYNC_STATUS, s);
mContentResolver.update(ContentUris
.withAppendedId(Mailbox.CONTENT_URI, mMailboxId), cv, null, null);
} catch (RemoteException e1) {
// Don't care if this fails
}
} else {
userLog(mMailbox.mDisplayName + ": stopped thread finished.");
}
// Make sure this gets restarted...
if (accountMailbox) {
SyncManager.kick("account mailbox stopped");
}
}
}
}