Home | History | Annotate | Download | only in content
      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 
     17 package android.content;
     18 
     19 import android.accounts.Account;
     20 import android.os.Bundle;
     21 import android.os.IBinder;
     22 import android.os.Process;
     23 import android.os.RemoteException;
     24 import android.os.Trace;
     25 
     26 import java.util.HashMap;
     27 import 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  */
     84 public 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                         try {
    165                             if (ContentResolver.getIsSyncable(account, authority) < 0) {
    166                                 ContentResolver.setIsSyncable(account, authority, 1);
    167                             }
    168                         } finally {
    169                             syncContextClient.onFinished(new SyncResult());
    170                         }
    171                         return;
    172                     }
    173                     SyncThread syncThread = new SyncThread(
    174                             "SyncAdapterThread-" + mNumSyncStarts.incrementAndGet(),
    175                             syncContextClient, authority, account, extras);
    176                     mSyncThreads.put(threadsKey, syncThread);
    177                     syncThread.start();
    178                     alreadyInProgress = false;
    179                 } else {
    180                     alreadyInProgress = true;
    181                 }
    182             }
    183 
    184             // do this outside since we don't want to call back into the syncContext while
    185             // holding the synchronization lock
    186             if (alreadyInProgress) {
    187                 syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS);
    188             }
    189         }
    190 
    191         @Override
    192         public void cancelSync(ISyncContext syncContext) {
    193             // synchronize to make sure that mSyncThreads doesn't change between when we
    194             // check it and when we use it
    195             SyncThread info = null;
    196             synchronized (mSyncThreadLock) {
    197                 for (SyncThread current : mSyncThreads.values()) {
    198                     if (current.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) {
    199                         info = current;
    200                         break;
    201                     }
    202                 }
    203             }
    204             if (info != null) {
    205                 if (mAllowParallelSyncs) {
    206                     onSyncCanceled(info);
    207                 } else {
    208                     onSyncCanceled();
    209                 }
    210             }
    211         }
    212 
    213         public void initialize(Account account, String authority) throws RemoteException {
    214             Bundle extras = new Bundle();
    215             extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
    216             startSync(null, authority, account, extras);
    217         }
    218     }
    219 
    220     /**
    221      * The thread that invokes {@link AbstractThreadedSyncAdapter#onPerformSync}. It also acquires
    222      * the provider for this sync before calling onPerformSync and releases it afterwards. Cancel
    223      * this thread in order to cancel the sync.
    224      */
    225     private class SyncThread extends Thread {
    226         private final SyncContext mSyncContext;
    227         private final String mAuthority;
    228         private final Account mAccount;
    229         private final Bundle mExtras;
    230         private final Account mThreadsKey;
    231 
    232         private SyncThread(String name, SyncContext syncContext, String authority,
    233                 Account account, Bundle extras) {
    234             super(name);
    235             mSyncContext = syncContext;
    236             mAuthority = authority;
    237             mAccount = account;
    238             mExtras = extras;
    239             mThreadsKey = toSyncKey(account);
    240         }
    241 
    242         @Override
    243         public void run() {
    244             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    245 
    246             // Trace this sync instance.  Note, conceptually this should be in
    247             // SyncStorageEngine.insertStartSyncEvent(), but the trace functions require unique
    248             // threads in order to track overlapping operations, so we'll do it here for now.
    249             Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, mAuthority);
    250 
    251             SyncResult syncResult = new SyncResult();
    252             ContentProviderClient provider = null;
    253             try {
    254                 if (isCanceled()) {
    255                     return;
    256                 }
    257                 provider = mContext.getContentResolver().acquireContentProviderClient(mAuthority);
    258                 if (provider != null) {
    259                     AbstractThreadedSyncAdapter.this.onPerformSync(mAccount, mExtras,
    260                             mAuthority, provider, syncResult);
    261                 } else {
    262                     syncResult.databaseError = true;
    263                 }
    264             } finally {
    265                 Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER);
    266 
    267                 if (provider != null) {
    268                     provider.release();
    269                 }
    270                 if (!isCanceled()) {
    271                     mSyncContext.onFinished(syncResult);
    272                 }
    273                 // synchronize so that the assignment will be seen by other threads
    274                 // that also synchronize accesses to mSyncThreads
    275                 synchronized (mSyncThreadLock) {
    276                     mSyncThreads.remove(mThreadsKey);
    277                 }
    278             }
    279         }
    280 
    281         private boolean isCanceled() {
    282             return Thread.currentThread().isInterrupted();
    283         }
    284     }
    285 
    286     /**
    287      * @return a reference to the IBinder of the SyncAdapter service.
    288      */
    289     public final IBinder getSyncAdapterBinder() {
    290         return mISyncAdapterImpl.asBinder();
    291     }
    292 
    293     /**
    294      * Perform a sync for this account. SyncAdapter-specific parameters may
    295      * be specified in extras, which is guaranteed to not be null. Invocations
    296      * of this method are guaranteed to be serialized.
    297      *
    298      * @param account the account that should be synced
    299      * @param extras SyncAdapter-specific parameters
    300      * @param authority the authority of this sync request
    301      * @param provider a ContentProviderClient that points to the ContentProvider for this
    302      *   authority
    303      * @param syncResult SyncAdapter-specific parameters
    304      */
    305     public abstract void onPerformSync(Account account, Bundle extras,
    306             String authority, ContentProviderClient provider, SyncResult syncResult);
    307 
    308     /**
    309      * Indicates that a sync operation has been canceled. This will be invoked on a separate
    310      * thread than the sync thread and so you must consider the multi-threaded implications
    311      * of the work that you do in this method.
    312      * <p>
    313      * This will only be invoked when the SyncAdapter indicates that it doesn't support
    314      * parallel syncs.
    315      */
    316     public void onSyncCanceled() {
    317         final SyncThread syncThread;
    318         synchronized (mSyncThreadLock) {
    319             syncThread = mSyncThreads.get(null);
    320         }
    321         if (syncThread != null) {
    322             syncThread.interrupt();
    323         }
    324     }
    325 
    326     /**
    327      * Indicates that a sync operation has been canceled. This will be invoked on a separate
    328      * thread than the sync thread and so you must consider the multi-threaded implications
    329      * of the work that you do in this method.
    330      * <p>
    331      * This will only be invoked when the SyncAdapter indicates that it does support
    332      * parallel syncs.
    333      * @param thread the Thread of the sync that is to be canceled.
    334      */
    335     public void onSyncCanceled(Thread thread) {
    336         thread.interrupt();
    337     }
    338 }
    339