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.mail.utils.LogUtils; 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 Context mContext; 57 public Account mAccount; 58 public final ContentResolver mContentResolver; 59 public final android.accounts.Account mAccountManagerAccount; 60 61 // Create the data for local changes that need to be sent up to the server 62 public abstract boolean sendLocalChanges(Serializer s) throws IOException; 63 // Parse incoming data from the EAS server, creating, modifying, and deleting objects as 64 // required through the EmailProvider 65 public abstract boolean parse(InputStream is) throws IOException, CommandStatusException; 66 // The name used to specify the collection type of the target (Email, Calendar, or Contacts) 67 public abstract String getCollectionName(); 68 public abstract void cleanup(); 69 public abstract boolean isSyncable(); 70 // Add sync options (filter, body type - html vs plain, and truncation) 71 public abstract void sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync) 72 throws IOException; 73 /** 74 * Delete all records of this class in this account 75 */ 76 public abstract void wipe(); 77 78 public boolean isLooping() { 79 return false; 80 } 81 82 public AbstractSyncAdapter(final Context context, final Mailbox mailbox, 83 final Account account) { 84 mContext = context; 85 mMailbox = mailbox; 86 mAccount = account; 87 mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress, 88 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE); 89 mContentResolver = mContext.getContentResolver(); 90 } 91 92 /** 93 * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts) 94 * @return the current SyncKey for the Mailbox 95 * @throws IOException 96 */ 97 public String getSyncKey() throws IOException { 98 if (mMailbox.mSyncKey == null) { 99 LogUtils.d(LogUtils.TAG, "Reset SyncKey to 0"); 100 mMailbox.mSyncKey = "0"; 101 } 102 return mMailbox.mSyncKey; 103 } 104 105 public void setSyncKey(String syncKey, boolean inCommands) throws IOException { 106 mMailbox.mSyncKey = syncKey; 107 } 108 109 /** 110 * Operation is our binder-safe ContentProviderOperation (CPO) construct; an Operation can 111 * be created from a CPO, a CPO Builder, or a CPO Builder with a "back reference" column name 112 * and offset (that might be used in Builder.withValueBackReference). The CPO is not actually 113 * built until it is ready to be executed (with applyBatch); this allows us to recalculate 114 * back reference offsets if we are required to re-send a large batch in smaller chunks. 115 * 116 * NOTE: A failed binder transaction is something of an emergency case, and shouldn't happen 117 * with any frequency. When it does, and we are forced to re-send the data to the content 118 * provider in smaller chunks, we DO lose the sync-window atomicity, and thereby add another 119 * small risk to the data. Of course, this is far, far better than dropping the data on the 120 * floor, as was done before the framework implemented TransactionTooLargeException 121 */ 122 protected static class Operation { 123 final ContentProviderOperation mOp; 124 final ContentProviderOperation.Builder mBuilder; 125 final String mColumnName; 126 final int mOffset; 127 // Is this Operation a separator? (a good place to break up a large transaction) 128 boolean mSeparator = false; 129 130 // For toString() 131 final String[] TYPES = new String[] {"???", "Ins", "Upd", "Del", "Assert"}; 132 133 Operation(ContentProviderOperation.Builder builder, String columnName, int offset) { 134 mOp = null; 135 mBuilder = builder; 136 mColumnName = columnName; 137 mOffset = offset; 138 } 139 140 Operation(ContentProviderOperation.Builder builder) { 141 mOp = null; 142 mBuilder = builder; 143 mColumnName = null; 144 mOffset = 0; 145 } 146 147 Operation(ContentProviderOperation op) { 148 mOp = op; 149 mBuilder = null; 150 mColumnName = null; 151 mOffset = 0; 152 } 153 154 @Override 155 public String toString() { 156 StringBuilder sb = new StringBuilder("Op: "); 157 ContentProviderOperation op = operationToContentProviderOperation(this, 0); 158 int type = 0; 159 //DO NOT SHIP WITH THE FOLLOWING LINE (the API is hidden!) 160 //type = op.getType(); 161 sb.append(TYPES[type]); 162 Uri uri = op.getUri(); 163 sb.append(' '); 164 sb.append(uri.getPath()); 165 if (mColumnName != null) { 166 sb.append(" Back value of " + mColumnName + ": " + mOffset); 167 } 168 return sb.toString(); 169 } 170 } 171 172 /** 173 * We apply the batch of CPO's here. We synchronize on the service to avoid thread-nasties, 174 * and we just return quickly if the service has already been stopped. 175 */ 176 private static ContentProviderResult[] execute(final ContentResolver contentResolver, 177 final String authority, final ArrayList<ContentProviderOperation> ops) 178 throws RemoteException, OperationApplicationException { 179 if (!ops.isEmpty()) { 180 ContentProviderResult[] result = contentResolver.applyBatch(authority, ops); 181 return result; 182 } 183 return new ContentProviderResult[0]; 184 } 185 186 /** 187 * Convert an Operation to a CPO; if the Operation has a back reference, apply it with the 188 * passed-in offset 189 */ 190 @VisibleForTesting 191 static ContentProviderOperation operationToContentProviderOperation(Operation op, int offset) { 192 if (op.mOp != null) { 193 return op.mOp; 194 } else if (op.mBuilder == null) { 195 throw new IllegalArgumentException("Operation must have CPO.Builder"); 196 } 197 ContentProviderOperation.Builder builder = op.mBuilder; 198 if (op.mColumnName != null) { 199 builder.withValueBackReference(op.mColumnName, op.mOffset - offset); 200 } 201 return builder.build(); 202 } 203 204 /** 205 * Create a list of CPOs from a list of Operations, and then apply them in a batch 206 */ 207 private static ContentProviderResult[] applyBatch(final ContentResolver contentResolver, 208 final String authority, final ArrayList<Operation> ops, final int offset) 209 throws RemoteException, OperationApplicationException { 210 // Handle the empty case 211 if (ops.isEmpty()) { 212 return new ContentProviderResult[0]; 213 } 214 ArrayList<ContentProviderOperation> cpos = new ArrayList<ContentProviderOperation>(); 215 for (Operation op: ops) { 216 cpos.add(operationToContentProviderOperation(op, offset)); 217 } 218 return execute(contentResolver, authority, cpos); 219 } 220 221 /** 222 * Apply the list of CPO's in the provider and copy the "mini" result into our full result array 223 */ 224 private static void applyAndCopyResults(final ContentResolver contentResolver, 225 final String authority, final ArrayList<Operation> mini, 226 final ContentProviderResult[] result, final int offset) throws RemoteException { 227 // Empty lists are ok; we just ignore them 228 if (mini.isEmpty()) return; 229 try { 230 ContentProviderResult[] miniResult = applyBatch(contentResolver, authority, mini, 231 offset); 232 // Copy the results from this mini-batch into our results array 233 System.arraycopy(miniResult, 0, result, offset, miniResult.length); 234 } catch (OperationApplicationException e) { 235 // Not possible since we're building the ops ourselves 236 } 237 } 238 239 /** 240 * Called by a sync adapter to execute a list of Operations in the ContentProvider handling 241 * the passed-in authority. If the attempt to apply the batch fails due to a too-large 242 * binder transaction, we split the Operations as directed by separators. If any of the 243 * "mini" batches fails due to a too-large transaction, we're screwed, but this would be 244 * vanishingly rare. Other, possibly transient, errors are handled by throwing a 245 * RemoteException, which the caller will likely re-throw as an IOException so that the sync 246 * can be attempted again. 247 * 248 * Callers MAY leave a dangling separator at the end of the list; note that the separators 249 * themselves are only markers and are not sent to the provider. 250 */ 251 protected static ContentProviderResult[] safeExecute(final ContentResolver contentResolver, 252 final String authority, final ArrayList<Operation> ops) throws RemoteException { 253 ContentProviderResult[] result = null; 254 try { 255 // Try to execute the whole thing 256 return applyBatch(contentResolver, authority, ops, 0); 257 } catch (TransactionTooLargeException e) { 258 // Nope; split into smaller chunks, demarcated by the separator operation 259 ArrayList<Operation> mini = new ArrayList<Operation>(); 260 // Build a result array with the total size we're sending 261 result = new ContentProviderResult[ops.size()]; 262 int count = 0; 263 int offset = 0; 264 for (Operation op: ops) { 265 if (op.mSeparator) { 266 try { 267 applyAndCopyResults(contentResolver, authority, mini, result, offset); 268 mini.clear(); 269 // Save away the offset here; this will need to be subtracted out of the 270 // value originally set by the adapter 271 offset = count + 1; // Remember to add 1 for the separator! 272 } catch (TransactionTooLargeException e1) { 273 throw new RuntimeException("Can't send transaction; sync stopped."); 274 } catch (RemoteException e1) { 275 throw e1; 276 } 277 } else { 278 mini.add(op); 279 } 280 count++; 281 } 282 // Check out what's left; if it's more than just a separator, apply the batch 283 int miniSize = mini.size(); 284 if ((miniSize > 0) && !(miniSize == 1 && mini.get(0).mSeparator)) { 285 applyAndCopyResults(contentResolver, authority, mini, result, offset); 286 } 287 } catch (RemoteException e) { 288 throw e; 289 } catch (OperationApplicationException e) { 290 // Not possible since we're building the ops ourselves 291 } 292 return result; 293 } 294 295 /** 296 * Called by a sync adapter to indicate a relatively safe place to split a batch of CPO's 297 */ 298 protected static void addSeparatorOperation(ArrayList<Operation> ops, Uri uri) { 299 Operation op = new Operation( 300 ContentProviderOperation.newDelete(ContentUris.withAppendedId(uri, SEPARATOR_ID))); 301 op.mSeparator = true; 302 ops.add(op); 303 } 304 } 305