1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.content; 18 19import android.accounts.Account; 20import android.os.Build; 21import android.os.Bundle; 22import android.os.IBinder; 23import android.os.Process; 24import android.os.Trace; 25import android.util.Log; 26 27import java.util.HashMap; 28import java.util.concurrent.atomic.AtomicInteger; 29 30/** 31 * An abstract implementation of a SyncAdapter that spawns a thread to invoke a sync operation. 32 * If a sync operation is already in progress when a sync request is received, an error will be 33 * returned to the new request and the existing request will be allowed to continue. 34 * However if there is no sync in progress then a thread will be spawned and {@link #onPerformSync} 35 * will be invoked on that thread. 36 * <p> 37 * Syncs can be cancelled at any time by the framework. For example a sync that was not 38 * user-initiated and lasts longer than 30 minutes will be considered timed-out and cancelled. 39 * Similarly the framework will attempt to determine whether or not an adapter is making progress 40 * by monitoring its network activity over the course of a minute. If the network traffic over this 41 * window is close enough to zero the sync will be cancelled. You can also request the sync be 42 * cancelled via {@link ContentResolver#cancelSync(Account, String)} or 43 * {@link ContentResolver#cancelSync(SyncRequest)}. 44 * <p> 45 * A sync is cancelled by issuing a {@link Thread#interrupt()} on the syncing thread. <strong>Either 46 * your code in {@link #onPerformSync(Account, Bundle, String, ContentProviderClient, SyncResult)} 47 * must check {@link Thread#interrupted()}, or you you must override one of 48 * {@link #onSyncCanceled(Thread)}/{@link #onSyncCanceled()}</strong> (depending on whether or not 49 * your adapter supports syncing of multiple accounts in parallel). If your adapter does not 50 * respect the cancel issued by the framework you run the risk of your app's entire process being 51 * killed. 52 * <p> 53 * In order to be a sync adapter one must extend this class, provide implementations for the 54 * abstract methods and write a service that returns the result of {@link #getSyncAdapterBinder()} 55 * in the service's {@link android.app.Service#onBind(android.content.Intent)} when invoked 56 * with an intent with action <code>android.content.SyncAdapter</code>. This service 57 * must specify the following intent filter and metadata tags in its AndroidManifest.xml file 58 * <pre> 59 * <intent-filter> 60 * <action android:name="android.content.SyncAdapter" /> 61 * </intent-filter> 62 * <meta-data android:name="android.content.SyncAdapter" 63 * android:resource="@xml/syncadapter" /> 64 * </pre> 65 * The <code>android:resource</code> attribute must point to a resource that looks like: 66 * <pre> 67 * <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" 68 * android:contentAuthority="authority" 69 * android:accountType="accountType" 70 * android:userVisible="true|false" 71 * android:supportsUploading="true|false" 72 * android:allowParallelSyncs="true|false" 73 * android:isAlwaysSyncable="true|false" 74 * android:syncAdapterSettingsAction="ACTION_OF_SETTINGS_ACTIVITY" 75 * /> 76 * </pre> 77 * <ul> 78 * <li>The <code>android:contentAuthority</code> and <code>android:accountType</code> attributes 79 * indicate which content authority and for which account types this sync adapter serves. 80 * <li><code>android:userVisible</code> defaults to true and controls whether or not this sync 81 * adapter shows up in the Sync Settings screen. 82 * <li><code>android:supportsUploading</code> defaults 83 * to true and if true an upload-only sync will be requested for all syncadapters associated 84 * with an authority whenever that authority's content provider does a 85 * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)} 86 * with syncToNetwork set to true. 87 * <li><code>android:allowParallelSyncs</code> defaults to false and if true indicates that 88 * the sync adapter can handle syncs for multiple accounts at the same time. Otherwise 89 * the SyncManager will wait until the sync adapter is not in use before requesting that 90 * it sync an account's data. 91 * <li><code>android:isAlwaysSyncable</code> defaults to false and if true tells the SyncManager 92 * to intialize the isSyncable state to 1 for that sync adapter for each account that is added. 93 * <li><code>android:syncAdapterSettingsAction</code> defaults to null and if supplied it 94 * specifies an Intent action of an activity that can be used to adjust the sync adapter's 95 * sync settings. The activity must live in the same package as the sync adapter. 96 * </ul> 97 */ 98public abstract class AbstractThreadedSyncAdapter { 99 private static final String TAG = "SyncAdapter"; 100 101 /** 102 * Kernel event log tag. Also listed in data/etc/event-log-tags. 103 * @deprecated Private constant. May go away in the next release. 104 */ 105 @Deprecated 106 public static final int LOG_SYNC_DETAILS = 2743; 107 108 private static final boolean ENABLE_LOG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG); 109 110 private final Context mContext; 111 private final AtomicInteger mNumSyncStarts; 112 private final ISyncAdapterImpl mISyncAdapterImpl; 113 114 // all accesses to this member variable must be synchronized on mSyncThreadLock 115 private final HashMap<Account, SyncThread> mSyncThreads = new HashMap<Account, SyncThread>(); 116 private final Object mSyncThreadLock = new Object(); 117 118 private final boolean mAutoInitialize; 119 private boolean mAllowParallelSyncs; 120 121 /** 122 * Creates an {@link AbstractThreadedSyncAdapter}. 123 * @param context the {@link android.content.Context} that this is running within. 124 * @param autoInitialize if true then sync requests that have 125 * {@link ContentResolver#SYNC_EXTRAS_INITIALIZE} set will be internally handled by 126 * {@link AbstractThreadedSyncAdapter} by calling 127 * {@link ContentResolver#setIsSyncable(android.accounts.Account, String, int)} with 1 if it 128 * is currently set to <0. 129 */ 130 public AbstractThreadedSyncAdapter(Context context, boolean autoInitialize) { 131 this(context, autoInitialize, false /* allowParallelSyncs */); 132 } 133 134 /** 135 * Creates an {@link AbstractThreadedSyncAdapter}. 136 * @param context the {@link android.content.Context} that this is running within. 137 * @param autoInitialize if true then sync requests that have 138 * {@link ContentResolver#SYNC_EXTRAS_INITIALIZE} set will be internally handled by 139 * {@link AbstractThreadedSyncAdapter} by calling 140 * {@link ContentResolver#setIsSyncable(android.accounts.Account, String, int)} with 1 if it 141 * is currently set to <0. 142 * @param allowParallelSyncs if true then allow syncs for different accounts to run 143 * at the same time, each in their own thread. This must be consistent with the setting 144 * in the SyncAdapter's configuration file. 145 */ 146 public AbstractThreadedSyncAdapter(Context context, 147 boolean autoInitialize, boolean allowParallelSyncs) { 148 mContext = context; 149 mISyncAdapterImpl = new ISyncAdapterImpl(); 150 mNumSyncStarts = new AtomicInteger(0); 151 mAutoInitialize = autoInitialize; 152 mAllowParallelSyncs = allowParallelSyncs; 153 } 154 155 public Context getContext() { 156 return mContext; 157 } 158 159 private Account toSyncKey(Account account) { 160 if (mAllowParallelSyncs) { 161 return account; 162 } else { 163 return null; 164 } 165 } 166 167 private class ISyncAdapterImpl extends ISyncAdapter.Stub { 168 @Override 169 public void startSync(ISyncContext syncContext, String authority, Account account, 170 Bundle extras) { 171 if (ENABLE_LOG) { 172 if (extras != null) { 173 extras.size(); // Unparcel so its toString() will show the contents. 174 } 175 Log.d(TAG, "startSync() start " + authority + " " + account + " " + extras); 176 } 177 try { 178 final SyncContext syncContextClient = new SyncContext(syncContext); 179 180 boolean alreadyInProgress; 181 // synchronize to make sure that mSyncThreads doesn't change between when we 182 // check it and when we use it 183 final Account threadsKey = toSyncKey(account); 184 synchronized (mSyncThreadLock) { 185 if (!mSyncThreads.containsKey(threadsKey)) { 186 if (mAutoInitialize 187 && extras != null 188 && extras.getBoolean( 189 ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { 190 try { 191 if (ContentResolver.getIsSyncable(account, authority) < 0) { 192 ContentResolver.setIsSyncable(account, authority, 1); 193 } 194 } finally { 195 syncContextClient.onFinished(new SyncResult()); 196 } 197 return; 198 } 199 SyncThread syncThread = new SyncThread( 200 "SyncAdapterThread-" + mNumSyncStarts.incrementAndGet(), 201 syncContextClient, authority, account, extras); 202 mSyncThreads.put(threadsKey, syncThread); 203 syncThread.start(); 204 alreadyInProgress = false; 205 } else { 206 if (ENABLE_LOG) { 207 Log.d(TAG, " alreadyInProgress"); 208 } 209 alreadyInProgress = true; 210 } 211 } 212 213 // do this outside since we don't want to call back into the syncContext while 214 // holding the synchronization lock 215 if (alreadyInProgress) { 216 syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); 217 } 218 } catch (RuntimeException | Error th) { 219 if (ENABLE_LOG) { 220 Log.d(TAG, "startSync() caught exception", th); 221 } 222 throw th; 223 } finally { 224 if (ENABLE_LOG) { 225 Log.d(TAG, "startSync() finishing"); 226 } 227 } 228 } 229 230 @Override 231 public void cancelSync(ISyncContext syncContext) { 232 try { 233 // synchronize to make sure that mSyncThreads doesn't change between when we 234 // check it and when we use it 235 SyncThread info = null; 236 synchronized (mSyncThreadLock) { 237 for (SyncThread current : mSyncThreads.values()) { 238 if (current.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { 239 info = current; 240 break; 241 } 242 } 243 } 244 if (info != null) { 245 if (ENABLE_LOG) { 246 Log.d(TAG, "cancelSync() " + info.mAuthority + " " + info.mAccount); 247 } 248 if (mAllowParallelSyncs) { 249 onSyncCanceled(info); 250 } else { 251 onSyncCanceled(); 252 } 253 } else { 254 if (ENABLE_LOG) { 255 Log.w(TAG, "cancelSync() unknown context"); 256 } 257 } 258 } catch (RuntimeException | Error th) { 259 if (ENABLE_LOG) { 260 Log.d(TAG, "cancelSync() caught exception", th); 261 } 262 throw th; 263 } finally { 264 if (ENABLE_LOG) { 265 Log.d(TAG, "cancelSync() finishing"); 266 } 267 } 268 } 269 } 270 271 /** 272 * The thread that invokes {@link AbstractThreadedSyncAdapter#onPerformSync}. It also acquires 273 * the provider for this sync before calling onPerformSync and releases it afterwards. Cancel 274 * this thread in order to cancel the sync. 275 */ 276 private class SyncThread extends Thread { 277 private final SyncContext mSyncContext; 278 private final String mAuthority; 279 private final Account mAccount; 280 private final Bundle mExtras; 281 private final Account mThreadsKey; 282 283 private SyncThread(String name, SyncContext syncContext, String authority, 284 Account account, Bundle extras) { 285 super(name); 286 mSyncContext = syncContext; 287 mAuthority = authority; 288 mAccount = account; 289 mExtras = extras; 290 mThreadsKey = toSyncKey(account); 291 } 292 293 @Override 294 public void run() { 295 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 296 297 if (ENABLE_LOG) { 298 Log.d(TAG, "Thread started"); 299 } 300 301 // Trace this sync instance. Note, conceptually this should be in 302 // SyncStorageEngine.insertStartSyncEvent(), but the trace functions require unique 303 // threads in order to track overlapping operations, so we'll do it here for now. 304 Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, mAuthority); 305 306 SyncResult syncResult = new SyncResult(); 307 ContentProviderClient provider = null; 308 try { 309 if (isCanceled()) { 310 if (ENABLE_LOG) { 311 Log.d(TAG, "Already canceled"); 312 } 313 return; 314 } 315 if (ENABLE_LOG) { 316 Log.d(TAG, "Calling onPerformSync..."); 317 } 318 319 provider = mContext.getContentResolver().acquireContentProviderClient(mAuthority); 320 if (provider != null) { 321 AbstractThreadedSyncAdapter.this.onPerformSync(mAccount, mExtras, 322 mAuthority, provider, syncResult); 323 } else { 324 syncResult.databaseError = true; 325 } 326 327 if (ENABLE_LOG) { 328 Log.d(TAG, "onPerformSync done"); 329 } 330 331 } catch (SecurityException e) { 332 if (ENABLE_LOG) { 333 Log.d(TAG, "SecurityException", e); 334 } 335 AbstractThreadedSyncAdapter.this.onSecurityException(mAccount, mExtras, 336 mAuthority, syncResult); 337 syncResult.databaseError = true; 338 } catch (RuntimeException | Error th) { 339 if (ENABLE_LOG) { 340 Log.d(TAG, "caught exception", th); 341 } 342 throw th; 343 } finally { 344 Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER); 345 346 if (provider != null) { 347 provider.release(); 348 } 349 if (!isCanceled()) { 350 mSyncContext.onFinished(syncResult); 351 } 352 // synchronize so that the assignment will be seen by other threads 353 // that also synchronize accesses to mSyncThreads 354 synchronized (mSyncThreadLock) { 355 mSyncThreads.remove(mThreadsKey); 356 } 357 358 if (ENABLE_LOG) { 359 Log.d(TAG, "Thread finished"); 360 } 361 } 362 } 363 364 private boolean isCanceled() { 365 return Thread.currentThread().isInterrupted(); 366 } 367 } 368 369 /** 370 * @return a reference to the IBinder of the SyncAdapter service. 371 */ 372 public final IBinder getSyncAdapterBinder() { 373 return mISyncAdapterImpl.asBinder(); 374 } 375 376 /** 377 * Perform a sync for this account. SyncAdapter-specific parameters may 378 * be specified in extras, which is guaranteed to not be null. Invocations 379 * of this method are guaranteed to be serialized. 380 * 381 * @param account the account that should be synced 382 * @param extras SyncAdapter-specific parameters 383 * @param authority the authority of this sync request 384 * @param provider a ContentProviderClient that points to the ContentProvider for this 385 * authority 386 * @param syncResult SyncAdapter-specific parameters 387 */ 388 public abstract void onPerformSync(Account account, Bundle extras, 389 String authority, ContentProviderClient provider, SyncResult syncResult); 390 391 /** 392 * Report that there was a security exception when opening the content provider 393 * prior to calling {@link #onPerformSync}. This will be treated as a sync 394 * database failure. 395 * 396 * @param account the account that attempted to sync 397 * @param extras SyncAdapter-specific parameters 398 * @param authority the authority of the failed sync request 399 * @param syncResult SyncAdapter-specific parameters 400 */ 401 public void onSecurityException(Account account, Bundle extras, 402 String authority, SyncResult syncResult) { 403 } 404 405 /** 406 * Indicates that a sync operation has been canceled. This will be invoked on a separate 407 * thread than the sync thread and so you must consider the multi-threaded implications 408 * of the work that you do in this method. 409 * <p> 410 * This will only be invoked when the SyncAdapter indicates that it doesn't support 411 * parallel syncs. 412 */ 413 public void onSyncCanceled() { 414 final SyncThread syncThread; 415 synchronized (mSyncThreadLock) { 416 syncThread = mSyncThreads.get(null); 417 } 418 if (syncThread != null) { 419 syncThread.interrupt(); 420 } 421 } 422 423 /** 424 * Indicates that a sync operation has been canceled. This will be invoked on a separate 425 * thread than the sync thread and so you must consider the multi-threaded implications 426 * of the work that you do in this method. 427 * <p> 428 * This will only be invoked when the SyncAdapter indicates that it does support 429 * parallel syncs. 430 * @param thread the Thread of the sync that is to be canceled. 431 */ 432 public void onSyncCanceled(Thread thread) { 433 thread.interrupt(); 434 } 435} 436