• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
sendLocalChanges(Serializer s)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
parse(InputStream is)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)
getCollectionName()68     public abstract String getCollectionName();
cleanup()69     public abstract void cleanup();
isSyncable()70     public abstract boolean isSyncable();
71     // Add sync options (filter, body type - html vs plain, and truncation)
sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)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      */
wipe()77     public abstract void wipe();
78 
isLooping()79     public boolean isLooping() {
80         return false;
81     }
82 
AbstractSyncAdapter(EasSyncService service)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 
userLog(String ....strings)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      */
getSyncKey()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 
setSyncKey(String syncKey, boolean inCommands)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 
Operation(ContentProviderOperation.Builder builder, String columnName, int offset)138         Operation(ContentProviderOperation.Builder builder, String columnName, int offset) {
139             mOp = null;
140             mBuilder = builder;
141             mColumnName = columnName;
142             mOffset = offset;
143         }
144 
Operation(ContentProviderOperation.Builder builder)145         Operation(ContentProviderOperation.Builder builder) {
146             mOp = null;
147             mBuilder = builder;
148             mColumnName = null;
149             mOffset = 0;
150         }
151 
Operation(ContentProviderOperation op)152         Operation(ContentProviderOperation op) {
153             mOp = op;
154             mBuilder = null;
155             mColumnName = null;
156             mOffset = 0;
157         }
158 
159         @Override
toString()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      */
execute(final ContentResolver contentResolver, final String authority, final ArrayList<ContentProviderOperation> ops)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
operationToContentProviderOperation(Operation op, int offset)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      */
applyBatch(final ContentResolver contentResolver, final String authority, final ArrayList<Operation> ops, final int offset)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      */
applyAndCopyResults(final ContentResolver contentResolver, final String authority, final ArrayList<Operation> mini, final ContentProviderResult[] result, final int offset)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      */
safeExecute(final ContentResolver contentResolver, final String authority, final ArrayList<Operation> ops)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      */
addSeparatorOperation(ArrayList<Operation> ops, Uri uri)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