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