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