517 lines
21 KiB
Java
517 lines
21 KiB
Java
/*
|
|
* 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.Binder;
|
|
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 {
|
|
private boolean isCallerSystem() {
|
|
final long callingUid = Binder.getCallingUid();
|
|
if (callingUid != Process.SYSTEM_UID) {
|
|
android.util.EventLog.writeEvent(0x534e4554, "203229608", -1, "");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void onUnsyncableAccount(ISyncAdapterUnsyncableAccountCallback cb) {
|
|
if (!isCallerSystem()) {
|
|
return;
|
|
}
|
|
Handler.getMain().sendMessage(obtainMessage(
|
|
AbstractThreadedSyncAdapter::handleOnUnsyncableAccount,
|
|
AbstractThreadedSyncAdapter.this, cb));
|
|
}
|
|
|
|
@Override
|
|
public void startSync(ISyncContext syncContext, String authority, Account account,
|
|
Bundle extras) {
|
|
if (!isCallerSystem()) {
|
|
return;
|
|
}
|
|
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) {
|
|
if (!isCallerSystem()) {
|
|
return;
|
|
}
|
|
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();
|
|
}
|
|
}
|