blob: 52400c40a123e660fbf814cf4ca4e122ac3fbcda [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.adapter;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.Mailbox;
import com.android.exchange.CommandStatusException;
import com.android.exchange.Eas;
import com.android.exchange.EasSyncService;
import com.google.common.annotations.VisibleForTesting;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.os.TransactionTooLargeException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
/**
* Parent class of all sync adapters (EasMailbox, EasCalendar, and EasContacts)
*
*/
public abstract class AbstractSyncAdapter {
public static final int SECONDS = 1000;
public static final int MINUTES = SECONDS*60;
public static final int HOURS = MINUTES*60;
public static final int DAYS = HOURS*24;
public static final int WEEKS = DAYS*7;
protected static final String PIM_WINDOW_SIZE = "4";
private static final long SEPARATOR_ID = Long.MAX_VALUE;
public Mailbox mMailbox;
public EasSyncService mService;
public Context mContext;
public Account mAccount;
public final ContentResolver mContentResolver;
public final android.accounts.Account mAccountManagerAccount;
// Create the data for local changes that need to be sent up to the server
public abstract boolean sendLocalChanges(Serializer s) throws IOException;
// Parse incoming data from the EAS server, creating, modifying, and deleting objects as
// required through the EmailProvider
public abstract boolean parse(InputStream is) throws IOException, CommandStatusException;
// The name used to specify the collection type of the target (Email, Calendar, or Contacts)
public abstract String getCollectionName();
public abstract void cleanup();
public abstract boolean isSyncable();
// Add sync options (filter, body type - html vs plain, and truncation)
public abstract void sendSyncOptions(Double protocolVersion, Serializer s) throws IOException;
/**
* Delete all records of this class in this account
*/
public abstract void wipe();
public boolean isLooping() {
return false;
}
public AbstractSyncAdapter(EasSyncService service) {
mService = service;
mMailbox = service.mMailbox;
mContext = service.mContext;
mAccount = service.mAccount;
mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress,
Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
mContentResolver = mContext.getContentResolver();
}
public void userLog(String ...strings) {
mService.userLog(strings);
}
public void incrementChangeCount() {
mService.mChangeCount++;
}
/**
* Set sync options common to PIM's (contacts and calendar)
* @param protocolVersion the protocol version under which we're syncing
* @param the filter to use (or null)
* @param s the Serializer
* @throws IOException
*/
protected void setPimSyncOptions(Double protocolVersion, String filter, Serializer s)
throws IOException {
s.tag(Tags.SYNC_DELETES_AS_MOVES);
s.tag(Tags.SYNC_GET_CHANGES);
s.data(Tags.SYNC_WINDOW_SIZE, PIM_WINDOW_SIZE);
s.start(Tags.SYNC_OPTIONS);
// Set the filter (lookback), if provided
if (filter != null) {
s.data(Tags.SYNC_FILTER_TYPE, filter);
}
// Set the truncation amount and body type
if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
s.start(Tags.BASE_BODY_PREFERENCE);
// Plain text
s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT);
s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
s.end();
} else {
s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
}
s.end();
}
/**
* Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts)
* @return the current SyncKey for the Mailbox
* @throws IOException
*/
public String getSyncKey() throws IOException {
if (mMailbox.mSyncKey == null) {
userLog("Reset SyncKey to 0");
mMailbox.mSyncKey = "0";
}
return mMailbox.mSyncKey;
}
public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
mMailbox.mSyncKey = syncKey;
}
/**
* Operation is our binder-safe ContentProviderOperation (CPO) construct; an Operation can
* be created from a CPO, a CPO Builder, or a CPO Builder with a "back reference" column name
* and offset (that might be used in Builder.withValueBackReference). The CPO is not actually
* built until it is ready to be executed (with applyBatch); this allows us to recalculate
* back reference offsets if we are required to re-send a large batch in smaller chunks.
*
* NOTE: A failed binder transaction is something of an emergency case, and shouldn't happen
* with any frequency. When it does, and we are forced to re-send the data to the content
* provider in smaller chunks, we DO lose the sync-window atomicity, and thereby add another
* small risk to the data. Of course, this is far, far better than dropping the data on the
* floor, as was done before the framework implemented TransactionTooLargeException
*/
protected static class Operation {
final ContentProviderOperation mOp;
final ContentProviderOperation.Builder mBuilder;
final String mColumnName;
final int mOffset;
// Is this Operation a separator? (a good place to break up a large transaction)
boolean mSeparator = false;
// For toString()
final String[] TYPES = new String[] {"???", "Ins", "Upd", "Del", "Assert"};
Operation(ContentProviderOperation.Builder builder, String columnName, int offset) {
mOp = null;
mBuilder = builder;
mColumnName = columnName;
mOffset = offset;
}
Operation(ContentProviderOperation.Builder builder) {
mOp = null;
mBuilder = builder;
mColumnName = null;
mOffset = 0;
}
Operation(ContentProviderOperation op) {
mOp = op;
mBuilder = null;
mColumnName = null;
mOffset = 0;
}
public String toString() {
StringBuilder sb = new StringBuilder("Op: ");
ContentProviderOperation op = operationToContentProviderOperation(this, 0);
int type = 0;
//DO NOT SHIP WITH THE FOLLOWING LINE (the API is hidden!)
//type = op.getType();
sb.append(TYPES[type]);
Uri uri = op.getUri();
sb.append(' ');
sb.append(uri.getPath());
if (mColumnName != null) {
sb.append(" Back value of " + mColumnName + ": " + mOffset);
}
return sb.toString();
}
}
/**
* We apply the batch of CPO's here. We synchronize on the service to avoid thread-nasties,
* and we just return quickly if the service has already been stopped.
*/
private ContentProviderResult[] execute(String authority,
ArrayList<ContentProviderOperation> ops)
throws RemoteException, OperationApplicationException {
synchronized (mService.getSynchronizer()) {
if (!mService.isStopped()) {
if (!ops.isEmpty()) {
ContentProviderResult[] result = mContentResolver.applyBatch(authority, ops);
mService.userLog("Results: " + result.length);
return result;
}
}
}
return new ContentProviderResult[0];
}
/**
* Convert an Operation to a CPO; if the Operation has a back reference, apply it with the
* passed-in offset
*/
@VisibleForTesting
static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) {
if (op.mOp != null) {
return op.mOp;
} else if (op.mBuilder == null) {
throw new IllegalArgumentException("Operation must have CPO.Builder");
}
ContentProviderOperation.Builder builder = op.mBuilder;
if (op.mColumnName != null) {
builder.withValueBackReference(op.mColumnName, op.mOffset - offset);
}
return builder.build();
}
/**
* Create a list of CPOs from a list of Operations, and then apply them in a batch
*/
private ContentProviderResult[] applyBatch(String authority, ArrayList<Operation> ops,
int offset) throws RemoteException, OperationApplicationException {
// Handle the empty case
if (ops.isEmpty()) {
return new ContentProviderResult[0];
}
ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>();
for (Operation op: ops) {
cpos.add(operationToContentProviderOperation(op, offset));
}
return execute(authority, cpos);
}
/**
* Apply the list of CPO's in the provider and copy the "mini" result into our full result array
*/
private void applyAndCopyResults(String authority, ArrayList<Operation> mini,
ContentProviderResult[] result, int offset) throws RemoteException {
// Empty lists are ok; we just ignore them
if (mini.isEmpty()) return;
try {
ContentProviderResult[] miniResult = applyBatch(authority, mini, offset);
// Copy the results from this mini-batch into our results array
System.arraycopy(miniResult, 0, result, offset, miniResult.length);
} catch (OperationApplicationException e) {
// Not possible since we're building the ops ourselves
}
}
/**
* Called by a sync adapter to execute a list of Operations in the ContentProvider handling
* the passed-in authority. If the attempt to apply the batch fails due to a too-large
* binder transaction, we split the Operations as directed by separators. If any of the
* "mini" batches fails due to a too-large transaction, we're screwed, but this would be
* vanishingly rare. Other, possibly transient, errors are handled by throwing a
* RemoteException, which the caller will likely re-throw as an IOException so that the sync
* can be attempted again.
*
* Callers MAY leave a dangling separator at the end of the list; note that the separators
* themselves are only markers and are not sent to the provider.
*/
protected ContentProviderResult[] safeExecute(String authority, ArrayList<Operation> ops)
throws RemoteException {
mService.userLog("Try to execute ", ops.size(), " CPO's for " + authority);
ContentProviderResult[] result = null;
try {
// Try to execute the whole thing
return applyBatch(authority, ops, 0);
} catch (TransactionTooLargeException e) {
// Nope; split into smaller chunks, demarcated by the separator operation
mService.userLog("Transaction too large; spliting!");
ArrayList<Operation> mini = new ArrayList<Operation>();
// Build a result array with the total size we're sending
result = new ContentProviderResult[ops.size()];
int count = 0;
int offset = 0;
for (Operation op: ops) {
if (op.mSeparator) {
try {
mService.userLog("Try mini-batch of ", mini.size(), " CPO's");
applyAndCopyResults(authority, mini, result, offset);
mini.clear();
// Save away the offset here; this will need to be subtracted out of the
// value originally set by the adapter
offset = count + 1; // Remember to add 1 for the separator!
} catch (TransactionTooLargeException e1) {
throw new RuntimeException("Can't send transaction; sync stopped.");
} catch (RemoteException e1) {
throw e1;
}
} else {
mini.add(op);
}
count++;
}
// Check out what's left; if it's more than just a separator, apply the batch
int miniSize = mini.size();
if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) {
applyAndCopyResults(authority, mini, result, offset);
}
} catch (RemoteException e) {
throw e;
} catch (OperationApplicationException e) {
// Not possible since we're building the ops ourselves
}
return result;
}
/**
* Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's
*/
protected void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) {
Operation op = new Operation(
ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID)));
op.mSeparator = true;
ops.add(op);
}
}