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