Java tutorial
/* * 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 initialize 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(); } }