OSDN Git Service

Only sync adapters with access can see an account - framework
authorSvetoslav Ganov <svetoslavganov@google.com>
Tue, 12 Jul 2016 02:32:30 +0000 (19:32 -0700)
committerSvetoslav Ganov <svetoslavganov@google.com>
Tue, 23 Aug 2016 21:43:27 +0000 (21:43 +0000)
It was possible for a sync adapter without accounts access to
see the account which it is supposed to sync which can be used to
identify the user. This change ensures that only sync adapters
with account access can run (which results in seeing the account),
otherwise we involve the user to approve access only to this account.

A sync adapter can access an account if one of these is true:
 - it is signed as the authenticator for this account
 - has the GET_ACCOUNTS permission
 - has an auth token for the account
 - it is a preinstalled app (system or privileged)

The main thing we need to figure out is if the extra prompts
for giving access to a sync adapter to the account create too
much friction.

bug:28163381

Change-Id: Ie083bb681b5a2aed81ca5f6a062193a175fad77e

core/java/android/accounts/AccountManager.java
core/java/android/accounts/AccountManagerInternal.java [new file with mode: 0644]
core/java/android/accounts/GrantCredentialsPermissionActivity.java
core/java/android/accounts/IAccountManager.aidl
core/java/android/content/SyncAdapterType.java
core/java/android/content/SyncAdaptersCache.java
core/res/AndroidManifest.xml
services/core/java/com/android/server/accounts/AccountManagerService.java
services/core/java/com/android/server/content/ContentService.java
services/core/java/com/android/server/content/SyncManager.java
services/core/java/com/android/server/content/SyncStorageEngine.java

index 7465ed9..ed08a70 100644 (file)
@@ -18,6 +18,7 @@ package android.accounts;
 
 import static android.Manifest.permission.GET_ACCOUNTS;
 
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.annotation.Size;
@@ -28,6 +29,7 @@ import android.content.ComponentName;
 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;
@@ -265,6 +267,15 @@ public class AccountManager {
             "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;
@@ -2960,4 +2971,49 @@ public class AccountManager {
             }
         }.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();
+        }
+    }
 }
diff --git a/core/java/android/accounts/AccountManagerInternal.java b/core/java/android/accounts/AccountManagerInternal.java
new file mode 100644 (file)
index 0000000..d777643
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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);
+}
index 12b2b9c..8d0ce58 100644 (file)
@@ -35,12 +35,10 @@ import java.io.IOException;
  */
 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;
@@ -109,7 +107,11 @@ public class GrantCredentialsPermissionActivity extends Activity implements View
                 }
             }
         };
-        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);
index 7199288..56a6488 100644 (file)
@@ -19,8 +19,10 @@ package android.accounts;
 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.
@@ -102,4 +104,10 @@ interface IAccountManager {
     /* 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);
 }
index 8a16ac9..6ef7fd2 100644 (file)
@@ -16,6 +16,7 @@
 
 package android.content;
 
+import android.annotation.Nullable;
 import android.text.TextUtils;
 import android.os.Parcelable;
 import android.os.Parcel;
@@ -33,6 +34,7 @@ public class SyncAdapterType implements Parcelable {
     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) {
@@ -50,6 +52,7 @@ public class SyncAdapterType implements Parcelable {
         this.allowParallelSyncs = false;
         this.settingsActivity = null;
         this.isKey = false;
+        this.packageName = null;
     }
 
     /** @hide */
@@ -57,7 +60,8 @@ public class SyncAdapterType implements Parcelable {
             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);
         }
@@ -72,6 +76,7 @@ public class SyncAdapterType implements Parcelable {
         this.allowParallelSyncs = allowParallelSyncs;
         this.settingsActivity = settingsActivity;
         this.isKey = false;
+        this.packageName = packageName;
     }
 
     private SyncAdapterType(String authority, String accountType) {
@@ -89,6 +94,7 @@ public class SyncAdapterType implements Parcelable {
         this.allowParallelSyncs = false;
         this.settingsActivity = null;
         this.isKey = true;
+        this.packageName = null;
     }
 
     public boolean supportsUploading() {
@@ -148,6 +154,16 @@ public class SyncAdapterType implements Parcelable {
         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);
     }
@@ -181,6 +197,7 @@ public class SyncAdapterType implements Parcelable {
                     + ", isAlwaysSyncable=" + isAlwaysSyncable
                     + ", allowParallelSyncs=" + allowParallelSyncs
                     + ", settingsActivity=" + settingsActivity
+                    + ", packageName=" + packageName
                     + "}";
         }
     }
