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