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