import static android.Manifest.permission.GET_ACCOUNTS;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.Size;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.IntentSender;
import android.content.res.Resources;
import android.database.SQLException;
import android.os.Build;
"android.accounts.AccountAuthenticator";
public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator";
+ /**
+ * Token for the special case where a UID has access only to an account
+ * but no authenticator specific auth tokens.
+ *
+ * @hide
+ */
+ public static final String ACCOUNT_ACCESS_TOKEN =
+ "com.android.abbfd278-af8b-415d-af8b-7571d5dab133";
+
private final Context mContext;
private final IAccountManager mService;
private final Handler mMainHandler;
}
}.start();
}
+
+ /**
+ * Gets whether a given package under a user has access to an account.
+ * Can be called only from the system UID.
+ *
+ * @param account The account for which to check.
+ * @param packageName The package for which to check.
+ * @param userHandle The user for which to check.
+ * @return True if the package can access the account.
+ *
+ * @hide
+ */
+ public boolean hasAccountAccess(@NonNull Account account, @NonNull String packageName,
+ @NonNull UserHandle userHandle) {
+ try {
+ return mService.hasAccountAccess(account, packageName, userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Creates an intent to request access to a given account for a UID.
+ * The returned intent should be stated for a result where {@link
+ * Activity#RESULT_OK} result means access was granted whereas {@link
+ * Activity#RESULT_CANCELED} result means access wasn't granted. Can
+ * be called only from the system UID.
+ *
+ * @param account The account for which to request.
+ * @param packageName The package name which to request.
+ * @param userHandle The user for which to request.
+ * @return The intent to request account access or null if the package
+ * doesn't exist.
+ *
+ * @hide
+ */
+ public IntentSender createRequestAccountAccessIntentSenderAsUser(@NonNull Account account,
+ @NonNull String packageName, @NonNull UserHandle userHandle) {
+ try {
+ return mService.createRequestAccountAccessIntentSenderAsUser(account, packageName,
+ userHandle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
--- /dev/null
+/*
+ * Copyright (C) 2016 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.accounts;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.os.RemoteCallback;
+
+/**
+ * Account manager local system service interface.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class AccountManagerInternal {
+
+ /**
+ * Requests that a given package is given access to an account.
+ * The provided callback will be invoked with a {@link android.os.Bundle}
+ * containing the result which will be a boolean value mapped to the
+ * {@link AccountManager#KEY_BOOLEAN_RESULT} key.
+ *
+ * @param account The account for which to request.
+ * @param packageName The package name for which to request.
+ * @param userId Concrete user id for which to request.
+ * @param callback A callback for receiving the result.
+ */
+ public abstract void requestAccountAccess(@NonNull Account account,
+ @NonNull String packageName, @IntRange(from = 0) int userId,
+ @NonNull RemoteCallback callback);
+}
*/
public class GrantCredentialsPermissionActivity extends Activity implements View.OnClickListener {
public static final String EXTRAS_ACCOUNT = "account";
- public static final String EXTRAS_AUTH_TOKEN_LABEL = "authTokenLabel";
public static final String EXTRAS_AUTH_TOKEN_TYPE = "authTokenType";
public static final String EXTRAS_RESPONSE = "response";
- public static final String EXTRAS_ACCOUNT_TYPE_LABEL = "accountTypeLabel";
- public static final String EXTRAS_PACKAGES = "application";
public static final String EXTRAS_REQUESTING_UID = "uid";
+
private Account mAccount;
private String mAuthTokenType;
private int mUid;
}
}
};
- AccountManager.get(this).getAuthTokenLabel(mAccount.type, mAuthTokenType, callback, null);
+
+ if (!AccountManager.ACCOUNT_ACCESS_TOKEN.equals(mAuthTokenType)) {
+ AccountManager.get(this).getAuthTokenLabel(mAccount.type,
+ mAuthTokenType, callback, null);
+ }
findViewById(R.id.allow_button).setOnClickListener(this);
findViewById(R.id.deny_button).setOnClickListener(this);
import android.accounts.IAccountManagerResponse;
import android.accounts.Account;
import android.accounts.AuthenticatorDescription;
+import android.content.IntentSender;
import android.os.Bundle;
-
+import android.os.RemoteCallback;
+import android.os.UserHandle;
/**
* Central application service that provides account management.
/* Check if credentials update is suggested */
void isCredentialsUpdateSuggested(in IAccountManagerResponse response, in Account account,
String statusToken);
+
+ /* Check if the package in a user can access an account */
+ boolean hasAccountAccess(in Account account, String packageName, in UserHandle userHandle);
+ /* Crate an intent to request account access for package and a given user id */
+ IntentSender createRequestAccountAccessIntentSenderAsUser(in Account account,
+ String packageName, in UserHandle userHandle);
}
package android.content;
+import android.annotation.Nullable;
import android.text.TextUtils;
import android.os.Parcelable;
import android.os.Parcel;
private final boolean isAlwaysSyncable;
private final boolean allowParallelSyncs;
private final String settingsActivity;
+ private final String packageName;
public SyncAdapterType(String authority, String accountType, boolean userVisible,
boolean supportsUploading) {
this.allowParallelSyncs = false;
this.settingsActivity = null;
this.isKey = false;
+ this.packageName = null;
}
/** @hide */
boolean supportsUploading,
boolean isAlwaysSyncable,
boolean allowParallelSyncs,
- String settingsActivity) {
+ String settingsActivity,
+ String packageName) {
if (TextUtils.isEmpty(authority)) {
throw new IllegalArgumentException("the authority must not be empty: " + authority);
}
this.allowParallelSyncs = allowParallelSyncs;
this.settingsActivity = settingsActivity;
this.isKey = false;
+ this.packageName = packageName;
}
private SyncAdapterType(String authority, String accountType) {
this.allowParallelSyncs = false;
this.settingsActivity = null;
this.isKey = true;
+ this.packageName = null;
}
public boolean supportsUploading() {
return settingsActivity;
}
+ /**
+ * The package hosting the sync adapter.
+ * @return The package name.
+ *
+ * @hide
+ */
+ public @Nullable String getPackageName() {
+ return packageName;
+ }
+
public static SyncAdapterType newKey(String authority, String accountType) {
return new SyncAdapterType(authority, accountType);
}
+ ", isAlwaysSyncable=" + isAlwaysSyncable
+ ", allowParallelSyncs=" + allowParallelSyncs
+ ", settingsActivity=" + settingsActivity
+ + ", packageName=" + packageName
+ "}";
}
}
dest.writeInt(isAlwaysSyncable ? 1 : 0);
dest.writeInt(allowParallelSyncs ? 1 : 0);
dest.writeString(settingsActivity);
+ dest.writeString(packageName);
}
public SyncAdapterType(Parcel source) {
source.readInt() != 0,
source.readInt() != 0,
source.readInt() != 0,
+ source.readString(),
source.readString());
}
sa.getString(com.android.internal.R.styleable
.SyncAdapter_settingsActivity);
return new SyncAdapterType(authority, accountType, userVisible, supportsUploading,
- isAlwaysSyncable, allowParallelSyncs, settingsActivity);
+ isAlwaysSyncable, allowParallelSyncs, settingsActivity, packageName);
} finally {
sa.recycle();
}
android:protectionLevel="dangerous"
android:description="@string/permdesc_getAccounts"
android:label="@string/permlab_getAccounts" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<!-- @SystemApi Allows applications to call into AccountAuthenticators.
<p>Not for use by third-party applications. -->
import android.accounts.AccountAndUser;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
+import android.accounts.AccountManagerInternal;
import android.accounts.AuthenticatorDescription;
import android.accounts.CantAddAccountActivity;
import android.accounts.GrantCredentialsPermissionActivity;
import android.accounts.IAccountAuthenticatorResponse;
import android.accounts.IAccountManager;
import android.accounts.IAccountManagerResponse;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityManagerNative;
+import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.AppOpsManager;
+import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.IntentSender;
import android.content.ServiceConnection;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Message;
import android.os.Parcel;
import android.os.Process;
+import android.os.RemoteCallback;
import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManager;
+import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
+ " AND " + ACCOUNTS_NAME + "=?"
+ " AND " + ACCOUNTS_TYPE + "=?";
+ private static final String COUNT_OF_MATCHING_GRANTS_ANY_TOKEN = ""
+ + "SELECT COUNT(*) FROM " + TABLE_GRANTS + ", " + TABLE_ACCOUNTS
+ + " WHERE " + GRANTS_ACCOUNTS_ID + "=" + ACCOUNTS_ID
+ + " AND " + GRANTS_GRANTEE_UID + "=?"
+ + " AND " + ACCOUNTS_NAME + "=?"
+ + " AND " + ACCOUNTS_TYPE + "=?";
+
private static final String SELECTION_AUTHTOKENS_BY_ACCOUNT =
AUTHTOKENS_ACCOUNTS_ID + "=(select _id FROM accounts WHERE name=? AND type=?)";
}
}
}, UserHandle.ALL, userFilter, null, null);
+
+ LocalServices.addService(AccountManagerInternal.class, new AccountManagerInternalImpl());
+
+ // Need to cancel account request notifications if the update/install can access the account
+ new PackageMonitor() {
+ @Override
+ public void onPackageAdded(String packageName, int uid) {
+ // Called on a handler, and running as the system
+ cancelAccountAccessRequestNotificationIfNeeded(uid, true);
+ }
+
+ @Override
+ public void onPackageUpdateFinished(String packageName, int uid) {
+ // Called on a handler, and running as the system
+ cancelAccountAccessRequestNotificationIfNeeded(uid, true);
+ }
+ }.register(mContext, mMessageHandler.getLooper(), UserHandle.ALL, true);
+
+ // Cancel account request notification if an app op was preventing the account access
+ mAppOpsManager.startWatchingMode(AppOpsManager.OP_GET_ACCOUNTS, null,
+ new AppOpsManager.OnOpChangedInternalListener() {
+ @Override
+ public void onOpChanged(int op, String packageName) {
+ try {
+ final int userId = ActivityManager.getCurrentUser();
+ final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
+ final int mode = mAppOpsManager.checkOpNoThrow(
+ AppOpsManager.OP_GET_ACCOUNTS, uid, packageName);
+ if (mode == AppOpsManager.MODE_ALLOWED) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ cancelAccountAccessRequestNotificationIfNeeded(packageName, uid, true);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ } catch (NameNotFoundException e) {
+ /* ignore */
+ }
+ }
+ });
+
+ // Cancel account request notification if a permission was preventing the account access
+ mPackageManager.addOnPermissionsChangeListener(
+ (int uid) -> {
+ Account[] accounts = null;
+ String[] packageNames = mPackageManager.getPackagesForUid(uid);
+ if (packageNames != null) {
+ final int userId = UserHandle.getUserId(uid);
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ for (String packageName : packageNames) {
+ if (mContext.getPackageManager().checkPermission(
+ Manifest.permission.GET_ACCOUNTS, packageName)
+ != PackageManager.PERMISSION_GRANTED) {
+ continue;
+ }
+
+ if (accounts == null) {
+ accounts = getAccountsAsUser(null, userId, "android");
+ if (ArrayUtils.isEmpty(accounts)) {
+ return;
+ }
+ }
+
+ for (Account account : accounts) {
+ cancelAccountAccessRequestNotificationIfNeeded(
+ account, uid, packageName, true);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ });
+ }
+
+ private void cancelAccountAccessRequestNotificationIfNeeded(int uid,
+ boolean checkAccess) {
+ Account[] accounts = getAccountsAsUser(null, UserHandle.getUserId(uid), "android");
+ for (Account account : accounts) {
+ cancelAccountAccessRequestNotificationIfNeeded(account, uid, checkAccess);
+ }
+ }
+
+ private void cancelAccountAccessRequestNotificationIfNeeded(String packageName, int uid,
+ boolean checkAccess) {
+ Account[] accounts = getAccountsAsUser(null, UserHandle.getUserId(uid), "android");
+ for (Account account : accounts) {
+ cancelAccountAccessRequestNotificationIfNeeded(account, uid, packageName, checkAccess);
+ }
+ }
+
+ private void cancelAccountAccessRequestNotificationIfNeeded(Account account, int uid,
+ boolean checkAccess) {
+ String[] packageNames = mPackageManager.getPackagesForUid(uid);
+ if (packageNames != null) {
+ for (String packageName : packageNames) {
+ cancelAccountAccessRequestNotificationIfNeeded(account, uid,
+ packageName, checkAccess);
+ }
+ }
+ }
+
+ private void cancelAccountAccessRequestNotificationIfNeeded(Account account,
+ int uid, String packageName, boolean checkAccess) {
+ if (!checkAccess || hasAccountAccess(account, packageName,
+ UserHandle.getUserHandleForUid(uid))) {
+ cancelNotification(getCredentialPermissionNotificationId(account,
+ AccountManager.ACCOUNT_ACCESS_TOKEN, uid), packageName,
+ UserHandle.getUserHandleForUid(uid));
+ }
}
@Override
} finally {
Binder.restoreCallingIdentity(id);
}
+
+ if (isChanged) {
+ synchronized (accounts.credentialsPermissionNotificationIds) {
+ for (Pair<Pair<Account, String>, Integer> key
+ : accounts.credentialsPermissionNotificationIds.keySet()) {
+ if (account.equals(key.first.first)
+ && AccountManager.ACCOUNT_ACCESS_TOKEN.equals(key.first.second)) {
+ final int uid = (Integer) key.second;
+ mMessageHandler.post(() -> cancelAccountAccessRequestNotificationIfNeeded(
+ account, uid, false));
+ }
+ }
+ }
+ }
+
return isChanged;
}
if (result.containsKey(AccountManager.KEY_AUTH_TOKEN_LABEL)) {
Intent intent = newGrantCredentialsPermissionIntent(
account,
+ null,
callerUid,
new AccountAuthenticatorResponse(this),
- authTokenType);
+ authTokenType,
+ true);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
onResult(bundle);
intent);
doNotification(mAccounts,
account, result.getString(AccountManager.KEY_AUTH_FAILED_MESSAGE),
- intent, accounts.userId);
+ intent, "android", accounts.userId);
}
}
super.onResult(result);
}
private void createNoCredentialsPermissionNotification(Account account, Intent intent,
- int userId) {
+ String packageName, int userId) {
int uid = intent.getIntExtra(
GrantCredentialsPermissionActivity.EXTRAS_REQUESTING_UID, -1);
String authTokenType = intent.getStringExtra(
PendingIntent.FLAG_CANCEL_CURRENT, null, user))
.build();
installNotification(getCredentialPermissionNotificationId(
- account, authTokenType, uid), n, user);
+ account, authTokenType, uid), n, packageName, user.getIdentifier());
}
- private Intent newGrantCredentialsPermissionIntent(Account account, int uid,
- AccountAuthenticatorResponse response, String authTokenType) {
+ private Intent newGrantCredentialsPermissionIntent(Account account, String packageName,
+ int uid, AccountAuthenticatorResponse response, String authTokenType,
+ boolean startInNewTask) {
Intent intent = new Intent(mContext, GrantCredentialsPermissionActivity.class);
- // See FLAG_ACTIVITY_NEW_TASK docs for limitations and benefits of the flag.
- // Since it was set in Eclair+ we can't change it without breaking apps using
- // the intent from a non-Activity context.
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.addCategory(
- String.valueOf(getCredentialPermissionNotificationId(account, authTokenType, uid)));
+ if (startInNewTask) {
+ // See FLAG_ACTIVITY_NEW_TASK docs for limitations and benefits of the flag.
+ // Since it was set in Eclair+ we can't change it without breaking apps using
+ // the intent from a non-Activity context. This is the default behavior.
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ intent.addCategory(String.valueOf(getCredentialPermissionNotificationId(account,
+ authTokenType, uid) + (packageName != null ? packageName : "")));
intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_ACCOUNT, account);
intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_AUTH_TOKEN_TYPE, authTokenType);
intent.putExtra(GrantCredentialsPermissionActivity.EXTRAS_RESPONSE, response);
}
@Override
+ public boolean hasAccountAccess(@NonNull Account account, @NonNull String packageName,
+ @NonNull UserHandle userHandle) {
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Can be called only by system UID");
+ }
+ Preconditions.checkNotNull(account, "account cannot be null");
+ Preconditions.checkNotNull(packageName, "packageName cannot be null");
+ Preconditions.checkNotNull(userHandle, "userHandle cannot be null");
+
+ final int userId = userHandle.getIdentifier();
+
+ Preconditions.checkArgumentInRange(userId, 0, Integer.MAX_VALUE, "user must be concrete");
+
+ try {
+
+ final int uid = mPackageManager.getPackageUidAsUser(packageName, userId);
+ // Use null token which means any token. Having a token means the package
+ // is trusted by the authenticator, hence it is fine to access the account.
+ if (permissionIsGranted(account, null, uid, userId)) {
+ return true;
+ }
+ // In addition to the permissions required to get an auth token we also allow
+ // the account to be accessed by holders of the get accounts permissions.
+ return checkUidPermission(Manifest.permission.GET_ACCOUNTS_PRIVILEGED, uid, packageName)
+ || checkUidPermission(Manifest.permission.GET_ACCOUNTS, uid, packageName);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private boolean checkUidPermission(String permission, int uid, String opPackageName) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ IPackageManager pm = ActivityThread.getPackageManager();
+ if (pm.checkUidPermission(permission, uid) != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ final int opCode = AppOpsManager.permissionToOpCode(permission);
+ return (opCode == AppOpsManager.OP_NONE || mAppOpsManager.noteOpNoThrow(
+ opCode, uid, opPackageName) == AppOpsManager.MODE_ALLOWED);
+ } catch (RemoteException e) {
+ /* ignore - local call */
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ return false;
+ }
+
+ @Override
+ public IntentSender createRequestAccountAccessIntentSenderAsUser(@NonNull Account account,
+ @NonNull String packageName, @NonNull UserHandle userHandle) {
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Can be called only by system UID");
+ }
+
+ Preconditions.checkNotNull(account, "account cannot be null");
+ Preconditions.checkNotNull(packageName, "packageName cannot be null");
+ Preconditions.checkNotNull(userHandle, "userHandle cannot be null");
+
+ final int userId = userHandle.getIdentifier();
+
+ Preconditions.checkArgumentInRange(userId, 0, Integer.MAX_VALUE, "user must be concrete");
+
+ final int uid;
+ try {
+ uid = mPackageManager.getPackageUidAsUser(packageName, userId);
+ } catch (NameNotFoundException e) {
+ Slog.e(TAG, "Unknown package " + packageName);
+ return null;
+ }
+
+ Intent intent = newRequestAccountAccessIntent(account, packageName, uid, null);
+
+ return PendingIntent.getActivityAsUser(
+ mContext, 0, intent, PendingIntent.FLAG_ONE_SHOT
+ | PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE,
+ null, new UserHandle(userId)).getIntentSender();
+ }
+
+ private Intent newRequestAccountAccessIntent(Account account, String packageName,
+ int uid, RemoteCallback callback) {
+ return newGrantCredentialsPermissionIntent(account, packageName, uid,
+ new AccountAuthenticatorResponse(new IAccountAuthenticatorResponse.Stub() {
+ @Override
+ public void onResult(Bundle value) throws RemoteException {
+ handleAuthenticatorResponse(true);
+ }
+
+ @Override
+ public void onRequestContinued() {
+ /* ignore */
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) throws RemoteException {
+ handleAuthenticatorResponse(false);
+ }
+
+ private void handleAuthenticatorResponse(boolean accessGranted) throws RemoteException {
+ cancelNotification(getCredentialPermissionNotificationId(account,
+ AccountManager.ACCOUNT_ACCESS_TOKEN, uid), packageName,
+ UserHandle.getUserHandleForUid(uid));
+ if (callback != null) {
+ Bundle result = new Bundle();
+ result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, accessGranted);
+ callback.sendResult(result);
+ }
+ }
+ }), AccountManager.ACCOUNT_ACCESS_TOKEN, false);
+ }
+
+ @Override
public boolean someUserHasAccount(@NonNull final Account account) {
if (!UserHandle.isSameApp(Process.SYSTEM_UID, Binder.getCallingUid())) {
throw new SecurityException("Only system can check for accounts across users");
}
private void doNotification(UserAccounts accounts, Account account, CharSequence message,
- Intent intent, int userId) {
+ Intent intent, String packageName, final int userId) {
long identityToken = clearCallingIdentity();
try {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
if (intent.getComponent() != null &&
GrantCredentialsPermissionActivity.class.getName().equals(
intent.getComponent().getClassName())) {
- createNoCredentialsPermissionNotification(account, intent, userId);
+ createNoCredentialsPermissionNotification(account, intent, packageName, userId);
} else {
+ Context contextForUser = getContextForUser(new UserHandle(userId));
final Integer notificationId = getSigninRequiredNotificationId(accounts, account);
intent.addCategory(String.valueOf(notificationId));
- UserHandle user = new UserHandle(userId);
- Context contextForUser = getContextForUser(user);
+
final String notificationTitleFormat =
contextForUser.getText(R.string.notification_title).toString();
Notification n = new Notification.Builder(contextForUser)
.setContentText(message)
.setContentIntent(PendingIntent.getActivityAsUser(
mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT,
- null, user))
+ null, new UserHandle(userId)))
.build();
- installNotification(notificationId, n, user);
+ installNotification(notificationId, n, packageName, userId);
}
} finally {
restoreCallingIdentity(identityToken);
}
@VisibleForTesting
- protected void installNotification(final int notificationId, final Notification n,
+ protected void installNotification(int notificationId, final Notification notification,
UserHandle user) {
- ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE))
- .notifyAsUser(null, notificationId, n, user);
+ installNotification(notificationId, notification, "android", user.getIdentifier());
+ }
+
+ private void installNotification(int notificationId, final Notification notification,
+ String packageName, int userId) {
+ final long token = clearCallingIdentity();
+ try {
+ INotificationManager notificationManager = NotificationManager.getService();
+ try {
+ notificationManager.enqueueNotificationWithTag(packageName, packageName, null,
+ notificationId, notification, new int[1], userId);
+ } catch (RemoteException e) {
+ /* ignore - local call */
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
}
@VisibleForTesting
protected void cancelNotification(int id, UserHandle user) {
+ cancelNotification(id, mContext.getPackageName(), user);
+ }
+
+ protected void cancelNotification(int id, String packageName, UserHandle user) {
long identityToken = clearCallingIdentity();
try {
- ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE))
- .cancelAsUser(null, id, user);
+ INotificationManager service = INotificationManager.Stub.asInterface(
+ ServiceManager.getService(Context.NOTIFICATION_SERVICE));
+ service.cancelNotificationWithTag(packageName, null, id, user.getIdentifier());
+ } catch (RemoteException e) {
+ /* ignore - local call */
} finally {
restoreCallingIdentity(identityToken);
}
private boolean permissionIsGranted(
Account account, String authTokenType, int callerUid, int userId) {
- final boolean isPrivileged = isPrivileged(callerUid);
- final boolean fromAuthenticator = account != null
- && isAccountManagedByCaller(account.type, callerUid, userId);
- final boolean hasExplicitGrants = account != null
- && hasExplicitlyGrantedPermission(account, authTokenType, callerUid);
+ if (UserHandle.getAppId(callerUid) == Process.SYSTEM_UID) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Access to " + account + " granted calling uid is system");
+ }
+ return true;
+ }
+
+ if (isPrivileged(callerUid)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Access to " + account + " granted calling uid "
+ + callerUid + " privileged");
+ }
+ return true;
+ }
+ if (account != null && isAccountManagedByCaller(account.type, callerUid, userId)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Access to " + account + " granted calling uid "
+ + callerUid + " manages the account");
+ }
+ return true;
+ }
+ if (account != null && hasExplicitlyGrantedPermission(account, authTokenType, callerUid)) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "Access to " + account + " granted calling uid "
+ + callerUid + " user granted access");
+ }
+ return true;
+ }
+
if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Log.v(TAG, "checkGrantsOrCallingUidAgainstAuthenticator: caller uid "
- + callerUid + ", " + account
- + ": is authenticator? " + fromAuthenticator
- + ", has explicit permission? " + hasExplicitGrants);
+ Log.v(TAG, "Access to " + account + " not granted for uid " + callerUid);
}
- return fromAuthenticator || hasExplicitGrants || isPrivileged;
+
+ return false;
}
private boolean isAccountVisibleToCaller(String accountType, int callingUid, int userId,
UserAccounts accounts = getUserAccountsForCaller();
synchronized (accounts.cacheLock) {
final SQLiteDatabase db = accounts.openHelper.getReadableDatabase();
- String[] args = { String.valueOf(callerUid), authTokenType,
- account.name, account.type};
- final boolean permissionGranted =
- DatabaseUtils.longForQuery(db, COUNT_OF_MATCHING_GRANTS, args) != 0;
+
+ final String query;
+ final String[] args;
+
+ if (authTokenType != null) {
+ query = COUNT_OF_MATCHING_GRANTS;
+ args = new String[] {String.valueOf(callerUid), authTokenType,
+ account.name, account.type};
+ } else {
+ query = COUNT_OF_MATCHING_GRANTS_ANY_TOKEN;
+ args = new String[] {String.valueOf(callerUid), account.name,
+ account.type};
+ }
+ final boolean permissionGranted = DatabaseUtils.longForQuery(db, query, args) != 0;
if (!permissionGranted && ActivityManager.isRunningInTestHarness()) {
// TODO: Skip this check when running automated tests. Replace this
// with a more general solution.
}
cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid),
UserHandle.of(accounts.userId));
+
+ cancelAccountAccessRequestNotificationIfNeeded(account, uid, true);
}
}
}
}
}
+
+ private final class AccountManagerInternalImpl extends AccountManagerInternal {
+ @Override
+ public void requestAccountAccess(@NonNull Account account, @NonNull String packageName,
+ @IntRange(from = 0) int userId, @NonNull RemoteCallback callback) {
+ if (account == null) {
+ Slog.w(TAG, "account cannot be null");
+ return;
+ }
+ if (packageName == null) {
+ Slog.w(TAG, "packageName cannot be null");
+ return;
+ }
+ if (userId < UserHandle.USER_SYSTEM) {
+ Slog.w(TAG, "user id must be concrete");
+ return;
+ }
+ if (callback == null) {
+ Slog.w(TAG, "callback cannot be null");
+ return;
+ }
+
+ if (hasAccountAccess(account, packageName, new UserHandle(userId))) {
+ Bundle result = new Bundle();
+ result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
+ callback.sendResult(result);
+ return;
+ }
+
+ final int uid;
+ try {
+ uid = mPackageManager.getPackageUidAsUser(packageName, userId);
+ } catch (NameNotFoundException e) {
+ Slog.e(TAG, "Unknown package " + packageName);
+ return;
+ }
+
+ Intent intent = newRequestAccountAccessIntent(account, packageName, uid, callback);
+ doNotification(mUsers.get(userId), account, null, intent, packageName, userId);
+ }
+ }
}
SyncManager syncManager = getSyncManager();
if (syncManager != null) {
syncManager.scheduleSync(account, userId, uId, authority, extras,
- 0 /* no delay */, 0 /* no delay */,
false /* onlyThoseWithUnkownSyncableState */);
}
} finally {
getSyncManager().updateOrAddPeriodicSync(info, runAtTime,
flextime, extras);
} else {
- long beforeRuntimeMillis = (flextime) * 1000;
- long runtimeMillis = runAtTime * 1000;
syncManager.scheduleSync(
request.getAccount(), userId, callerUid, request.getProvider(), extras,
- beforeRuntimeMillis, runtimeMillis,
false /* onlyThoseWithUnknownSyncableState */);
}
} finally {
try {
SyncManager syncManager = getSyncManager();
if (syncManager != null) {
- return syncManager.getIsSyncable(
+ return syncManager.computeSyncable(
account, userId, providerName);
}
} finally {
import android.accounts.Account;
import android.accounts.AccountAndUser;
import android.accounts.AccountManager;
+import android.accounts.AccountManagerInternal;
import android.app.ActivityManager;
import android.app.ActivityManagerNative;
import android.app.AppGlobals;
import android.os.Message;
import android.os.Messenger;
import android.os.PowerManager;
+import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.util.Pair;
import android.util.Slog;
+import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerInternal;
import com.google.android.collect.Lists;
public class SyncManager {
static final String TAG = "SyncManager";
+ private static final boolean DEBUG_ACCOUNT_ACCESS = false;
+
/** Delay a sync due to local changes this long. In milliseconds */
private static final long LOCAL_SYNC_DELAY;
private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarm";
private static final String SYNC_LOOP_WAKE_LOCK = "SyncLoopWakeLock";
+
+ private static final int SYNC_OP_STATE_VALID = 0;
+ private static final int SYNC_OP_STATE_INVALID = 1;
+ private static final int SYNC_OP_STATE_INVALID_NO_ACCOUNT_ACCESS = 2;
+
private Context mContext;
private static final AccountAndUser[] INITIAL_ACCOUNTS_ARRAY = new AccountAndUser[0];
private final UserManager mUserManager;
+ private final AccountManager mAccountManager;
+
+ private final AccountManagerInternal mAccountManagerInternal;
+
private List<UserInfo> getAllUsers() {
return mUserManager.getUsers();
}
@Override
public void onSyncRequest(SyncStorageEngine.EndPoint info, int reason, Bundle extras) {
scheduleSync(info.account, info.userId, reason, info.provider, extras,
- 0 /* no flexMillis */,
- 0 /* run immediately */,
false);
}
});
if (!removed) {
scheduleSync(null, UserHandle.USER_ALL,
SyncOperation.REASON_SERVICE_CHANGED,
- type.authority, null, 0 /* no delay */, 0 /* no delay */,
- false /* onlyThoseWithUnkownSyncableState */);
+ type.authority, null, false /* onlyThoseWithUnkownSyncableState */);
}
}
}, mSyncHandler);
}
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ mAccountManager = (AccountManager) mContext.getSystemService(Context.ACCOUNT_SERVICE);
+ mAccountManagerInternal = LocalServices.getService(AccountManagerInternal.class);
+
mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService(
BatteryStats.SERVICE_NAME));
return mSyncStorageEngine;
}
- public int getIsSyncable(Account account, int userId, String providerName) {
+ private int getIsSyncable(Account account, int userId, String providerName) {
int isSyncable = mSyncStorageEngine.getIsSyncable(account, userId, providerName);
UserInfo userInfo = UserManager.get(mContext).getUserInfo(userId);
RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo =
mSyncAdapters.getServiceInfo(
SyncAdapterType.newKey(providerName, account.type), userId);
- if (syncAdapterInfo == null) return isSyncable;
+ if (syncAdapterInfo == null) return AuthorityInfo.NOT_SYNCABLE;
PackageInfo pInfo = null;
try {
pInfo = AppGlobals.getPackageManager().getPackageInfo(
syncAdapterInfo.componentName.getPackageName(), 0, userId);
- if (pInfo == null) return isSyncable;
+ if (pInfo == null) return AuthorityInfo.NOT_SYNCABLE;
} catch (RemoteException re) {
// Shouldn't happen.
- return isSyncable;
+ return AuthorityInfo.NOT_SYNCABLE;
}
if (pInfo.restrictedAccountType != null
&& pInfo.restrictedAccountType.equals(account.type)) {
return isSyncable;
} else {
- return 0;
+ return AuthorityInfo.NOT_SYNCABLE;
}
}
* @param extras a Map of SyncAdapter-specific information to control
* syncs of a specific provider. Can be null. Is ignored
* if the url is null.
- * @param beforeRuntimeMillis milliseconds before runtimeMillis that this sync can run.
- * @param runtimeMillis maximum milliseconds in the future to wait before performing sync.
* @param onlyThoseWithUnkownSyncableState Only sync authorities that have unknown state.
*/
public void scheduleSync(Account requestedAccount, int userId, int reason,
- String requestedAuthority, Bundle extras, long beforeRuntimeMillis,
- long runtimeMillis, boolean onlyThoseWithUnkownSyncableState) {
+ String requestedAuthority, Bundle extras, boolean onlyThoseWithUnkownSyncableState) {
final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
if (extras == null) {
extras = new Bundle();
+ requestedAuthority);
}
- AccountAndUser[] accounts;
- if (requestedAccount != null && userId != UserHandle.USER_ALL) {
- accounts = new AccountAndUser[] { new AccountAndUser(requestedAccount, userId) };
+ AccountAndUser[] accounts = null;
+ if (requestedAccount != null) {
+ if (userId != UserHandle.USER_ALL) {
+ accounts = new AccountAndUser[]{new AccountAndUser(requestedAccount, userId)};
+ } else {
+ for (AccountAndUser runningAccount : mRunningAccounts) {
+ if (requestedAccount.equals(runningAccount.account)) {
+ accounts = ArrayUtils.appendElement(AccountAndUser.class,
+ accounts, runningAccount);
+ }
+ }
+ }
} else {
accounts = mRunningAccounts;
- if (accounts.length == 0) {
- if (isLoggable) {
- Slog.v(TAG, "scheduleSync: no accounts configured, dropping");
- }
- return;
+ }
+
+ if (ArrayUtils.isEmpty(accounts)) {
+ if (isLoggable) {
+ Slog.v(TAG, "scheduleSync: no accounts configured, dropping");
}
+ return;
}
final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
}
for (String authority : syncableAuthorities) {
- int isSyncable = getIsSyncable(account.account, account.userId,
- authority);
+ int isSyncable = computeSyncable(account.account, account.userId, authority);
+
if (isSyncable == AuthorityInfo.NOT_SYNCABLE) {
continue;
}
- final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo;
- syncAdapterInfo = mSyncAdapters.getServiceInfo(
- SyncAdapterType.newKey(authority, account.account.type), account.userId);
+
+ final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo =
+ mSyncAdapters.getServiceInfo(SyncAdapterType.newKey(authority,
+ account.account.type), account.userId);
if (syncAdapterInfo == null) {
continue;
}
+
final int owningUid = syncAdapterInfo.uid;
- final String owningPackage = syncAdapterInfo.componentName.getPackageName();
- try {
- if (ActivityManagerNative.getDefault().getAppStartMode(owningUid,
- owningPackage) == ActivityManager.APP_START_MODE_DISABLED) {
- Slog.w(TAG, "Not scheduling job " + syncAdapterInfo.uid + ":"
- + syncAdapterInfo.componentName
- + " -- package not allowed to start");
- continue;
+
+ if (isSyncable == AuthorityInfo.SYNCABLE_NO_ACCOUNT_ACCESS) {
+ if (isLoggable) {
+ Slog.v(TAG, " Not scheduling sync operation: "
+ + "isSyncable == SYNCABLE_NO_ACCOUNT_ACCESS");
}
- } catch (RemoteException e) {
+ Bundle finalExtras = new Bundle(extras);
+ mAccountManagerInternal.requestAccountAccess(account.account,
+ syncAdapterInfo.componentName.getPackageName(),
+ UserHandle.getUserId(owningUid),
+ new RemoteCallback((Bundle result) -> {
+ if (result != null
+ && result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) {
+ scheduleSync(account.account, userId, reason, authority,
+ finalExtras, onlyThoseWithUnkownSyncableState);
+ }
+ }
+ ));
+ continue;
}
+
final boolean allowParallelSyncs = syncAdapterInfo.type.allowParallelSyncs();
final boolean isAlwaysSyncable = syncAdapterInfo.type.isAlwaysSyncable();
if (isSyncable < 0 && isAlwaysSyncable) {
account.account, account.userId, authority, AuthorityInfo.SYNCABLE);
isSyncable = AuthorityInfo.SYNCABLE;
}
+
if (onlyThoseWithUnkownSyncableState && isSyncable >= 0) {
continue;
}
account.account, authority, account.userId);
long delayUntil =
mSyncStorageEngine.getDelayUntilTime(info);
+
+ final String owningPackage = syncAdapterInfo.componentName.getPackageName();
+
if (isSyncable < 0) {
// Initialisation sync.
Bundle newExtras = new Bundle();
if (isLoggable) {
Slog.v(TAG, "scheduleSync:"
+ " delay until " + delayUntil
- + " run by " + runtimeMillis
- + " flexMillis " + beforeRuntimeMillis
+ ", source " + source
+ ", account " + account
+ ", authority " + authority
}
}
+ public int computeSyncable(Account account, int userId, String authority) {
+ final int status = getIsSyncable(account, userId, authority);
+ if (status == AuthorityInfo.NOT_SYNCABLE) {
+ return AuthorityInfo.NOT_SYNCABLE;
+ }
+ final SyncAdapterType type = SyncAdapterType.newKey(authority, account.type);
+ final RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo =
+ mSyncAdapters.getServiceInfo(type, userId);
+ if (syncAdapterInfo == null) {
+ return AuthorityInfo.NOT_SYNCABLE;
+ }
+ final int owningUid = syncAdapterInfo.uid;
+ final String owningPackage = syncAdapterInfo.componentName.getPackageName();
+ try {
+ if (ActivityManagerNative.getDefault().getAppStartMode(owningUid,
+ owningPackage) == ActivityManager.APP_START_MODE_DISABLED) {
+ Slog.w(TAG, "Not scheduling job " + syncAdapterInfo.uid + ":"
+ + syncAdapterInfo.componentName
+ + " -- package not allowed to start");
+ return AuthorityInfo.NOT_SYNCABLE;
+ }
+ } catch (RemoteException e) {
+ /* ignore - local call */
+ }
+ if (!canAccessAccount(account, owningPackage, owningUid)) {
+ Log.w(TAG, "Access to " + account + " denied for package "
+ + owningPackage + " in UID " + syncAdapterInfo.uid);
+ return AuthorityInfo.SYNCABLE_NO_ACCOUNT_ACCESS;
+ }
+
+ return status;
+ }
+
+ private boolean canAccessAccount(Account account, String packageName, int uid) {
+ if (mAccountManager.hasAccountAccess(account, packageName,
+ UserHandle.getUserHandleForUid(uid))) {
+ return true;
+ }
+ // We relax the account access rule to also include the system apps as
+ // they are trusted and we want to minimize the cases where the user
+ // involvement is required to grant access to the synced account.
+ try {
+ mContext.getPackageManager().getApplicationInfoAsUser(packageName,
+ PackageManager.MATCH_SYSTEM_ONLY, UserHandle.getUserId(uid));
+ return true;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
private void removeSyncsForAuthority(EndPoint info) {
verifyJobScheduler();
List<SyncOperation> ops = getAllPendingSyncs();
final Bundle extras = new Bundle();
extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true);
scheduleSync(account, userId, reason, authority, extras,
- LOCAL_SYNC_DELAY /* earliest run time */,
- 2 * LOCAL_SYNC_DELAY /* latest sync time. */,
false /* onlyThoseWithUnkownSyncableState */);
}
mContext.getOpPackageName());
for (Account account : accounts) {
scheduleSync(account, userId, SyncOperation.REASON_USER_START, null, null,
- 0 /* no delay */, 0 /* No flexMillis */,
true /* onlyThoseWithUnknownSyncableState */);
}
}
}
}
- if (isOperationValid(op)) {
- if (!dispatchSyncOperation(op)) {
+ final int syncOpState = computeSyncOpState(op);
+ switch (syncOpState) {
+ case SYNC_OP_STATE_INVALID_NO_ACCOUNT_ACCESS:
+ case SYNC_OP_STATE_INVALID: {
mSyncJobService.callJobFinished(op.jobId, false);
- }
- } else {
+ } return;
+ }
+
+ if (!dispatchSyncOperation(op)) {
mSyncJobService.callJobFinished(op.jobId, false);
}
+
setAuthorityPendingState(op.target);
}
if (syncTargets != null) {
scheduleSync(syncTargets.account, syncTargets.userId,
- SyncOperation.REASON_ACCOUNTS_UPDATED, syncTargets.provider, null, 0, 0,
- true);
+ SyncOperation.REASON_ACCOUNTS_UPDATED, syncTargets.provider, null, true);
}
}
SyncStorageEngine.SOURCE_PERIODIC, extras,
syncAdapterInfo.type.allowParallelSyncs(), true, SyncOperation.NO_JOB_ID,
pollFrequencyMillis, flexMillis);
+
+ final int syncOpState = computeSyncOpState(op);
+ switch (syncOpState) {
+ case SYNC_OP_STATE_INVALID_NO_ACCOUNT_ACCESS: {
+ mAccountManagerInternal.requestAccountAccess(op.target.account,
+ op.owningPackage, UserHandle.getUserId(op.owningUid),
+ new RemoteCallback((Bundle result) -> {
+ if (result != null
+ && result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)) {
+ updateOrAddPeriodicSync(target, pollFrequency, flex, extras);
+ }
+ }
+ ));
+ } return;
+
+ case SYNC_OP_STATE_INVALID: {
+ return;
+ }
+ }
+
scheduleSyncOperationH(op);
mSyncStorageEngine.reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
}
/**
* Determine if a sync is no longer valid and should be dropped.
*/
- private boolean isOperationValid(SyncOperation op) {
+ private int computeSyncOpState(SyncOperation op) {
final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
int state;
final EndPoint target = op.target;
- boolean syncEnabled = mSyncStorageEngine.getMasterSyncAutomatically(target.userId);
+
// Drop the sync if the account of this operation no longer exists.
AccountAndUser[] accounts = mRunningAccounts;
if (!containsAccountAndUser(accounts, target.account, target.userId)) {
if (isLoggable) {
Slog.v(TAG, " Dropping sync operation: account doesn't exist.");
}
- return false;
+ return SYNC_OP_STATE_INVALID;
}
// Drop this sync request if it isn't syncable.
- state = getIsSyncable(target.account, target.userId, target.provider);
- if (state == 0) {
+ state = computeSyncable(target.account, target.userId, target.provider);
+ if (state == AuthorityInfo.SYNCABLE_NO_ACCOUNT_ACCESS) {
if (isLoggable) {
- Slog.v(TAG, " Dropping sync operation: isSyncable == 0.");
+ Slog.v(TAG, " Dropping sync operation: "
+ + "isSyncable == SYNCABLE_NO_ACCOUNT_ACCESS");
}
- return false;
+ return SYNC_OP_STATE_INVALID_NO_ACCOUNT_ACCESS;
}
- syncEnabled = syncEnabled && mSyncStorageEngine.getSyncAutomatically(
- target.account, target.userId, target.provider);
+ if (state != AuthorityInfo.SYNCABLE) {
+ if (isLoggable) {
+ Slog.v(TAG, " Dropping sync operation: isSyncable != SYNCABLE");
+ }
+ return SYNC_OP_STATE_INVALID;
+ }
+
+ final boolean syncEnabled = mSyncStorageEngine.getMasterSyncAutomatically(target.userId)
+ && mSyncStorageEngine.getSyncAutomatically(target.account,
+ target.userId, target.provider);
// We ignore system settings that specify the sync is invalid if:
// 1) It's manual - we try it anyway. When/if it fails it will be rescheduled.
if (isLoggable) {
Slog.v(TAG, " Dropping sync operation: disallowed by settings/network.");
}
- return false;
+ return SYNC_OP_STATE_INVALID;
}
- return true;
+ return SYNC_OP_STATE_VALID;
}
private boolean dispatchSyncOperation(SyncOperation op) {
*/
public static final int SYNCABLE_NOT_INITIALIZED = 2;
+ /**
+ * The adapter is syncable but does not have access to the synced account and needs a
+ * user access approval.
+ */
+ public static final int SYNCABLE_NO_ACCOUNT_ACCESS = 3;
+
final EndPoint target;
final int ident;
boolean enabled;