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
17package android.content;
18
19import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
20
21import android.accounts.Account;
22import android.annotation.MainThread;
23import android.annotation.NonNull;
24import android.os.Build;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.IBinder;
28import android.os.Process;
29import android.os.RemoteException;
30import android.os.Trace;
31import android.util.Log;
32
33import java.util.HashMap;
34import 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 intialize 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 */
104public 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     */
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     */
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
161    public Context getContext() {
162        return mContext;
163    }
164
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
175        public void onUnsyncableAccount(ISyncAdapterUnsyncableAccountCallback cb) {
176            Handler.getMain().sendMessage(obtainMessage(
177                    AbstractThreadedSyncAdapter::handleOnUnsyncableAccount,
178                    AbstractThreadedSyncAdapter.this, cb));
179        }
180
181        @Override
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
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
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
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
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     */
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     */
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
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     */
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     */
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     */
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     */
493    public void onSyncCanceled(Thread thread) {
494        thread.interrupt();
495    }
496}
497