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