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    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