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