AbstractSyncAdapter.java revision bb0141b49e7eff978fa445249dc888461ea581e3
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.mail.utils.LogUtils;
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    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