| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.content; |
| |
| import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; |
| |
| import android.accounts.Account; |
| import android.annotation.MainThread; |
| import android.annotation.NonNull; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.Trace; |
| import android.util.Log; |
| |
| import java.util.HashMap; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| /** |
| * An abstract implementation of a SyncAdapter that spawns a thread to invoke a sync operation. |
| * If a sync operation is already in progress when a sync request is received, an error will be |
| * returned to the new request and the existing request will be allowed to continue. |
| * However if there is no sync in progress then a thread will be spawned and {@link #onPerformSync} |
| * will be invoked on that thread. |
| * <p> |
| * Syncs can be cancelled at any time by the framework. For example a sync that was not |
| * user-initiated and lasts longer than 30 minutes will be considered timed-out and cancelled. |
| * Similarly the framework will attempt to determine whether or not an adapter is making progress |
| * by monitoring its network activity over the course of a minute. If the network traffic over this |
| * window is close enough to zero the sync will be cancelled. You can also request the sync be |
| * cancelled via {@link ContentResolver#cancelSync(Account, String)} or |
| * {@link ContentResolver#cancelSync(SyncRequest)}. |
| * <p> |
| * A sync is cancelled by issuing a {@link Thread#interrupt()} on the syncing thread. <strong>Either |
| * your code in {@link #onPerformSync(Account, Bundle, String, ContentProviderClient, SyncResult)} |
| * must check {@link Thread#interrupted()}, or you you must override one of |
| * {@link #onSyncCanceled(Thread)}/{@link #onSyncCanceled()}</strong> (depending on whether or not |
| * your adapter supports syncing of multiple accounts in parallel). If your adapter does not |
| * respect the cancel issued by the framework you run the risk of your app's entire process being |
| * killed. |
| * <p> |
| * In order to be a sync adapter one must extend this class, provide implementations for the |
| * abstract methods and write a service that returns the result of {@link #getSyncAdapterBinder()} |
| * in the service's {@link android.app.Service#onBind(android.content.Intent)} when invoked |
| * with an intent with action <code>android.content.SyncAdapter</code>. This service |
| * must specify the following intent filter and metadata tags in its AndroidManifest.xml file |
| * <pre> |
| * <intent-filter> |
| * <action android:name="android.content.SyncAdapter" /> |
| * </intent-filter> |
| * <meta-data android:name="android.content.SyncAdapter" |
| * android:resource="@xml/syncadapter" /> |
| * </pre> |
| * The <code>android:resource</code> attribute must point to a resource that looks like: |
| * <pre> |
| * <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" |
| * android:contentAuthority="authority" |
| * android:accountType="accountType" |
| * android:userVisible="true|false" |
| * android:supportsUploading="true|false" |
| * android:allowParallelSyncs="true|false" |
| * android:isAlwaysSyncable="true|false" |
| * android:syncAdapterSettingsAction="ACTION_OF_SETTINGS_ACTIVITY" |
| * /> |
| * </pre> |
| * <ul> |
| * <li>The <code>android:contentAuthority</code> and <code>android:accountType</code> attributes |
| * indicate which content authority and for which account types this sync adapter serves. |
| * <li><code>android:userVisible</code> defaults to true and controls whether or not this sync |
| * adapter shows up in the Sync Settings screen. |
| * <li><code>android:supportsUploading</code> defaults |
| * to true and if true an upload-only sync will be requested for all syncadapters associated |
| * with an authority whenever that authority's content provider does a |
| * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)} |
| * with syncToNetwork set to true. |
| * <li><code>android:allowParallelSyncs</code> defaults to false and if true indicates that |
| * the sync adapter can handle syncs for multiple accounts at the same time. Otherwise |
| * the SyncManager will wait until the sync adapter is not in use before requesting that |
| * it sync an account's data. |
| * <li><code>android:isAlwaysSyncable</code> defaults to false and if true tells the SyncManager |
| * to intialize the isSyncable state to 1 for that sync adapter for each account that is added. |
| * <li><code>android:syncAdapterSettingsAction</code> defaults to null and if supplied it |
| * specifies an Intent action of an activity that can be used to adjust the sync adapter's |
| * sync settings. The activity must live in the same package as the sync adapter. |
| * </ul> |
| */ |
| public abstract class AbstractThreadedSyncAdapter { |
| private static final String TAG = "SyncAdapter"; |
| |
| /** |
| * Kernel event log tag. Also listed in data/etc/event-log-tags. |
| * @deprecated Private constant. May go away in the next release. |
| */ |
| @Deprecated |
| public static final int LOG_SYNC_DETAILS = 2743; |
| |
| private static final boolean ENABLE_LOG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG); |
| |
| private final Context mContext; |
| private final AtomicInteger mNumSyncStarts; |
| private final ISyncAdapterImpl mISyncAdapterImpl; |
| |
| // all accesses to this member variable must be synchronized on mSyncThreadLock |
| private final HashMap<Account, SyncThread> mSyncThreads = new HashMap<Account, SyncThread>(); |
| private final Object mSyncThreadLock = new Object(); |
| |
| private final boolean mAutoInitialize; |
| private boolean mAllowParallelSyncs; |
| |
| /** |
| * Creates an {@link AbstractThreadedSyncAdapter}. |
| * @param context the {@link android.content.Context} that this is running within. |
| * @param autoInitialize if true then sync requests that have |
| * {@link ContentResolver#SYNC_EXTRAS_INITIALIZE} set will be internally handled by |
| * {@link AbstractThreadedSyncAdapter} by calling |
| * {@link ContentResolver#setIsSyncable(android.accounts.Account, String, int)} with 1 if it |
| * is currently set to <0. |
| */ |
| public AbstractThreadedSyncAdapter(Context context, boolean autoInitialize) { |
| this(context, autoInitialize, false /* allowParallelSyncs */); |
| } |
| |
| /** |
| * Creates an {@link AbstractThreadedSyncAdapter}. |
| * @param context the {@link android.content.Context} that this is running within. |
| * @param autoInitialize if true then sync requests that have |
| * {@link ContentResolver#SYNC_EXTRAS_INITIALIZE} set will be internally handled by |
| * {@link AbstractThreadedSyncAdapter} by calling |
| * {@link ContentResolver#setIsSyncable(android.accounts.Account, String, int)} with 1 if it |
| * is currently set to <0. |
| * @param allowParallelSyncs if true then allow syncs for different accounts to run |
| * at the same time, each in their own thread. This must be consistent with the setting |
| * in the SyncAdapter's configuration file. |
| */ |
| public AbstractThreadedSyncAdapter(Context context, |
| boolean autoInitialize, boolean allowParallelSyncs) { |
| mContext = context; |
| mISyncAdapterImpl = new ISyncAdapterImpl(); |
| mNumSyncStarts = new AtomicInteger(0); |
| mAutoInitialize = autoInitialize; |
| mAllowParallelSyncs = allowParallelSyncs; |
| } |
| |
| public Context getContext() { |
| return mContext; |
| } |
| |
| private Account toSyncKey(Account account) { |
| if (mAllowParallelSyncs) { |
| return account; |
| } else { |
| return null; |
| } |
| } |
| |
| private class ISyncAdapterImpl extends ISyncAdapter.Stub { |
| @Override |
| public void onUnsyncableAccount(ISyncAdapterUnsyncableAccountCallback cb) { |
| Handler.getMain().sendMessage(obtainMessage( |
| AbstractThreadedSyncAdapter::handleOnUnsyncableAccount, |
| AbstractThreadedSyncAdapter.this, cb)); |
| } |
| |
| @Override |
| public void startSync(ISyncContext syncContext, String authority, Account account, |
| Bundle extras) { |
| if (ENABLE_LOG) { |
| if (extras != null) { |
| extras.size(); // Unparcel so its toString() will show the contents. |
| } |
| Log.d(TAG, "startSync() start " + authority + " " + account + " " + extras); |
| } |
| try { |
| final SyncContext syncContextClient = new SyncContext(syncContext); |
| |
| boolean alreadyInProgress; |
| // synchronize to make sure that mSyncThreads doesn't change between when we |
| // check it and when we use it |
| final Account threadsKey = toSyncKey(account); |
| synchronized (mSyncThreadLock) { |
| if (!mSyncThreads.containsKey(threadsKey)) { |
| if (mAutoInitialize |
| && extras != null |
| && extras.getBoolean( |
| ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { |
| try { |
| if (ContentResolver.getIsSyncable(account, authority) < 0) { |
| ContentResolver.setIsSyncable(account, authority, 1); |
| } |
| } finally { |
| syncContextClient.onFinished(new SyncResult()); |
| } |
| return; |
| } |
| SyncThread syncThread = new SyncThread( |
| "SyncAdapterThread-" + mNumSyncStarts.incrementAndGet(), |
| syncContextClient, authority, account, extras); |
| mSyncThreads.put(threadsKey, syncThread); |
| syncThread.start(); |
| alreadyInProgress = false; |
| } else { |
| if (ENABLE_LOG) { |
| Log.d(TAG, " alreadyInProgress"); |
| } |
| alreadyInProgress = true; |
| } |
| } |
| |
| // do this outside since we don't want to call back into the syncContext while |
| // holding the synchronization lock |
| if (alreadyInProgress) { |
| syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); |
| } |
| } catch (RuntimeException | Error th) { |
| if (ENABLE_LOG) { |
| Log.d(TAG, "startSync() caught exception", th); |
| } |
| throw th; |
| } finally { |
| if (ENABLE_LOG) { |
| Log.d(TAG, "startSync() finishing"); |
| } |
| } |
| } |
| |
| @Override |
| public void cancelSync(ISyncContext syncContext) { |
| try { |
| // synchronize to make sure that mSyncThreads doesn't change between when we |
| // check it and when we use it |
| SyncThread info = null; |
| synchronized (mSyncThreadLock) { |
| for (SyncThread current : mSyncThreads.values()) { |
| if (current.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { |
| info = current; |
| break; |
| } |
| } |
| } |
| if (info != null) { |
| if (ENABLE_LOG) { |
| Log.d(TAG, "cancelSync() " + info.mAuthority + " " + info.mAccount); |
| } |
| if (mAllowParallelSyncs) { |
| onSyncCanceled(info); |
| } else { |
| onSyncCanceled(); |
| } |
| } else { |
| if (ENABLE_LOG) { |
| Log.w(TAG, "cancelSync() unknown context"); |
| } |
| } |
| } catch (RuntimeException | Error th) { |
| if (ENABLE_LOG) { |
| Log.d(TAG, "cancelSync() caught exception", th); |
| } |
| throw th; |
| } finally { |
| if (ENABLE_LOG) { |
| Log.d(TAG, "cancelSync() finishing"); |
| } |
| } |
| } |
| } |
| |
| /** |
| * The thread that invokes {@link AbstractThreadedSyncAdapter#onPerformSync}. It also acquires |
| * the provider for this sync before calling onPerformSync and releases it afterwards. Cancel |
| * this thread in order to cancel the sync. |
| */ |
| private class SyncThread extends Thread { |
| private final SyncContext mSyncContext; |
| private final String mAuthority; |
| private final Account mAccount; |
| private final Bundle mExtras; |
| private final Account mThreadsKey; |
| |
| private SyncThread(String name, SyncContext syncContext, String authority, |
| Account account, Bundle extras) { |
| super(name); |
| mSyncContext = syncContext; |
| mAuthority = authority; |
| mAccount = account; |
| mExtras = extras; |
| mThreadsKey = toSyncKey(account); |
| } |
| |
| @Override |
| public void run() { |
| Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); |
| |
| if (ENABLE_LOG) { |
| Log.d(TAG, "Thread started"); |
| } |
| |
| // Trace this sync instance. Note, conceptually this should be in |
| // SyncStorageEngine.insertStartSyncEvent(), but the trace functions require unique |
| // threads in order to track overlapping operations, so we'll do it here for now. |
| Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, mAuthority); |
| |
| SyncResult syncResult = new SyncResult(); |
| ContentProviderClient provider = null; |
| try { |
| if (isCanceled()) { |
| if (ENABLE_LOG) { |
| Log.d(TAG, "Already canceled"); |
| } |
| return; |
| } |
| if (ENABLE_LOG) { |
| Log.d(TAG, "Calling onPerformSync..."); |
| } |
| |
| provider = mContext.getContentResolver().acquireContentProviderClient(mAuthority); |
| if (provider != null) { |
| AbstractThreadedSyncAdapter.this.onPerformSync(mAccount, mExtras, |
| mAuthority, provider, syncResult); |
| } else { |
| syncResult.databaseError = true; |
| } |
| |
| if (ENABLE_LOG) { |
| Log.d(TAG, "onPerformSync done"); |
| } |
| |
| } catch (SecurityException e) { |
| if (ENABLE_LOG) { |
| Log.d(TAG, "SecurityException", e); |
| } |
| AbstractThreadedSyncAdapter.this.onSecurityException(mAccount, mExtras, |
| mAuthority, syncResult); |
| syncResult.databaseError = true; |
| } catch (RuntimeException | Error th) { |
| if (ENABLE_LOG) { |
| Log.d(TAG, "caught exception", th); |
| } |
| throw th; |
| } finally { |
| Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER); |
| |
| if (provider != null) { |
| provider.release(); |
| } |
| if (!isCanceled()) { |
| mSyncContext.onFinished(syncResult); |
| } |
| // synchronize so that the assignment will be seen by other threads |
| // that also synchronize accesses to mSyncThreads |
| synchronized (mSyncThreadLock) { |
| mSyncThreads.remove(mThreadsKey); |
| } |
| |
| if (ENABLE_LOG) { |
| Log.d(TAG, "Thread finished"); |
| } |
| } |
| } |
| |
| private boolean isCanceled() { |
| return Thread.currentThread().isInterrupted(); |
| } |
| } |
| |
| /** |
| * @return a reference to the IBinder of the SyncAdapter service. |
| */ |
| public final IBinder getSyncAdapterBinder() { |
| return mISyncAdapterImpl.asBinder(); |
| } |
| |
| /** |
| * Handle a call of onUnsyncableAccount. |
| * |
| * @param cb The callback to report the return value to |
| */ |
| private void handleOnUnsyncableAccount(@NonNull ISyncAdapterUnsyncableAccountCallback cb) { |
| boolean doSync; |
| try { |
| doSync = onUnsyncableAccount(); |
| } catch (RuntimeException e) { |
| Log.e(TAG, "Exception while calling onUnsyncableAccount, assuming 'true'", e); |
| doSync = true; |
| } |
| |
| try { |
| cb.onUnsyncableAccountDone(doSync); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Could not report result of onUnsyncableAccount", e); |
| } |
| } |
| |
| /** |
| * Allows to defer syncing until all accounts are properly set up. |
| * |
| * <p>Called when a account / authority pair |
| * <ul> |
| * <li>that can be handled by this adapter</li> |
| * <li>{@link ContentResolver#requestSync(SyncRequest) is synced}</li> |
| * <li>and the account/provider {@link ContentResolver#getIsSyncable(Account, String) has |
| * unknown state (<0)}.</li> |
| * </ul> |
| * |
| * <p>This might be called on a different service connection as {@link #onPerformSync}. |
| * |
| * <p>The system expects this method to immediately return. If the call stalls the system |
| * behaves as if this method returned {@code true}. If it is required to perform a longer task |
| * (such as interacting with the user), return {@code false} and proceed in a difference |
| * context, such as an {@link android.app.Activity}, or foreground service. The sync can then be |
| * rescheduled once the account becomes syncable. |
| * |
| * @return If {@code false} syncing is deferred. Returns {@code true} by default, i.e. by |
| * default syncing starts immediately. |
| */ |
| @MainThread |
| public boolean onUnsyncableAccount() { |
| return true; |
| } |
| |
| /** |
| * Perform a sync for this account. SyncAdapter-specific parameters may |
| * be specified in extras, which is guaranteed to not be null. Invocations |
| * of this method are guaranteed to be serialized. |
| * |
| * @param account the account that should be synced |
| * @param extras SyncAdapter-specific parameters |
| * @param authority the authority of this sync request |
| * @param provider a ContentProviderClient that points to the ContentProvider for this |
| * authority |
| * @param syncResult SyncAdapter-specific parameters |
| */ |
| public abstract void onPerformSync(Account account, Bundle extras, |
| String authority, ContentProviderClient provider, SyncResult syncResult); |
| |
| /** |
| * Report that there was a security exception when opening the content provider |
| * prior to calling {@link #onPerformSync}. This will be treated as a sync |
| * database failure. |
| * |
| * @param account the account that attempted to sync |
| * @param extras SyncAdapter-specific parameters |
| * @param authority the authority of the failed sync request |
| * @param syncResult SyncAdapter-specific parameters |
| */ |
| public void onSecurityException(Account account, Bundle extras, |
| String authority, SyncResult syncResult) { |
| } |
| |
| /** |
| * Indicates that a sync operation has been canceled. This will be invoked on a separate |
| * thread than the sync thread and so you must consider the multi-threaded implications |
| * of the work that you do in this method. |
| * <p> |
| * This will only be invoked when the SyncAdapter indicates that it doesn't support |
| * parallel syncs. |
| */ |
| public void onSyncCanceled() { |
| final SyncThread syncThread; |
| synchronized (mSyncThreadLock) { |
| syncThread = mSyncThreads.get(null); |
| } |
| if (syncThread != null) { |
| syncThread.interrupt(); |
| } |
| } |
| |
| /** |
| * Indicates that a sync operation has been canceled. This will be invoked on a separate |
| * thread than the sync thread and so you must consider the multi-threaded implications |
| * of the work that you do in this method. |
| * <p> |
| * This will only be invoked when the SyncAdapter indicates that it does support |
| * parallel syncs. |
| * @param thread the Thread of the sync that is to be canceled. |
| */ |
| public void onSyncCanceled(Thread thread) { |
| thread.interrupt(); |
| } |
| } |