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