AbstractSyncAdapter.java revision df177fdec2013e94eb77405ff1c4d0cb95707add
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 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 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 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) 70 public abstract String getCollectionName(); 71 public abstract void cleanup(); 72 public abstract boolean isSyncable(); 73 // Add sync options (filter, body type - html vs plain, and truncation) 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 */ 79 public abstract void wipe(); 80 81 public boolean isLooping() { 82 return false; 83 } 84 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 95 public void userLog(String ...strings) { 96 mService.userLog(strings); 97 } 98 99 /** 100 * Set sync options common to PIM's (contacts and calendar) 101 * @param protocolVersion the protocol version under which we're syncing 102 * @param filter the filter to use (or null) 103 * @param s the Serializer 104 * @throws IOException 105 */ 106 protected void setPimSyncOptions(Double protocolVersion, String filter, Serializer s) 107 throws IOException { 108 s.tag(Tags.SYNC_DELETES_AS_MOVES); 109 s.tag(Tags.SYNC_GET_CHANGES); 110 s.data(Tags.SYNC_WINDOW_SIZE, PIM_WINDOW_SIZE); 111 s.start(Tags.SYNC_OPTIONS); 112 // Set the filter (lookback), if provided 113 if (filter != null) { 114 s.data(Tags.SYNC_FILTER_TYPE, filter); 115 } 116 // Set the truncation amount and body type 117 if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) { 118 s.start(Tags.BASE_BODY_PREFERENCE); 119 // Plain text 120 s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT); 121 s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE); 122 s.end(); 123 } else { 124 s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE); 125 } 126 s.end(); 127 } 128 129 /** 130 * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts) 131 * @return the current SyncKey for the Mailbox 132 * @throws IOException 133 */ 134 public String getSyncKey() throws IOException { 135 if (mMailbox.mSyncKey == null) { 136 userLog("Reset SyncKey to 0"); 137 mMailbox.mSyncKey = "0"; 138 } 139 return mMailbox.mSyncKey; 140 } 141 142 public void setSyncKey(String syncKey, boolean inCommands) throws IOException { 143 mMailbox.mSyncKey = syncKey; 144 } 145 146 /** 147 * Operation is our binder-safe ContentProviderOperation (CPO) construct; an Operation can 148 * be created from a CPO, a CPO Builder, or a CPO Builder with a "back reference" column name 149 * and offset (that might be used in Builder.withValueBackReference). The CPO is not actually 150 * built until it is ready to be executed (with applyBatch); this allows us to recalculate 151 * back reference offsets if we are required to re-send a large batch in smaller chunks. 152 * 153 * NOTE: A failed binder transaction is something of an emergency case, and shouldn't happen 154 * with any frequency. When it does, and we are forced to re-send the data to the content 155 * provider in smaller chunks, we DO lose the sync-window atomicity, and thereby add another 156 * small risk to the data. Of course, this is far, far better than dropping the data on the 157 * floor, as was done before the framework implemented TransactionTooLargeException 158 */ 159 protected static class Operation { 160 final ContentProviderOperation mOp; 161 final ContentProviderOperation.Builder mBuilder; 162 final String mColumnName; 163 final int mOffset; 164 // Is this Operation a separator? (a good place to break up a large transaction) 165 boolean mSeparator = false; 166 167 // For toString() 168 final String[] TYPES = new String[] {"???", "Ins", "Upd", "Del", "Assert"}; 169 170 Operation(ContentProviderOperation.Builder builder, String columnName, int offset) { 171 mOp = null; 172 mBuilder = builder; 173 mColumnName = columnName; 174 mOffset = offset; 175 } 176 177 Operation(ContentProviderOperation.Builder builder) { 178 mOp = null; 179 mBuilder = builder; 180 mColumnName = null; 181 mOffset = 0; 182 } 183 184 Operation(ContentProviderOperation op) { 185 mOp = op; 186 mBuilder = null; 187 mColumnName = null; 188 mOffset = 0; 189 } 190 191 @Override 192 public String toString() { 193 StringBuilder sb = new StringBuilder("Op: "); 194 ContentProviderOperation op = operationToContentProviderOperation(this, 0); 195 int type = 0; 196 //DO NOT SHIP WITH THE FOLLOWING LINE (the API is hidden!) 197 //type = op.getType(); 198 sb.append(TYPES[type]); 199 Uri uri = op.getUri(); 200 sb.append(' '); 201 sb.append(uri.getPath()); 202 if (mColumnName != null) { 203 sb.append(" Back value of " + mColumnName + ": " + mOffset); 204 } 205 return sb.toString(); 206 } 207 } 208 209 /** 210 * We apply the batch of CPO's here. We synchronize on the service to avoid thread-nasties, 211 * and we just return quickly if the service has already been stopped. 212 */ 213 private static ContentProviderResult[] execute(final ContentResolver contentResolver, 214 final String authority, final ArrayList<ContentProviderOperation> ops) 215 throws RemoteException, OperationApplicationException { 216 if (!ops.isEmpty()) { 217 ContentProviderResult[] result = contentResolver.applyBatch(authority, ops); 218 //mService.userLog("Results: " + result.length); 219 return result; 220 } 221 return new ContentProviderResult[0]; 222 } 223 224 /** 225 * Convert an Operation to a CPO; if the Operation has a back reference, apply it with the 226 * passed-in offset 227 */ 228 @VisibleForTesting 229 static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) { 230 if (op.mOp != null) { 231 return op.mOp; 232 } else if (op.mBuilder == null) { 233 throw new IllegalArgumentException("Operation must have CPO.Builder"); 234 } 235 ContentProviderOperation.Builder builder = op.mBuilder; 236 if (op.mColumnName != null) { 237 builder.withValueBackReference(op.mColumnName, op.mOffset - offset); 238 } 239 return builder.build(); 240 } 241 242 /** 243 * Create a list of CPOs from a list of Operations, and then apply them in a batch 244 */ 245 private static ContentProviderResult[] applyBatch(final ContentResolver contentResolver, 246 final String authority, final ArrayList<Operation> ops, final int offset) 247 throws RemoteException, OperationApplicationException { 248 // Handle the empty case 249 if (ops.isEmpty()) { 250 return new ContentProviderResult[0]; 251 } 252 ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>(); 253 for (Operation op: ops) { 254 cpos.add(operationToContentProviderOperation(op, offset)); 255 } 256 return execute(contentResolver, authority, cpos); 257 } 258 259 /** 260 * Apply the list of CPO's in the provider and copy the "mini" result into our full result array 261 */ 262 private static void applyAndCopyResults(final ContentResolver contentResolver, 263 final String authority, final ArrayList<Operation> mini, 264 final ContentProviderResult[] result, final int offset) throws RemoteException { 265 // Empty lists are ok; we just ignore them 266 if (mini.isEmpty()) return; 267 try { 268 ContentProviderResult[] miniResult = applyBatch(contentResolver, authority, mini, 269 offset); 270 // Copy the results from this mini-batch into our results array 271 System.arraycopy(miniResult, 0, result, offset, miniResult.length); 272 } catch (OperationApplicationException e) { 273 // Not possible since we're building the ops ourselves 274 } 275 } 276 277 /** 278 * Called by a sync adapter to execute a list of Operations in the ContentProvider handling 279 * the passed-in authority. If the attempt to apply the batch fails due to a too-large 280 * binder transaction, we split the Operations as directed by separators. If any of the 281 * "mini" batches fails due to a too-large transaction, we're screwed, but this would be 282 * vanishingly rare. Other, possibly transient, errors are handled by throwing a 283 * RemoteException, which the caller will likely re-throw as an IOException so that the sync 284 * can be attempted again. 285 * 286 * Callers MAY leave a dangling separator at the end of the list; note that the separators 287 * themselves are only markers and are not sent to the provider. 288 */ 289 protected static ContentProviderResult[] safeExecute(final ContentResolver contentResolver, 290 final String authority, final ArrayList<Operation> ops) throws RemoteException { 291 //mService.userLog("Try to execute ", ops.size(), " CPO's for " + authority); 292 ContentProviderResult[] result = null; 293 try { 294 // Try to execute the whole thing 295 return applyBatch(contentResolver, authority, ops, 0); 296 } catch (TransactionTooLargeException e) { 297 // Nope; split into smaller chunks, demarcated by the separator operation 298 //mService.userLog("Transaction too large; spliting!"); 299 ArrayList<Operation> mini = new ArrayList<Operation>(); 300 // Build a result array with the total size we're sending 301 result = new ContentProviderResult[ops.size()]; 302 int count = 0; 303 int offset = 0; 304 for (Operation op: ops) { 305 if (op.mSeparator) { 306 try { 307 //mService.userLog("Try mini-batch of ", mini.size(), " CPO's"); 308 applyAndCopyResults(contentResolver, authority, mini, result, offset); 309 mini.clear(); 310 // Save away the offset here; this will need to be subtracted out of the 311 // value originally set by the adapter 312 offset = count + 1; // Remember to add 1 for the separator! 313 } catch (TransactionTooLargeException e1) { 314 throw new RuntimeException("Can't send transaction; sync stopped."); 315 } catch (RemoteException e1) { 316 throw e1; 317 } 318 } else { 319 mini.add(op); 320 } 321 count++; 322 } 323 // Check out what's left; if it's more than just a separator, apply the batch 324 int miniSize = mini.size(); 325 if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) { 326 applyAndCopyResults(contentResolver, authority, mini, result, offset); 327 } 328 } catch (RemoteException e) { 329 throw e; 330 } catch (OperationApplicationException e) { 331 // Not possible since we're building the ops ourselves 332 } 333 return result; 334 } 335 336 /** 337 * Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's 338 */ 339 protected static void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) { 340 Operation op = new Operation( 341 ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID))); 342 op.mSeparator = true; 343 ops.add(op); 344 } 345} 346