Loading apex/jobscheduler/framework/java/android/app/job/JobInfo.java +11 −0 Original line number Original line Diff line number Diff line Loading @@ -1382,6 +1382,12 @@ public class JobInfo implements Parcelable { * Calling this method will override any requirements previously defined * Calling this method will override any requirements previously defined * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only * want to call one of these methods. * want to call one of these methods. * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, * an app must hold the {@link android.Manifest.permission#INTERNET} and * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to * schedule a job that requires a network. * * <p class="note"> * <p class="note"> * When your job executes in * When your job executes in * {@link JobService#onStartJob(JobParameters)}, be sure to use the * {@link JobService#onStartJob(JobParameters)}, be sure to use the Loading Loading @@ -1438,6 +1444,11 @@ public class JobInfo implements Parcelable { * otherwise you'll use the default network which may not meet this * otherwise you'll use the default network which may not meet this * constraint. * constraint. * * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, * an app must hold the {@link android.Manifest.permission#INTERNET} and * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to * schedule a job that requires a network. * * @param networkRequest The detailed description of the kind of network * @param networkRequest The detailed description of the kind of network * this job requires, or {@code null} if no specific kind of * this job requires, or {@code null} if no specific kind of * network is required. Defining a {@link NetworkSpecifier} * network is required. Defining a {@link NetworkSpecifier} Loading apex/jobscheduler/framework/java/android/app/job/JobParameters.java +4 −0 Original line number Original line Diff line number Diff line Loading @@ -481,6 +481,10 @@ public class JobParameters implements Parcelable { * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over * a metered network when there is a surplus of metered data available. * a metered network when there is a surplus of metered data available. * * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, * this will return {@code null} if the app does not hold the permissions specified in * {@link JobInfo.Builder#setRequiredNetwork(NetworkRequest)}. * * @return the network that should be used to perform any network requests * @return the network that should be used to perform any network requests * for this job, or {@code null} if this job didn't set any required * for this job, or {@code null} if this job didn't set any required * network type or if the job executed when there was no available network to use. * network type or if the job executed when there was no available network to use. Loading apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +85 −30 Original line number Original line Diff line number Diff line Loading @@ -23,6 +23,7 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import android.Manifest; import android.annotation.EnforcePermission; import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Nullable; Loading Loading @@ -190,6 +191,14 @@ public class JobSchedulerService extends com.android.server.SystemService @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L; private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L; /** * Require the app to have the INTERNET and ACCESS_NETWORK_STATE permissions when scheduling * a job with a connectivity constraint. */ @ChangeId @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) static final long REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS = 271850009L; @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public static Clock sSystemClock = Clock.systemUTC(); public static Clock sSystemClock = Clock.systemUTC(); Loading Loading @@ -299,6 +308,14 @@ public class JobSchedulerService extends com.android.server.SystemService private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers = private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers = new RemoteCallbackList<>(); new RemoteCallbackList<>(); /** * Cache of grant status of permissions, keyed by UID->PID->permission name. A missing value * means the state has not been queried. */ @GuardedBy("mPermissionCache") private final SparseArray<SparseArrayMap<String, Boolean>> mPermissionCache = new SparseArray<>(); private final CountQuotaTracker mQuotaTracker; private final CountQuotaTracker mQuotaTracker; private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()"; private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()"; private static final String QUOTA_TRACKER_SCHEDULE_LOGGED = private static final String QUOTA_TRACKER_SCHEDULE_LOGGED = Loading Loading @@ -1042,6 +1059,10 @@ public class JobSchedulerService extends com.android.server.SystemService final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { synchronized (mPermissionCache) { // Something changed. Better clear the cached permission set. mPermissionCache.remove(pkgUid); } // Purge the app's jobs if the whole package was just disabled. When this is // Purge the app's jobs if the whole package was just disabled. When this is // the case the component name will be a bare package name. // the case the component name will be a bare package name. if (pkgName != null && pkgUid != -1) { if (pkgName != null && pkgUid != -1) { Loading Loading @@ -1106,17 +1127,19 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid); Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid); } } } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { synchronized (mPermissionCache) { // Something changed. Better clear the cached permission set. mPermissionCache.remove(pkgUid); } if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); synchronized (mLock) { synchronized (mLock) { mUidToPackageCache.remove(uid); mUidToPackageCache.remove(pkgUid); } } else { synchronized (mJobSchedulerStub.mPersistCache) { mJobSchedulerStub.mPersistCache.remove(pkgUid); } } } } } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { synchronized (mPermissionCache) { mPermissionCache.remove(pkgUid); } if (DEBUG) { if (DEBUG) { Slog.d(TAG, "Removing jobs for " + pkgName + " (uid=" + pkgUid + ")"); Slog.d(TAG, "Removing jobs for " + pkgName + " (uid=" + pkgUid + ")"); } } Loading Loading @@ -1155,6 +1178,14 @@ public class JobSchedulerService extends com.android.server.SystemService } } } } mConcurrencyManager.onUserRemoved(userId); mConcurrencyManager.onUserRemoved(userId); synchronized (mPermissionCache) { for (int u = mPermissionCache.size() - 1; u >= 0; --u) { final int uid = mPermissionCache.keyAt(u); if (userId == UserHandle.getUserId(uid)) { mPermissionCache.removeAt(u); } } } } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { // Has this package scheduled any jobs, such that we will take action // Has this package scheduled any jobs, such that we will take action // if it were to be force-stopped? // if it were to be force-stopped? Loading Loading @@ -3748,18 +3779,38 @@ public class JobSchedulerService extends com.android.server.SystemService } } /** /** * Binder stub trampoline implementation * Returns whether the app has the permission granted. * This currently only works for normal permissions and <b>DOES NOT</b> work for runtime * permissions. * TODO: handle runtime permissions */ */ final class JobSchedulerStub extends IJobScheduler.Stub { private boolean hasPermission(int uid, int pid, @NonNull String permission) { synchronized (mPermissionCache) { SparseArrayMap<String, Boolean> pidPermissions = mPermissionCache.get(uid); if (pidPermissions == null) { pidPermissions = new SparseArrayMap<>(); mPermissionCache.put(uid, pidPermissions); } final Boolean cached = pidPermissions.get(pid, permission); if (cached != null) { return cached; } final int result = getContext().checkPermission(permission, pid, uid); final boolean permissionGranted = (result == PackageManager.PERMISSION_GRANTED); pidPermissions.add(pid, permission, permissionGranted); return permissionGranted; } } /** /** * Cache determination of whether a given app can persist jobs * Binder stub trampoline implementation * key is uid of the calling app; value is undetermined/true/false */ */ private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>(); final class JobSchedulerStub extends IJobScheduler.Stub { // Enforce that only the app itself (or shared uid participant) can schedule a // Enforce that only the app itself (or shared uid participant) can schedule a // job that runs one of the app's services, as well as verifying that the // job that runs one of the app's services, as well as verifying that the // named service properly requires the BIND_JOB_SERVICE permission // named service properly requires the BIND_JOB_SERVICE permission // TODO(141645789): merge enforceValidJobRequest() with validateJob() private void enforceValidJobRequest(int uid, int pid, JobInfo job) { private void enforceValidJobRequest(int uid, int pid, JobInfo job) { final PackageManager pm = getContext() final PackageManager pm = getContext() .createContextAsUser(UserHandle.getUserHandleForUid(uid), 0) .createContextAsUser(UserHandle.getUserHandleForUid(uid), 0) Loading @@ -3784,31 +3835,33 @@ public class JobSchedulerService extends com.android.server.SystemService throw new IllegalArgumentException( throw new IllegalArgumentException( "Tried to schedule job for non-existent component: " + service); "Tried to schedule job for non-existent component: " + service); } } // If we get this far we're good to go; all we need to do now is check // whether the app is allowed to persist its scheduled work. if (job.isPersisted() && !canPersistJobs(pid, uid)) { if (job.isPersisted() && !canPersistJobs(pid, uid)) { throw new IllegalArgumentException("Requested job cannot be persisted without" throw new IllegalArgumentException("Requested job cannot be persisted without" + " holding android.permission.RECEIVE_BOOT_COMPLETED permission"); + " holding android.permission.RECEIVE_BOOT_COMPLETED permission"); } } if (job.getRequiredNetwork() != null && CompatChanges.isChangeEnabled( REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { // All networking, including with the local network and even local to the device, // requires the INTERNET permission. if (!hasPermission(uid, pid, Manifest.permission.INTERNET)) { throw new SecurityException(Manifest.permission.INTERNET + " required for jobs with a connectivity constraint"); } if (!hasPermission(uid, pid, Manifest.permission.ACCESS_NETWORK_STATE)) { throw new SecurityException(Manifest.permission.ACCESS_NETWORK_STATE + " required for jobs with a connectivity constraint"); } } } } private boolean canPersistJobs(int pid, int uid) { private boolean canPersistJobs(int pid, int uid) { // If we get this far we're good to go; all we need to do now is check // whether the app is allowed to persist its scheduled work. final boolean canPersist; synchronized (mPersistCache) { Boolean cached = mPersistCache.get(uid); if (cached != null) { canPersist = cached.booleanValue(); } else { // Persisting jobs is tantamount to running at boot, so we permit // Persisting jobs is tantamount to running at boot, so we permit // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED // permission // permission int result = getContext().checkPermission( return hasPermission(uid, pid, Manifest.permission.RECEIVE_BOOT_COMPLETED); android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid); canPersist = (result == PackageManager.PERMISSION_GRANTED); mPersistCache.put(uid, canPersist); } } return canPersist; } } private int validateJob(@NonNull JobInfo job, int callingUid, int callingPid, private int validateJob(@NonNull JobInfo job, int callingUid, int callingPid, Loading Loading @@ -4027,6 +4080,8 @@ public class JobSchedulerService extends com.android.server.SystemService + " not permitted to schedule jobs for other apps"); + " not permitted to schedule jobs for other apps"); } } enforceValidJobRequest(callerUid, callerPid, job); int result = validateJob(job, callerUid, callerPid, userId, packageName, null); int result = validateJob(job, callerUid, callerPid, userId, packageName, null); if (result != JobScheduler.RESULT_SUCCESS) { if (result != JobScheduler.RESULT_SUCCESS) { return result; return result; Loading apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +44 −1 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static android.app.job.JobInfo.getPriorityString; import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE; import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.Manifest; import android.annotation.BytesLong; import android.annotation.BytesLong; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Nullable; Loading @@ -39,6 +40,7 @@ import android.compat.annotation.EnabledAfter; import android.content.ComponentName; import android.content.ComponentName; import android.content.Context; import android.content.Context; import android.content.Intent; import android.content.Intent; import android.content.PermissionChecker; import android.content.ServiceConnection; import android.content.ServiceConnection; import android.net.Network; import android.net.Network; import android.net.Uri; import android.net.Uri; Loading Loading @@ -339,12 +341,13 @@ public final class JobServiceContext implements ServiceConnection { job.changedAuthorities.toArray(triggeredAuthorities); job.changedAuthorities.toArray(triggeredAuthorities); } } final JobInfo ji = job.getJob(); final JobInfo ji = job.getJob(); final Network passedNetwork = canGetNetworkInformation(job) ? job.network : null; mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(), mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(), ji.getExtras(), ji.getExtras(), ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(), ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(), isDeadlineExpired, job.shouldTreatAsExpeditedJob(), isDeadlineExpired, job.shouldTreatAsExpeditedJob(), job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities, job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities, job.network); passedNetwork); mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis(); mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis(); mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job); mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job); mMaxExecutionTimeMillis = mMaxExecutionTimeMillis = Loading Loading @@ -504,6 +507,37 @@ public final class JobServiceContext implements ServiceConnection { } } } } private boolean canGetNetworkInformation(@NonNull JobStatus job) { if (job.getJob().getRequiredNetwork() == null) { // The job never had a network constraint, so we're not going to give it a network // object. Add this check as an early return to avoid wasting cycles doing permission // checks for this job. return false; } // The calling app is doing the work, so use its UID, not the source UID. final int uid = job.getUid(); if (CompatChanges.isChangeEnabled( JobSchedulerService.REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { final String pkgName = job.getServiceComponent().getPackageName(); if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.INTERNET)) { return false; } if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.ACCESS_NETWORK_STATE)) { return false; } } return true; } private boolean hasPermissionForDelivery(int uid, @NonNull String pkgName, @NonNull String permission) { final int result = PermissionChecker.checkPermissionForDataDelivery(mContext, permission, PermissionChecker.PID_UNKNOWN, uid, pkgName, /* attributionTag */ null, "network info via JS"); return result == PermissionChecker.PERMISSION_GRANTED; } @EconomicPolicy.AppAction @EconomicPolicy.AppAction private static int getStartActionId(@NonNull JobStatus job) { private static int getStartActionId(@NonNull JobStatus job) { switch (job.getEffectivePriority()) { switch (job.getEffectivePriority()) { Loading Loading @@ -603,6 +637,15 @@ public final class JobServiceContext implements ServiceConnection { } } void informOfNetworkChangeLocked(Network newNetwork) { void informOfNetworkChangeLocked(Network newNetwork) { if (newNetwork != null && mRunningJob != null && !canGetNetworkInformation(mRunningJob)) { // The app can't get network information, so there's no point informing it of network // changes. This case may happen if an app had scheduled network job and then // started targeting U+ without requesting the required network permissions. if (DEBUG) { Slog.d(TAG, "Skipping network change call because of missing permissions"); } return; } if (mVerb != VERB_EXECUTING) { if (mVerb != VERB_EXECUTING) { Slog.w(TAG, "Sending onNetworkChanged for a job that isn't started. " + mRunningJob); Slog.w(TAG, "Sending onNetworkChanged for a job that isn't started. " + mRunningJob); if (mVerb == VERB_BINDING || mVerb == VERB_STARTING) { if (mVerb == VERB_BINDING || mVerb == VERB_STARTING) { Loading Loading
apex/jobscheduler/framework/java/android/app/job/JobInfo.java +11 −0 Original line number Original line Diff line number Diff line Loading @@ -1382,6 +1382,12 @@ public class JobInfo implements Parcelable { * Calling this method will override any requirements previously defined * Calling this method will override any requirements previously defined * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only * by {@link #setRequiredNetwork(NetworkRequest)}; you typically only * want to call one of these methods. * want to call one of these methods. * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, * an app must hold the {@link android.Manifest.permission#INTERNET} and * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to * schedule a job that requires a network. * * <p class="note"> * <p class="note"> * When your job executes in * When your job executes in * {@link JobService#onStartJob(JobParameters)}, be sure to use the * {@link JobService#onStartJob(JobParameters)}, be sure to use the Loading Loading @@ -1438,6 +1444,11 @@ public class JobInfo implements Parcelable { * otherwise you'll use the default network which may not meet this * otherwise you'll use the default network which may not meet this * constraint. * constraint. * * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, * an app must hold the {@link android.Manifest.permission#INTERNET} and * {@link android.Manifest.permission#ACCESS_NETWORK_STATE} permissions to * schedule a job that requires a network. * * @param networkRequest The detailed description of the kind of network * @param networkRequest The detailed description of the kind of network * this job requires, or {@code null} if no specific kind of * this job requires, or {@code null} if no specific kind of * network is required. Defining a {@link NetworkSpecifier} * network is required. Defining a {@link NetworkSpecifier} Loading
apex/jobscheduler/framework/java/android/app/job/JobParameters.java +4 −0 Original line number Original line Diff line number Diff line Loading @@ -481,6 +481,10 @@ public class JobParameters implements Parcelable { * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over * such as allowing a {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over * a metered network when there is a surplus of metered data available. * a metered network when there is a surplus of metered data available. * * * Starting in Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, * this will return {@code null} if the app does not hold the permissions specified in * {@link JobInfo.Builder#setRequiredNetwork(NetworkRequest)}. * * @return the network that should be used to perform any network requests * @return the network that should be used to perform any network requests * for this job, or {@code null} if this job didn't set any required * for this job, or {@code null} if this job didn't set any required * network type or if the job executed when there was no available network to use. * network type or if the job executed when there was no available network to use. Loading
apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +85 −30 Original line number Original line Diff line number Diff line Loading @@ -23,6 +23,7 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import android.Manifest; import android.annotation.EnforcePermission; import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Nullable; Loading Loading @@ -190,6 +191,14 @@ public class JobSchedulerService extends com.android.server.SystemService @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L; private static final long REQUIRE_NETWORK_CONSTRAINT_FOR_NETWORK_JOB_WORK_ITEMS = 241104082L; /** * Require the app to have the INTERNET and ACCESS_NETWORK_STATE permissions when scheduling * a job with a connectivity constraint. */ @ChangeId @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) static final long REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS = 271850009L; @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public static Clock sSystemClock = Clock.systemUTC(); public static Clock sSystemClock = Clock.systemUTC(); Loading Loading @@ -299,6 +308,14 @@ public class JobSchedulerService extends com.android.server.SystemService private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers = private final RemoteCallbackList<IUserVisibleJobObserver> mUserVisibleJobObservers = new RemoteCallbackList<>(); new RemoteCallbackList<>(); /** * Cache of grant status of permissions, keyed by UID->PID->permission name. A missing value * means the state has not been queried. */ @GuardedBy("mPermissionCache") private final SparseArray<SparseArrayMap<String, Boolean>> mPermissionCache = new SparseArray<>(); private final CountQuotaTracker mQuotaTracker; private final CountQuotaTracker mQuotaTracker; private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()"; private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()"; private static final String QUOTA_TRACKER_SCHEDULE_LOGGED = private static final String QUOTA_TRACKER_SCHEDULE_LOGGED = Loading Loading @@ -1042,6 +1059,10 @@ public class JobSchedulerService extends com.android.server.SystemService final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { synchronized (mPermissionCache) { // Something changed. Better clear the cached permission set. mPermissionCache.remove(pkgUid); } // Purge the app's jobs if the whole package was just disabled. When this is // Purge the app's jobs if the whole package was just disabled. When this is // the case the component name will be a bare package name. // the case the component name will be a bare package name. if (pkgName != null && pkgUid != -1) { if (pkgName != null && pkgUid != -1) { Loading Loading @@ -1106,17 +1127,19 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid); Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid); } } } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { synchronized (mPermissionCache) { // Something changed. Better clear the cached permission set. mPermissionCache.remove(pkgUid); } if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); synchronized (mLock) { synchronized (mLock) { mUidToPackageCache.remove(uid); mUidToPackageCache.remove(pkgUid); } } else { synchronized (mJobSchedulerStub.mPersistCache) { mJobSchedulerStub.mPersistCache.remove(pkgUid); } } } } } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { } else if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { synchronized (mPermissionCache) { mPermissionCache.remove(pkgUid); } if (DEBUG) { if (DEBUG) { Slog.d(TAG, "Removing jobs for " + pkgName + " (uid=" + pkgUid + ")"); Slog.d(TAG, "Removing jobs for " + pkgName + " (uid=" + pkgUid + ")"); } } Loading Loading @@ -1155,6 +1178,14 @@ public class JobSchedulerService extends com.android.server.SystemService } } } } mConcurrencyManager.onUserRemoved(userId); mConcurrencyManager.onUserRemoved(userId); synchronized (mPermissionCache) { for (int u = mPermissionCache.size() - 1; u >= 0; --u) { final int uid = mPermissionCache.keyAt(u); if (userId == UserHandle.getUserId(uid)) { mPermissionCache.removeAt(u); } } } } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) { // Has this package scheduled any jobs, such that we will take action // Has this package scheduled any jobs, such that we will take action // if it were to be force-stopped? // if it were to be force-stopped? Loading Loading @@ -3748,18 +3779,38 @@ public class JobSchedulerService extends com.android.server.SystemService } } /** /** * Binder stub trampoline implementation * Returns whether the app has the permission granted. * This currently only works for normal permissions and <b>DOES NOT</b> work for runtime * permissions. * TODO: handle runtime permissions */ */ final class JobSchedulerStub extends IJobScheduler.Stub { private boolean hasPermission(int uid, int pid, @NonNull String permission) { synchronized (mPermissionCache) { SparseArrayMap<String, Boolean> pidPermissions = mPermissionCache.get(uid); if (pidPermissions == null) { pidPermissions = new SparseArrayMap<>(); mPermissionCache.put(uid, pidPermissions); } final Boolean cached = pidPermissions.get(pid, permission); if (cached != null) { return cached; } final int result = getContext().checkPermission(permission, pid, uid); final boolean permissionGranted = (result == PackageManager.PERMISSION_GRANTED); pidPermissions.add(pid, permission, permissionGranted); return permissionGranted; } } /** /** * Cache determination of whether a given app can persist jobs * Binder stub trampoline implementation * key is uid of the calling app; value is undetermined/true/false */ */ private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>(); final class JobSchedulerStub extends IJobScheduler.Stub { // Enforce that only the app itself (or shared uid participant) can schedule a // Enforce that only the app itself (or shared uid participant) can schedule a // job that runs one of the app's services, as well as verifying that the // job that runs one of the app's services, as well as verifying that the // named service properly requires the BIND_JOB_SERVICE permission // named service properly requires the BIND_JOB_SERVICE permission // TODO(141645789): merge enforceValidJobRequest() with validateJob() private void enforceValidJobRequest(int uid, int pid, JobInfo job) { private void enforceValidJobRequest(int uid, int pid, JobInfo job) { final PackageManager pm = getContext() final PackageManager pm = getContext() .createContextAsUser(UserHandle.getUserHandleForUid(uid), 0) .createContextAsUser(UserHandle.getUserHandleForUid(uid), 0) Loading @@ -3784,31 +3835,33 @@ public class JobSchedulerService extends com.android.server.SystemService throw new IllegalArgumentException( throw new IllegalArgumentException( "Tried to schedule job for non-existent component: " + service); "Tried to schedule job for non-existent component: " + service); } } // If we get this far we're good to go; all we need to do now is check // whether the app is allowed to persist its scheduled work. if (job.isPersisted() && !canPersistJobs(pid, uid)) { if (job.isPersisted() && !canPersistJobs(pid, uid)) { throw new IllegalArgumentException("Requested job cannot be persisted without" throw new IllegalArgumentException("Requested job cannot be persisted without" + " holding android.permission.RECEIVE_BOOT_COMPLETED permission"); + " holding android.permission.RECEIVE_BOOT_COMPLETED permission"); } } if (job.getRequiredNetwork() != null && CompatChanges.isChangeEnabled( REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { // All networking, including with the local network and even local to the device, // requires the INTERNET permission. if (!hasPermission(uid, pid, Manifest.permission.INTERNET)) { throw new SecurityException(Manifest.permission.INTERNET + " required for jobs with a connectivity constraint"); } if (!hasPermission(uid, pid, Manifest.permission.ACCESS_NETWORK_STATE)) { throw new SecurityException(Manifest.permission.ACCESS_NETWORK_STATE + " required for jobs with a connectivity constraint"); } } } } private boolean canPersistJobs(int pid, int uid) { private boolean canPersistJobs(int pid, int uid) { // If we get this far we're good to go; all we need to do now is check // whether the app is allowed to persist its scheduled work. final boolean canPersist; synchronized (mPersistCache) { Boolean cached = mPersistCache.get(uid); if (cached != null) { canPersist = cached.booleanValue(); } else { // Persisting jobs is tantamount to running at boot, so we permit // Persisting jobs is tantamount to running at boot, so we permit // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED // permission // permission int result = getContext().checkPermission( return hasPermission(uid, pid, Manifest.permission.RECEIVE_BOOT_COMPLETED); android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid); canPersist = (result == PackageManager.PERMISSION_GRANTED); mPersistCache.put(uid, canPersist); } } return canPersist; } } private int validateJob(@NonNull JobInfo job, int callingUid, int callingPid, private int validateJob(@NonNull JobInfo job, int callingUid, int callingPid, Loading Loading @@ -4027,6 +4080,8 @@ public class JobSchedulerService extends com.android.server.SystemService + " not permitted to schedule jobs for other apps"); + " not permitted to schedule jobs for other apps"); } } enforceValidJobRequest(callerUid, callerPid, job); int result = validateJob(job, callerUid, callerPid, userId, packageName, null); int result = validateJob(job, callerUid, callerPid, userId, packageName, null); if (result != JobScheduler.RESULT_SUCCESS) { if (result != JobScheduler.RESULT_SUCCESS) { return result; return result; Loading
apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +44 −1 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static android.app.job.JobInfo.getPriorityString; import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE; import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.Manifest; import android.annotation.BytesLong; import android.annotation.BytesLong; import android.annotation.NonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Nullable; Loading @@ -39,6 +40,7 @@ import android.compat.annotation.EnabledAfter; import android.content.ComponentName; import android.content.ComponentName; import android.content.Context; import android.content.Context; import android.content.Intent; import android.content.Intent; import android.content.PermissionChecker; import android.content.ServiceConnection; import android.content.ServiceConnection; import android.net.Network; import android.net.Network; import android.net.Uri; import android.net.Uri; Loading Loading @@ -339,12 +341,13 @@ public final class JobServiceContext implements ServiceConnection { job.changedAuthorities.toArray(triggeredAuthorities); job.changedAuthorities.toArray(triggeredAuthorities); } } final JobInfo ji = job.getJob(); final JobInfo ji = job.getJob(); final Network passedNetwork = canGetNetworkInformation(job) ? job.network : null; mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(), mParams = new JobParameters(mRunningCallback, job.getNamespace(), job.getJobId(), ji.getExtras(), ji.getExtras(), ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(), ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(), isDeadlineExpired, job.shouldTreatAsExpeditedJob(), isDeadlineExpired, job.shouldTreatAsExpeditedJob(), job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities, job.shouldTreatAsUserInitiatedJob(), triggeredUris, triggeredAuthorities, job.network); passedNetwork); mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis(); mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis(); mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job); mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job); mMaxExecutionTimeMillis = mMaxExecutionTimeMillis = Loading Loading @@ -504,6 +507,37 @@ public final class JobServiceContext implements ServiceConnection { } } } } private boolean canGetNetworkInformation(@NonNull JobStatus job) { if (job.getJob().getRequiredNetwork() == null) { // The job never had a network constraint, so we're not going to give it a network // object. Add this check as an early return to avoid wasting cycles doing permission // checks for this job. return false; } // The calling app is doing the work, so use its UID, not the source UID. final int uid = job.getUid(); if (CompatChanges.isChangeEnabled( JobSchedulerService.REQUIRE_NETWORK_PERMISSIONS_FOR_CONNECTIVITY_JOBS, uid)) { final String pkgName = job.getServiceComponent().getPackageName(); if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.INTERNET)) { return false; } if (!hasPermissionForDelivery(uid, pkgName, Manifest.permission.ACCESS_NETWORK_STATE)) { return false; } } return true; } private boolean hasPermissionForDelivery(int uid, @NonNull String pkgName, @NonNull String permission) { final int result = PermissionChecker.checkPermissionForDataDelivery(mContext, permission, PermissionChecker.PID_UNKNOWN, uid, pkgName, /* attributionTag */ null, "network info via JS"); return result == PermissionChecker.PERMISSION_GRANTED; } @EconomicPolicy.AppAction @EconomicPolicy.AppAction private static int getStartActionId(@NonNull JobStatus job) { private static int getStartActionId(@NonNull JobStatus job) { switch (job.getEffectivePriority()) { switch (job.getEffectivePriority()) { Loading Loading @@ -603,6 +637,15 @@ public final class JobServiceContext implements ServiceConnection { } } void informOfNetworkChangeLocked(Network newNetwork) { void informOfNetworkChangeLocked(Network newNetwork) { if (newNetwork != null && mRunningJob != null && !canGetNetworkInformation(mRunningJob)) { // The app can't get network information, so there's no point informing it of network // changes. This case may happen if an app had scheduled network job and then // started targeting U+ without requesting the required network permissions. if (DEBUG) { Slog.d(TAG, "Skipping network change call because of missing permissions"); } return; } if (mVerb != VERB_EXECUTING) { if (mVerb != VERB_EXECUTING) { Slog.w(TAG, "Sending onNetworkChanged for a job that isn't started. " + mRunningJob); Slog.w(TAG, "Sending onNetworkChanged for a job that isn't started. " + mRunningJob); if (mVerb == VERB_BINDING || mVerb == VERB_STARTING) { if (mVerb == VERB_BINDING || mVerb == VERB_STARTING) { Loading