@@ -201,6 +218,7 @@ public class SyncAdapterType implements Parcelable {
         dest.writeInt(isAlwaysSyncable ? 1 : 0);
         dest.writeInt(allowParallelSyncs ? 1 : 0);
         dest.writeString(settingsActivity);
+        dest.writeString(packageName);
     }
 
     public SyncAdapterType(Parcel source) {
@@ -211,6 +229,7 @@ public class SyncAdapterType implements Parcelable {
                 source.readInt() != 0,
                 source.readInt() != 0,
                 source.readInt() != 0,
+                source.readString(),
                 source.readString());
     }
 
index 6704b75..ddbdb8a 100644 (file)
@@ -81,7 +81,7 @@ public class SyncAdaptersCache extends RegisteredServicesCache<SyncAdapterType>
                     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();
         }
index 3339d02..8bdb55e 100644 (file)
         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. -->
index 9108acf..5055562 100644 (file)
@@ -22,6 +22,7 @@ import android.accounts.Account;
 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;
@@ -29,11 +30,14 @@ import android.accounts.IAccountAuthenticator;
 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;
@@ -46,9 +50,11 @@ import android.content.ContentValues;
 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;
@@ -72,11 +78,14 @@ import android.os.Looper;
 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;
@@ -86,6 +95,7 @@ import android.util.SparseBooleanArray;
 
 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;
@@ -237,6 +247,13 @@ public class AccountManagerService
             + " 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=?)";
 
@@ -376,6 +393,118 @@ public class AccountManagerService
                 }
             }
         }, 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
@@ -1723,6 +1852,21 @@ public class AccountManagerService
         } 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;
     }
 
@@ -2319,9 +2463,11 @@ public class AccountManagerService
                         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);
@@ -2372,7 +2518,7 @@ public class AccountManagerService
                                     intent);
                             doNotification(mAccounts,
                                     account, result.getString(AccountManager.KEY_AUTH_FAILED_MESSAGE),
-                                    intent, accounts.userId);
+                                    intent, "android", accounts.userId);
                         }
                     }
                     super.onResult(result);
@@ -2403,7 +2549,7 @@ public class AccountManagerService
     }
 
     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(
@@ -2431,20 +2577,23 @@ public class AccountManagerService
                         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);
@@ -3295,6 +3444,118 @@ public class AccountManagerService
     }
 
     @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");
@@ -4934,7 +5195,7 @@ public class AccountManagerService
     }
 
     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)) {
@@ -4944,12 +5205,12 @@ public class AccountManagerService
             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)
@@ -4961,9 +5222,9 @@ public class AccountManagerService
                         .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);
@@ -4971,18 +5232,40 @@ public class AccountManagerService
     }
 
     @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);
         }
@@ -5043,18 +5326,40 @@ public class AccountManagerService
 
     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,
@@ -5144,10 +5449,20 @@ public class AccountManagerService
         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.
@@ -5288,6 +5603,8 @@ public class AccountManagerService
             }
             cancelNotification(getCredentialPermissionNotificationId(account, authTokenType, uid),
                     UserHandle.of(accounts.userId));
+
+            cancelAccountAccessRequestNotificationIfNeeded(account, uid, true);
         }
     }
 
@@ -5605,4 +5922,45 @@ public class AccountManagerService
             }
         }
     }
+
+    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);
+        }
+    }
 }
