Home | History | Annotate | Download | only in adapter
      1 /*
      2  * Copyright (C) 2008-2009 Marc Blank
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.exchange.adapter;
     19 
     20 import android.content.ContentProviderOperation;
     21 import android.content.ContentProviderResult;
     22 import android.content.ContentResolver;
     23 import android.content.ContentUris;
     24 import android.content.Context;
     25 import android.content.OperationApplicationException;
     26 import android.net.Uri;
     27 import android.os.RemoteException;
     28 import android.os.TransactionTooLargeException;
     29 
     30 import com.android.emailcommon.provider.Account;
     31 import com.android.emailcommon.provider.Mailbox;
     32 import com.android.exchange.CommandStatusException;
     33 import com.android.exchange.Eas;
     34 import com.android.mail.utils.LogUtils;
     35 import com.google.common.annotations.VisibleForTesting;
     36 
     37 import java.io.IOException;
     38 import java.io.InputStream;
     39 import java.util.ArrayList;
     40 
     41 /**
     42  * Parent class of all sync adapters (EasMailbox, EasCalendar, and EasContacts)
     43  *
     44  */
     45 public abstract class AbstractSyncAdapter {
     46 
     47     public static final int SECONDS = 1000;
     48     public static final int MINUTES = SECONDS*60;
     49     public static final int HOURS = MINUTES*60;
     50     public static final int DAYS = HOURS*24;
     51     public static final int WEEKS = DAYS*7;
     52 
     53     private static final long SEPARATOR_ID = Long.MAX_VALUE;
     54 
     55     public Mailbox mMailbox;
     56     public Context mContext;
     57     public Account mAccount;
     58     public final ContentResolver mContentResolver;
     59     public final android.accounts.Account mAccountManagerAccount;
     60 
     61     // Create the data for local changes that need to be sent up to the server
     62     public abstract boolean sendLocalChanges(Serializer s) throws IOException;
     63     // Parse incoming data from the EAS server, creating, modifying, and deleting objects as
     64     // required through the EmailProvider
     65     public abstract boolean parse(InputStream is) throws IOException, CommandStatusException;
     66     // The name used to specify the collection type of the target (Email, Calendar, or Contacts)
     67     public abstract String getCollectionName();
     68     public abstract void cleanup();
     69     public abstract boolean isSyncable();
     70     // Add sync options (filter, body type - html vs plain, and truncation)
     71     public abstract void sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)
     72             throws IOException;
     73     /**
     74      * Delete all records of this class in this account
     75      */
     76     public abstract void wipe();
     77 
     78     public boolean isLooping() {
     79         return false;
     80     }
     81 
     82     public AbstractSyncAdapter(final Context context, final Mailbox mailbox,
     83                                final Account account) {
     84         mContext = context;
     85         mMailbox = mailbox;
     86         mAccount = account;
     87         mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress,
     88                 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
     89         mContentResolver = mContext.getContentResolver();
     90     }
     91 
     92     /**
     93      * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts)
     94      * @return the current SyncKey for the Mailbox
     95      * @throws IOException
     96      */
     97     public String getSyncKey() throws IOException {
     98         if (mMailbox.mSyncKey == null) {
     99             LogUtils.d(LogUtils.TAG, "Reset SyncKey to 0");
    100             mMailbox.mSyncKey = "0";
    101         }
    102         return mMailbox.mSyncKey;
    103     }
    104 
    105     public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
    106         mMailbox.mSyncKey = syncKey;
    107     }
    108 
    109     /**
    110      * Operation is our binder-safe ContentProviderOperation (CPO) construct; an Operation can
    111      * be created from a CPO, a CPO Builder, or a CPO Builder with a "back reference" column name
    112      * and offset (that might be used in Builder.withValueBackReference).  The CPO is not actually
    113      * built until it is ready to be executed (with applyBatch); this allows us to recalculate
    114      * back reference offsets if we are required to re-send a large batch in smaller chunks.
    115      *
    116      * NOTE: A failed binder transaction is something of an emergency case, and shouldn't happen
    117      * with any frequency.  When it does, and we are forced to re-send the data to the content
    118      * provider in smaller chunks, we DO lose the sync-window atomicity, and thereby add another
    119      * small risk to the data.  Of course, this is far, far better than dropping the data on the
    120      * floor, as was done before the framework implemented TransactionTooLargeException
    121      */
    122     protected static class Operation {
    123         final ContentProviderOperation mOp;
    124         final ContentProviderOperation.Builder mBuilder;
    125         final String mColumnName;
    126         final int mOffset;
    127         // Is this Operation a separator? (a good place to break up a large transaction)
    128         boolean mSeparator = false;
    129 
    130         // For toString()
    131         final String[] TYPES = new String[] {"???", "Ins", "Upd", "Del", "Assert"};
    132 
    133         Operation(ContentProviderOperation.Builder builder, String columnName, int offset) {
    134             mOp = null;
    135             mBuilder = builder;
    136             mColumnName = columnName;
    137             mOffset = offset;
    138         }
    139 
    140         Operation(ContentProviderOperation.Builder builder) {
    141             mOp = null;
    142             mBuilder = builder;
    143             mColumnName = null;
    144             mOffset = 0;
    145         }
    146 
    147         Operation(ContentProviderOperation op) {
    148             mOp = op;
    149             mBuilder = null;
    150             mColumnName = null;
    151             mOffset = 0;
    152         }
    153 
    154         @Override
    155         public String toString() {
    156             StringBuilder sb = new StringBuilder("Op: ");
    157             ContentProviderOperation op = operationToContentProviderOperation(this, 0);
    158             int type = 0;
    159             //DO NOT SHIP WITH THE FOLLOWING LINE (the API is hidden!)
    160             //type = op.getType();
    161             sb.append(TYPES[type]);
    162             Uri uri = op.getUri();
    163             sb.append(' ');
    164             sb.append(uri.getPath());
    165             if (mColumnName != null) {
    166                 sb.append(" Back value of " + mColumnName + ": " + mOffset);
    167             }
    168             return sb.toString();
    169         }
    170     }
    171 
    172     /**
    173      * We apply the batch of CPO's here.  We synchronize on the service to avoid thread-nasties,
    174      * and we just return quickly if the service has already been stopped.
    175      */
    176     private static ContentProviderResult[] execute(final ContentResolver contentResolver,
    177             final String authority, final ArrayList<ContentProviderOperation> ops)
    178             throws RemoteException, OperationApplicationException {
    179         if (!ops.isEmpty()) {
    180             ContentProviderResult[] result = contentResolver.applyBatch(authority, ops);
    181             return result;
    182         }
    183         return new ContentProviderResult[0];
    184     }
    185 
    186     /**
    187      * Convert an Operation to a CPO; if the Operation has a back reference, apply it with the
    188      * passed-in offset
    189      */
    190     @VisibleForTesting
    191     static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) {
    192         if (op.mOp != null) {
    193             return op.mOp;
    194         } else if (op.mBuilder == null) {
    195             throw new IllegalArgumentException("Operation must have CPO.Builder");
    196         }
    197         ContentProviderOperation.Builder builder = op.mBuilder;
    198         if (op.mColumnName != null) {
    199             builder.withValueBackReference(op.mColumnName, op.mOffset - offset);
    200         }
    201         return builder.build();
    202     }
    203 
    204     /**
    205      * Create a list of CPOs from a list of Operations, and then apply them in a batch
    206      */
    207     private static ContentProviderResult[] applyBatch(final ContentResolver contentResolver,
    208             final String authority, final ArrayList<Operation> ops, final int offset)
    209             throws RemoteException, OperationApplicationException {
    210         // Handle the empty case
    211         if (ops.isEmpty()) {
    212             return new ContentProviderResult[0];
    213         }
    214         ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>();
    215         for (Operation op: ops) {
    216             cpos.add(operationToContentProviderOperation(op, offset));
    217         }
    218         return execute(contentResolver, authority, cpos);
    219     }
    220 
    221     /**
    222      * Apply the list of CPO's in the provider and copy the "mini" result into our full result array
    223      */
    224     private static void applyAndCopyResults(final ContentResolver contentResolver,
    225             final String authority, final ArrayList<Operation> mini,
    226             final ContentProviderResult[] result, final int offset) throws RemoteException {
    227         // Empty lists are ok; we just ignore them
    228         if (mini.isEmpty()) return;
    229         try {
    230             ContentProviderResult[] miniResult = applyBatch(contentResolver, authority, mini,
    231                     offset);
    232             // Copy the results from this mini-batch into our results array
    233             System.arraycopy(miniResult, 0, result, offset, miniResult.length);
    234         } catch (OperationApplicationException e) {
    235             // Not possible since we're building the ops ourselves
    236         }
    237     }
    238 
    239     /**
    240      * Called by a sync adapter to execute a list of Operations in the ContentProvider handling
    241      * the passed-in authority.  If the attempt to apply the batch fails due to a too-large
    242      * binder transaction, we split the Operations as directed by separators.  If any of the
    243      * "mini" batches fails due to a too-large transaction, we're screwed, but this would be
    244      * vanishingly rare.  Other, possibly transient, errors are handled by throwing a
    245      * RemoteException, which the caller will likely re-throw as an IOException so that the sync
    246      * can be attempted again.
    247      *
    248      * Callers MAY leave a dangling separator at the end of the list; note that the separators
    249      * themselves are only markers and are not sent to the provider.
    250      */
    251     protected static ContentProviderResult[] safeExecute(final ContentResolver contentResolver,
    252             final String authority, final ArrayList<Operation> ops) throws RemoteException {
    253         ContentProviderResult[] result = null;
    254         try {
    255             // Try to execute the whole thing
    256             return applyBatch(contentResolver, authority, ops, 0);
    257         } catch (TransactionTooLargeException e) {
    258             // Nope; split into smaller chunks, demarcated by the separator operation
    259             ArrayList<Operation> mini = new ArrayList<Operation>();
    260             // Build a result array with the total size we're sending
    261             result = new ContentProviderResult[ops.size()];
    262             int count = 0;
    263             int offset = 0;
    264             for (Operation op: ops) {
    265                 if (op.mSeparator) {
    266                     try {
    267                         applyAndCopyResults(contentResolver, authority, mini, result, offset);
    268                         mini.clear();
    269                         // Save away the offset here; this will need to be subtracted out of the
    270                         // value originally set by the adapter
    271                         offset = count + 1; // Remember to add 1 for the separator!
    272                     } catch (TransactionTooLargeException e1) {
    273                         throw new RuntimeException("Can't send transaction; sync stopped.");
    274                     } catch (RemoteException e1) {
    275                         throw e1;
    276                     }
    277                 } else {
    278                     mini.add(op);
    279                 }
    280                 count++;
    281             }
    282             // Check out what's left; if it's more than just a separator, apply the batch
    283             int miniSize = mini.size();
    284             if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) {
    285                 applyAndCopyResults(contentResolver, authority, mini, result, offset);
    286             }
    287         } catch (RemoteException e) {
    288             throw e;
    289         } catch (OperationApplicationException e) {
    290             // Not possible since we're building the ops ourselves
    291         }
    292         return result;
    293     }
    294 
    295     /**
    296      * Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's
    297      */
    298     protected static void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) {
    299         Operation op = new Operation(
    300                 ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID)));
    301         op.mSeparator = true;
    302         ops.add(op);
    303     }
    304 }
    305