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