index 01b2393..12955f5 100644 (file)
@@ -482,7 +482,6 @@ public final class ContentService extends IContentService.Stub {
             SyncManager syncManager = getSyncManager();
             if (syncManager != null) {
                 syncManager.scheduleSync(account, userId, uId, authority, extras,
-                        0 /* no delay */, 0 /* no delay */,
                         false /* onlyThoseWithUnkownSyncableState */);
             }
         } finally {
@@ -547,11 +546,8 @@ public final class ContentService extends IContentService.Stub {
                 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 {
@@ -841,7 +837,7 @@ public final class ContentService extends IContentService.Stub {
         try {
             SyncManager syncManager = getSyncManager();
             if (syncManager != null) {
-                return syncManager.getIsSyncable(
+                return syncManager.computeSyncable(
                         account, userId, providerName);
             }
         } finally {
index 39ddc3a..e0aac7b 100644 (file)
@@ -19,6 +19,7 @@ package com.android.server.content;
 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;
@@ -64,6 +65,7 @@ import android.os.Looper;
 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;
@@ -79,6 +81,7 @@ import android.util.Log;
 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;
@@ -133,6 +136,8 @@ import java.util.Set;
 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;
 
@@ -194,6 +199,11 @@ public class SyncManager {
     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];
@@ -310,6 +320,10 @@ public class SyncManager {
 
     private final UserManager mUserManager;
 
+    private final AccountManager mAccountManager;
+
+    private final AccountManagerInternal mAccountManagerInternal;
+
     private List<UserInfo> getAllUsers() {
         return mUserManager.getUsers();
     }
@@ -490,8 +504,6 @@ public class SyncManager {
             @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);
             }
         });
@@ -522,8 +534,7 @@ public class SyncManager {
                 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);
@@ -562,6 +573,9 @@ public class SyncManager {
         }
         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));
 
@@ -655,7 +669,7 @@ public class SyncManager {
         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);
 
@@ -666,22 +680,22 @@ public class SyncManager {
         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;
         }
     }
 
@@ -733,13 +747,10 @@ public class SyncManager {
      * @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();
@@ -749,17 +760,27 @@ public class SyncManager {
                     + 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);
@@ -808,29 +829,41 @@ public class SyncManager {
             }
 
             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) {
@@ -838,6 +871,7 @@ public class SyncManager {
                             account.account, account.userId, authority, AuthorityInfo.SYNCABLE);
                     isSyncable = AuthorityInfo.SYNCABLE;
                 }
+
                 if (onlyThoseWithUnkownSyncableState && isSyncable >= 0) {
                     continue;
                 }
@@ -863,6 +897,9 @@ public class SyncManager {
                                 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();
@@ -887,8 +924,6 @@ public class SyncManager {
                     if (isLoggable) {
                         Slog.v(TAG, "scheduleSync:"
                                 + " delay until " + delayUntil
-                                + " run by " + runtimeMillis
-                                + " flexMillis " + beforeRuntimeMillis
                                 + ", source " + source
                                 + ", account " + account
                                 + ", authority " + authority
@@ -904,6 +939,56 @@ public class SyncManager {
         }
     }
 
+    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();
@@ -960,8 +1045,6 @@ public class SyncManager {
         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 */);
     }
 
@@ -1421,7 +1504,6 @@ public class SyncManager {
                 mContext.getOpPackageName());
         for (Account account : accounts) {
             scheduleSync(account, userId, SyncOperation.REASON_USER_START, null, null,
-                    0 /* no delay */, 0 /* No flexMillis */,
                     true /* onlyThoseWithUnknownSyncableState */);
         }
     }
@@ -2530,13 +2612,18 @@ public class SyncManager {
                 }
             }
 
-            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);
         }
 
@@ -2596,8 +2683,7 @@ public class SyncManager {
 
             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);
             }
         }
 
@@ -2665,6 +2751,26 @@ public class SyncManager {
                     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);
         }
@@ -2725,29 +2831,38 @@ public class SyncManager {
         /**
          * 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.
@@ -2760,9 +2875,9 @@ public class SyncManager {
                 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) {
index bc3fc6a..64849aa 100644 (file)
@@ -234,6 +234,12 @@ public class SyncStorageEngine extends Handler {
          */
         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;