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