Skip to content
Snippets Groups Projects
Commit 486b2417 authored by Philip P. Moltmann's avatar Philip P. Moltmann
Browse files

Add AbstractThreadedSyncAdapter#onUnsyncableAccount API

This allows for an SyncAdapter to defer all onPerformSync calls (and the
admission check for those) until the adapter is ready to accept those
calls.

Test: atest android.content.cts.AccountAccessSameCertTest
      atest android.content.cts.ContentResolverSyncTestCase
      atest android.content.cts.DeferSyncTest
      atest com.android.cts.content.CtsSyncAccountAccessOtherCertTestCases
      Set up a user and work account and waited until their gmail
      synced.

Bug: 72459220
Change-Id: I8f9ab735d64189578ccdd1c3e1d7b7c5383b8ee9
parent bdd460bf
No related branches found
No related tags found
No related merge requests found
......@@ -113,6 +113,7 @@ java_library {
"core/java/android/content/IOnPrimaryClipChangedListener.aidl",
"core/java/android/content/IRestrictionsManager.aidl",
"core/java/android/content/ISyncAdapter.aidl",
"core/java/android/content/ISyncAdapterUnsyncableAccountCallback.aidl",
"core/java/android/content/ISyncContext.aidl",
"core/java/android/content/ISyncServiceAdapter.aidl",
"core/java/android/content/ISyncStatusObserver.aidl",
......
......@@ -8759,6 +8759,7 @@ package android.content {
method public void onSecurityException(android.accounts.Account, android.os.Bundle, java.lang.String, android.content.SyncResult);
method public void onSyncCanceled();
method public void onSyncCanceled(java.lang.Thread);
method public boolean onUnsyncableAccount();
field public static final deprecated int LOG_SYNC_DETAILS = 2743; // 0xab7
}
 
......@@ -21,6 +21,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.Trace;
import android.util.Log;
......@@ -165,6 +166,12 @@ public abstract class AbstractThreadedSyncAdapter {
}
private class ISyncAdapterImpl extends ISyncAdapter.Stub {
@Override
public void onUnsyncableAccount(ISyncAdapterUnsyncableAccountCallback cb)
throws RemoteException {
cb.onUnsyncableAccountDone(AbstractThreadedSyncAdapter.this.onUnsyncableAccount());
}
@Override
public void startSync(ISyncContext syncContext, String authority, Account account,
Bundle extras) {
......@@ -373,6 +380,26 @@ public abstract class AbstractThreadedSyncAdapter {
return mISyncAdapterImpl.asBinder();
}
/**
* 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}.
*
* @return If {@code false} syncing is deferred. Returns {@code true} by default, i.e. by
* default syncing starts immediately.
*/
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
......
......@@ -19,12 +19,21 @@ package android.content;
import android.accounts.Account;
import android.os.Bundle;
import android.content.ISyncContext;
import android.content.ISyncAdapterUnsyncableAccountCallback;
/**
* Interface used to control the sync activity on a SyncAdapter
* @hide
*/
oneway interface ISyncAdapter {
/**
* Called before {@link #startSync}. This allows the adapter to defer syncs until the
* adapter is ready for the account
*
* @param cb If called back with {@code false} accounts are not synced.
*/
void onUnsyncableAccount(ISyncAdapterUnsyncableAccountCallback cb);
/**
* Initiate a sync for this account. SyncAdapter-specific parameters may
* be specified in extras, which is guaranteed to not be null.
......
/*
* Copyright (C) 2018 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;
/**
* Callback for {@link ISyncAdapter#onUnsyncableAccount}
* @hide
*/
oneway interface ISyncAdapterUnsyncableAccountCallback {
/**
* Deliver the result for {@link ISyncAdapter#onUnsyncableAccount}
*
* @param isReady Iff {@code false} account is not synced.
*/
void onUnsyncableAccountDone(boolean isReady);
}
......@@ -20,6 +20,8 @@ import android.accounts.Account;
import android.accounts.AccountAndUser;
import android.accounts.AccountManager;
import android.accounts.AccountManagerInternal;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.app.Notification;
......@@ -32,6 +34,7 @@ import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ISyncAdapter;
import android.content.ISyncAdapterUnsyncableAccountCallback;
import android.content.ISyncContext;
import android.content.Intent;
import android.content.IntentFilter;
......@@ -212,6 +215,10 @@ public class SyncManager {
private static final int SYNC_OP_STATE_INVALID = 1;
private static final int SYNC_OP_STATE_INVALID_NO_ACCOUNT_ACCESS = 2;
/** Flags used when connecting to a sync adapter service */
private static final int SYNC_ADAPTER_CONNECTION_FLAGS = Context.BIND_AUTO_CREATE
| Context.BIND_NOT_FOREGROUND | Context.BIND_ALLOW_OOM_MANAGEMENT;
private Context mContext;
private static final AccountAndUser[] INITIAL_ACCOUNTS_ARRAY = new AccountAndUser[0];
......@@ -876,7 +883,7 @@ public class SyncManager {
public void scheduleSync(Account requestedAccount, int userId, int reason,
String requestedAuthority, Bundle extras, int targetSyncState) {
scheduleSync(requestedAccount, userId, reason, requestedAuthority, extras, targetSyncState,
0 /* min delay */);
0 /* min delay */, true /* checkIfAccountReady */);
}
/**
......@@ -884,7 +891,7 @@ public class SyncManager {
*/
private void scheduleSync(Account requestedAccount, int userId, int reason,
String requestedAuthority, Bundle extras, int targetSyncState,
final long minDelayMillis) {
final long minDelayMillis, boolean checkIfAccountReady) {
final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
if (extras == null) {
extras = new Bundle();
......@@ -963,7 +970,8 @@ public class SyncManager {
}
for (String authority : syncableAuthorities) {
int isSyncable = computeSyncable(account.account, account.userId, authority);
int isSyncable = computeSyncable(account.account, account.userId, authority,
!checkIfAccountReady);
if (isSyncable == AuthorityInfo.NOT_SYNCABLE) {
continue;
......@@ -1000,7 +1008,8 @@ public class SyncManager {
if (result != null
&& result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) {
scheduleSync(account.account, userId, reason, authority,
finalExtras, targetSyncState, minDelayMillis);
finalExtras, targetSyncState, minDelayMillis,
true /* checkIfAccountReady */);
}
}
));
......@@ -1009,7 +1018,7 @@ public class SyncManager {
final boolean allowParallelSyncs = syncAdapterInfo.type.allowParallelSyncs();
final boolean isAlwaysSyncable = syncAdapterInfo.type.isAlwaysSyncable();
if (isSyncable < 0 && isAlwaysSyncable) {
if (!checkIfAccountReady && isSyncable < 0 && isAlwaysSyncable) {
mSyncStorageEngine.setIsSyncable(
account.account, account.userId, authority, AuthorityInfo.SYNCABLE);
isSyncable = AuthorityInfo.SYNCABLE;
......@@ -1045,25 +1054,34 @@ public class SyncManager {
final String owningPackage = syncAdapterInfo.componentName.getPackageName();
if (isSyncable == AuthorityInfo.NOT_INITIALIZED) {
// Initialisation sync.
Bundle newExtras = new Bundle();
newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
if (isLoggable) {
Slog.v(TAG, "schedule initialisation Sync:"
+ ", delay until " + delayUntil
+ ", run by " + 0
+ ", flexMillis " + 0
+ ", source " + source
+ ", account " + account
+ ", authority " + authority
+ ", extras " + newExtras);
if (checkIfAccountReady) {
Bundle finalExtras = new Bundle(extras);
sendOnUnsyncableAccount(mContext, syncAdapterInfo, account.userId,
() -> scheduleSync(account.account, account.userId, reason,
authority, finalExtras, targetSyncState, minDelayMillis,
false));
} else {
// Initialisation sync.
Bundle newExtras = new Bundle();
newExtras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true);
if (isLoggable) {
Slog.v(TAG, "schedule initialisation Sync:"
+ ", delay until " + delayUntil
+ ", run by " + 0
+ ", flexMillis " + 0
+ ", source " + source
+ ", account " + account
+ ", authority " + authority
+ ", extras " + newExtras);
}
postScheduleSyncMessage(
new SyncOperation(account.account, account.userId,
owningUid, owningPackage, reason, source,
authority, newExtras, allowParallelSyncs),
minDelayMillis
);
}
postScheduleSyncMessage(
new SyncOperation(account.account, account.userId,
owningUid, owningPackage, reason, source,
authority, newExtras, allowParallelSyncs),
minDelayMillis
);
} else if (targetSyncState == AuthorityInfo.UNDEFINED
|| targetSyncState == isSyncable) {
if (isLoggable) {
......@@ -1085,10 +1103,6 @@ public class SyncManager {
}
}
private int computeSyncable(Account account, int userId, String authority) {
return computeSyncable(account, userId, authority, true);
}
public int computeSyncable(Account account, int userId, String authority,
boolean checkAccountAccess) {
final int status = getIsSyncable(account, userId, authority);
......@@ -1198,7 +1212,7 @@ public class SyncManager {
final Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
scheduleSync(account, userId, reason, authority, extras,
AuthorityInfo.UNDEFINED, LOCAL_SYNC_DELAY);
AuthorityInfo.UNDEFINED, LOCAL_SYNC_DELAY, true /* checkIfAccountReady */);
}
public SyncAdapterType[] getSyncAdapterTypes(int userId) {
......@@ -1705,6 +1719,28 @@ public class SyncManager {
}
}
/**
* Construct intent used to bind to an adapter.
*
* @param context Context to create intent for
* @param syncAdapterComponent The adapter description
* @param userId The user the adapter belongs to
*
* @return The intent required to bind to the adapter
*/
static @NonNull Intent getAdapterBindIntent(@NonNull Context context,
@NonNull ComponentName syncAdapterComponent, @UserIdInt int userId) {
final Intent intent = new Intent();
intent.setAction("android.content.SyncAdapter");
intent.setComponent(syncAdapterComponent);
intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.sync_binding_label);
intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivityAsUser(context, 0,
new Intent(Settings.ACTION_SYNC_SETTINGS), 0, null, UserHandle.of(userId)));
return intent;
}
/**
* @hide
*/
......@@ -1793,19 +1829,11 @@ public class SyncManager {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.d(TAG, "bindToSyncAdapter: " + serviceComponent + ", connection " + this);
}
Intent intent = new Intent();
intent.setAction("android.content.SyncAdapter");
intent.setComponent(serviceComponent);
intent.putExtra(Intent.EXTRA_CLIENT_LABEL,
com.android.internal.R.string.sync_binding_label);
intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivityAsUser(
mContext, 0, new Intent(Settings.ACTION_SYNC_SETTINGS), 0,
null, new UserHandle(userId)));
Intent intent = getAdapterBindIntent(mContext, serviceComponent, userId);
mBound = true;
final boolean bindResult = mContext.bindServiceAsUser(intent, this,
Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
| Context.BIND_ALLOW_OOM_MANAGEMENT,
new UserHandle(mSyncOperation.target.userId));
SYNC_ADAPTER_CONNECTION_FLAGS, new UserHandle(mSyncOperation.target.userId));
mLogger.log("bindService() returned=", mBound, " for ", this);
if (!bindResult) {
mBound = false;
......@@ -2528,6 +2556,92 @@ public class SyncManager {
}
}
interface OnReadyCallback {
void onReady();
}
static void sendOnUnsyncableAccount(@NonNull Context context,
@NonNull RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo,
@UserIdInt int userId, @NonNull OnReadyCallback onReadyCallback) {
OnUnsyncableAccountCheck connection = new OnUnsyncableAccountCheck(syncAdapterInfo,
onReadyCallback);
boolean isBound = context.bindServiceAsUser(
getAdapterBindIntent(context, syncAdapterInfo.componentName, userId),
connection, SYNC_ADAPTER_CONNECTION_FLAGS, UserHandle.of(userId));
if (isBound) {
// Unbind after SERVICE_BOUND_TIME_MILLIS to not leak the connection.
(new Handler(Looper.getMainLooper())).postDelayed(
() -> context.unbindService(connection),
OnUnsyncableAccountCheck.SERVICE_BOUND_TIME_MILLIS);
} else {
/*
* The default implementation of adapter.onUnsyncableAccount returns true. Hence if
* there the service cannot be bound, assume the default behavior.
*/
connection.onReady();
}
}
/**
* Helper class for calling ISyncAdapter.onUnsyncableAccountDone.
*
* If this returns {@code true} the onReadyCallback is called. Otherwise nothing happens.
*/
private static class OnUnsyncableAccountCheck implements ServiceConnection {
static final long SERVICE_BOUND_TIME_MILLIS = 5000;
private final @NonNull OnReadyCallback mOnReadyCallback;
private final @NonNull RegisteredServicesCache.ServiceInfo<SyncAdapterType>
mSyncAdapterInfo;
OnUnsyncableAccountCheck(
@NonNull RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo,
@NonNull OnReadyCallback onReadyCallback) {
mSyncAdapterInfo = syncAdapterInfo;
mOnReadyCallback = onReadyCallback;
}
private void onReady() {
long identity = Binder.clearCallingIdentity();
try {
mOnReadyCallback.onReady();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
final ISyncAdapter adapter = ISyncAdapter.Stub.asInterface(service);
try {
adapter.onUnsyncableAccount(new ISyncAdapterUnsyncableAccountCallback.Stub() {
@Override
public void onUnsyncableAccountDone(boolean isReady) {
if (isReady) {
onReady();
}
}
});
} catch (RemoteException e) {
Slog.e(TAG, "Could not call onUnsyncableAccountDone " + mSyncAdapterInfo, e);
/*
* The default implementation of adapter.onUnsyncableAccount returns true. Hence if
* there is a crash in the implementation, assume the default behavior.
*/
onReady();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
// Wait until the service connects again
}
}
/**
* A helper object to keep track of the time we have spent syncing since the last boot
*/
......@@ -3199,7 +3313,7 @@ public class SyncManager {
return SYNC_OP_STATE_INVALID;
}
// Drop this sync request if it isn't syncable.
state = computeSyncable(target.account, target.userId, target.provider);
state = computeSyncable(target.account, target.userId, target.provider, true);
if (state == AuthorityInfo.SYNCABLE_NO_ACCOUNT_ACCESS) {
if (isLoggable) {
Slog.v(TAG, " Dropping sync operation: "
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment