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