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