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