| /* |
| * 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 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 com.android.emailcommon.provider.Account; |
| import com.android.emailcommon.provider.Mailbox; |
| import com.android.exchange.CommandStatusException; |
| import com.android.exchange.Eas; |
| import com.android.mail.utils.LogUtils; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| 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; |
| |
| private static final long SEPARATOR_ID = Long.MAX_VALUE; |
| |
| public Mailbox mMailbox; |
| 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, boolean initialSync) |
| throws IOException; |
| /** |
| * Delete all records of this class in this account |
| */ |
| public abstract void wipe(); |
| |
| public boolean isLooping() { |
| return false; |
| } |
| |
| public AbstractSyncAdapter(final Context context, final Mailbox mailbox, |
| final Account account) { |
| mContext = context; |
| mMailbox = mailbox; |
| mAccount = account; |
| mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress, |
| Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); |
| mContentResolver = mContext.getContentResolver(); |
| } |
| |
| /** |
| * 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) { |
| LogUtils.d(LogUtils.TAG, "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; |
| } |
| |
| @Override |
| 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 static ContentProviderResult[] execute(final ContentResolver contentResolver, |
| final String authority, final ArrayList<ContentProviderOperation> ops) |
| throws RemoteException, OperationApplicationException { |
| if (!ops.isEmpty()) { |
| ContentProviderResult[] result = contentResolver.applyBatch(authority, ops); |
| 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 static ContentProviderResult[] applyBatch(final ContentResolver contentResolver, |
| final String authority, final ArrayList<Operation> ops, final 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(contentResolver, authority, cpos); |
| } |
| |
| /** |
| * Apply the list of CPO's in the provider and copy the "mini" result into our full result array |
| */ |
| private static void applyAndCopyResults(final ContentResolver contentResolver, |
| final String authority, final ArrayList<Operation> mini, |
| final ContentProviderResult[] result, final int offset) throws RemoteException { |
| // Empty lists are ok; we just ignore them |
| if (mini.isEmpty()) return; |
| try { |
| ContentProviderResult[] miniResult = applyBatch(contentResolver, 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 static ContentProviderResult[] safeExecute(final ContentResolver contentResolver, |
| final String authority, final ArrayList<Operation> ops) throws RemoteException { |
| ContentProviderResult[] result = null; |
| try { |
| // Try to execute the whole thing |
| return applyBatch(contentResolver, authority, ops, 0); |
| } catch (TransactionTooLargeException e) { |
| // Nope; split into smaller chunks, demarcated by the separator operation |
| 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 { |
| applyAndCopyResults(contentResolver, 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(contentResolver, 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 static void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) { |
| Operation op = new Operation( |
| ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID))); |
| op.mSeparator = true; |
| ops.add(op); |
| } |
| } |