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