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