• 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 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
sendLocalChanges(Serializer s)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
parse(InputStream is)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)
getCollectionName()70     public abstract String getCollectionName();
cleanup()71     public abstract void cleanup();
isSyncable()72     public abstract boolean isSyncable();
73     // Add sync options (filter, body type - html vs plain, and truncation)
sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)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      */
wipe()79     public abstract void wipe();
80 
isLooping()81     public boolean isLooping() {
82         return false;
83     }
84 
AbstractSyncAdapter(EasSyncService service)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 
userLog(String ....strings)95     public void userLog(String ...strings) {
96         mService.userLog(strings);
97     }
98 
incrementChangeCount()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      */
setPimSyncOptions(Double protocolVersion, String filter, Serializer s)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      */
getSyncKey()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 
setSyncKey(String syncKey, boolean inCommands)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 
Operation(ContentProviderOperation.Builder builder, String columnName, int offset)174         Operation(ContentProviderOperation.Builder builder, String columnName, int offset) {
175             mOp = null;
176             mBuilder = builder;
177             mColumnName = columnName;
178             mOffset = offset;
179         }
180 
Operation(ContentProviderOperation.Builder builder)181         Operation(ContentProviderOperation.Builder builder) {
182             mOp = null;
183             mBuilder = builder;
184             mColumnName = null;
185             mOffset = 0;
186         }
187 
Operation(ContentProviderOperation op)188         Operation(ContentProviderOperation op) {
189             mOp = op;
190             mBuilder = null;
191             mColumnName = null;
192             mOffset = 0;
193         }
194 
toString()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      */
execute(String authority, ArrayList<ContentProviderOperation> ops)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
operationToContentProviderOperation(Operation op, int offset)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      */
applyBatch(String authority, ArrayList<Operation> ops, int offset)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      */
applyAndCopyResults(String authority, ArrayList<Operation> mini, ContentProviderResult[] result, int offset)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      */
safeExecute(String authority, ArrayList<Operation> ops)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      */
addSeparatorOperation(ArrayList<Operation> ops, Uri uri)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