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