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.exchange.EasSyncService;
     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 EasSyncService mService;
     57     public Context mContext;
     58     public Account mAccount;
     59     public final ContentResolver mContentResolver;
     60     public final android.accounts.Account mAccountManagerAccount;
     61 
     62     // Create the data for local changes that need to be sent up to the server
     63     public abstract boolean sendLocalChanges(Serializer s) throws IOException;
     64     // Parse incoming data from the EAS server, creating, modifying, and deleting objects as
     65     // required through the EmailProvider
     66     public abstract boolean parse(InputStream is) throws IOException, CommandStatusException;
     67     // The name used to specify the collection type of the target (Email, Calendar, or Contacts)
     68     public abstract String getCollectionName();
     69     public abstract void cleanup();
     70     public abstract boolean isSyncable();
     71     // Add sync options (filter, body type - html vs plain, and truncation)
     72     public abstract void sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)
     73             throws IOException;
     74     /**
     75      * Delete all records of this class in this account
     76      */
     77     public abstract void wipe();
     78 
     79     public boolean isLooping() {
     80         return false;
     81     }
     82 
     83     public AbstractSyncAdapter(EasSyncService service) {
     84         mService = service;
     85         mMailbox = service.mMailbox;
     86         mContext = service.mContext;
     87         mAccount = service.mAccount;
     88         mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress,
     89                 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE);
     90         mContentResolver = mContext.getContentResolver();
     91     }
     92 
     93     public void userLog(String ...strings) {
     94         mService.userLog(strings);
     95     }
     96 
     97     /**
     98      * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts)
     99      * @return the current SyncKey for the Mailbox
    100      * @throws IOException
    101      */
    102     public String getSyncKey() throws IOException {
    103         if (mMailbox.mSyncKey == null) {
    104             userLog("Reset SyncKey to 0");
    105             mMailbox.mSyncKey = "0";
    106         }
    107         return mMailbox.mSyncKey;
    108     }
    109 
    110     public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
    111         mMailbox.mSyncKey = syncKey;
    112     }
    113 
    114     /**
    115      * Operation is our binder-safe ContentProviderOperation (CPO) construct; an Operation can
    116      * be created from a CPO, a CPO Builder, or a CPO Builder with a "back reference" column name
    117      * and offset (that might be used in Builder.withValueBackReference).  The CPO is not actually
    118      * built until it is ready to be executed (with applyBatch); this allows us to recalculate
    119      * back reference offsets if we are required to re-send a large batch in smaller chunks.
    120      *
    121      * NOTE: A failed binder transaction is something of an emergency case, and shouldn't happen
    122      * with any frequency.  When it does, and we are forced to re-send the data to the content
    123      * provider in smaller chunks, we DO lose the sync-window atomicity, and thereby add another
    124      * small risk to the data.  Of course, this is far, far better than dropping the data on the
    125      * floor, as was done before the framework implemented TransactionTooLargeException
    126      */
    127     protected static class Operation {
    128         final ContentProviderOperation mOp;
    129         final ContentProviderOperation.Builder mBuilder;
    130         final String mColumnName;
    131         final int mOffset;
    132         // Is this Operation a separator? (a good place to break up a large transaction)
    133         boolean mSeparator = false;
    134 
    135         // For toString()
    136         final String[] TYPES = new String[] {"???", "Ins", "Upd", "Del", "Assert"};
    137 
    138         Operation(ContentProviderOperation.Builder builder, String columnName, int offset) {
    139             mOp = null;
    140             mBuilder = builder;
    141             mColumnName = columnName;
    142             mOffset = offset;
    143         }
    144 
    145         Operation(ContentProviderOperation.Builder builder) {
    146             mOp = null;
    147             mBuilder = builder;
    148             mColumnName = null;
    149             mOffset = 0;
    150         }
    151 
    152         Operation(ContentProviderOperation op) {
    153             mOp = op;
    154             mBuilder = null;
    155             mColumnName = null;
    156             mOffset = 0;
    157         }
    158 
    159         @Override
    160         public String toString() {
    161             StringBuilder sb = new StringBuilder("Op: ");
    162             ContentProviderOperation op = operationToContentProviderOperation(this, 0);
    163             int type = 0;
    164             //DO NOT SHIP WITH THE FOLLOWING LINE (the API is hidden!)
    165             //type = op.getType();
    166             sb.append(TYPES[type]);
    167             Uri uri = op.getUri();
    168             sb.append(' ');
    169             sb.append(uri.getPath());
    170             if (mColumnName != null) {
    171                 sb.append(" Back value of " + mColumnName + ": " + mOffset);
    172             }
    173             return sb.toString();
    174         }
    175     }
    176 
    177     /**
    178      * We apply the batch of CPO's here.  We synchronize on the service to avoid thread-nasties,
    179      * and we just return quickly if the service has already been stopped.
    180      */
    181     private static ContentProviderResult[] execute(final ContentResolver contentResolver,
    182             final String authority, final ArrayList<ContentProviderOperation> ops)
    183             throws RemoteException, OperationApplicationException {
    184         if (!ops.isEmpty()) {
    185             ContentProviderResult[] result = contentResolver.applyBatch(authority, ops);
    186             //mService.userLog("Results: " + result.length);
    187             return result;
    188         }
    189         return new ContentProviderResult[0];
    190     }
    191 
    192     /**
    193      * Convert an Operation to a CPO; if the Operation has a back reference, apply it with the
    194      * passed-in offset
    195      */
    196     @VisibleForTesting
    197     static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) {
    198         if (op.mOp != null) {
    199             return op.mOp;
    200         } else if (op.mBuilder == null) {
    201             throw new IllegalArgumentException("Operation must have CPO.Builder");
    202         }
    203         ContentProviderOperation.Builder builder = op.mBuilder;
    204         if (op.mColumnName != null) {
    205             builder.withValueBackReference(op.mColumnName, op.mOffset - offset);
    206         }
    207         return builder.build();
    208     }
    209 
    210     /**
    211      * Create a list of CPOs from a list of Operations, and then apply them in a batch
    212      */
    213     private static ContentProviderResult[] applyBatch(final ContentResolver contentResolver,
    214             final String authority, final ArrayList<Operation> ops, final int offset)
    215             throws RemoteException, OperationApplicationException {
    216         // Handle the empty case
    217         if (ops.isEmpty()) {
    218             return new ContentProviderResult[0];
    219         }
    220         ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>();
    221         for (Operation op: ops) {
    222             cpos.add(operationToContentProviderOperation(op, offset));
    223         }
    224         return execute(contentResolver, authority, cpos);
    225     }
    226 
    227     /**
    228      * Apply the list of CPO's in the provider and copy the "mini" result into our full result array
    229      */
    230     private static void applyAndCopyResults(final ContentResolver contentResolver,
    231             final String authority, final ArrayList<Operation> mini,
    232             final ContentProviderResult[] result, final int offset) throws RemoteException {
    233         // Empty lists are ok; we just ignore them
    234         if (mini.isEmpty()) return;
    235         try {
    236             ContentProviderResult[] miniResult = applyBatch(contentResolver, authority, mini,
    237                     offset);
    238             // Copy the results from this mini-batch into our results array
    239             System.arraycopy(miniResult, 0, result, offset, miniResult.length);
    240         } catch (OperationApplicationException e) {
    241             // Not possible since we're building the ops ourselves
    242         }
    243     }
    244 
    245     /**
    246      * Called by a sync adapter to execute a list of Operations in the ContentProvider handling
    247      * the passed-in authority.  If the attempt to apply the batch fails due to a too-large
    248      * binder transaction, we split the Operations as directed by separators.  If any of the
    249      * "mini" batches fails due to a too-large transaction, we're screwed, but this would be
    250      * vanishingly rare.  Other, possibly transient, errors are handled by throwing a
    251      * RemoteException, which the caller will likely re-throw as an IOException so that the sync
    252      * can be attempted again.
    253      *
    254      * Callers MAY leave a dangling separator at the end of the list; note that the separators
    255      * themselves are only markers and are not sent to the provider.
    256      */
    257     protected static ContentProviderResult[] safeExecute(final ContentResolver contentResolver,
    258             final String authority, final ArrayList<Operation> ops) throws RemoteException {
    259         //mService.userLog("Try to execute ", ops.size(), " CPO's for " + authority);
    260         ContentProviderResult[] result = null;
    261         try {
    262             // Try to execute the whole thing
    263             return applyBatch(contentResolver, authority, ops, 0);
    264         } catch (TransactionTooLargeException e) {
    265             // Nope; split into smaller chunks, demarcated by the separator operation
    266             //mService.userLog("Transaction too large; spliting!");
    267             ArrayList<Operation> mini = new ArrayList<Operation>();
    268             // Build a result array with the total size we're sending
    269             result = new ContentProviderResult[ops.size()];
    270             int count = 0;
    271             int offset = 0;
    272             for (Operation op: ops) {
    273                 if (op.mSeparator) {
    274                     try {
    275                         //mService.userLog("Try mini-batch of ", mini.size(), " CPO's");
    276                         applyAndCopyResults(contentResolver, authority, mini, result, offset);
    277                         mini.clear();
    278                         // Save away the offset here; this will need to be subtracted out of the
    279                         // value originally set by the adapter
    280                         offset = count + 1; // Remember to add 1 for the separator!
    281                     } catch (TransactionTooLargeException e1) {
    282                         throw new RuntimeException("Can't send transaction; sync stopped.");
    283                     } catch (RemoteException e1) {
    284                         throw e1;
    285                     }
    286                 } else {
    287                     mini.add(op);
    288                 }
    289                 count++;
    290             }
    291             // Check out what's left; if it's more than just a separator, apply the batch
    292             int miniSize = mini.size();
    293             if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) {
    294                 applyAndCopyResults(contentResolver, authority, mini, result, offset);
    295             }
    296         } catch (RemoteException e) {
    297             throw e;
    298         } catch (OperationApplicationException e) {
    299             // Not possible since we're building the ops ourselves
    300         }
    301         return result;
    302     }
    303 
    304     /**
    305      * Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's
    306      */
    307     protected static void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) {
    308         Operation op = new Operation(
    309                 ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID)));
    310         op.mSeparator = true;
    311         ops.add(op);
    312     }
    313 }
    314