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  *   &lt;intent-filter&gt;
66  *     &lt;action android:name="android.content.SyncAdapter" /&gt;
67  *   &lt;/intent-filter&gt;
68  *   &lt;meta-data android:name="android.content.SyncAdapter"
69  *             android:resource="@xml/syncadapter" /&gt;
70  * </pre>
71  * The <code>android:resource</code> attribute must point to a resource that looks like:
72  * <pre>
73  * &lt;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  * /&gt;
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