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 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  */
     97 public 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