diff options
| author | 2023-12-05 20:48:27 +0000 | |
|---|---|---|
| committer | 2023-12-05 20:52:51 +0000 | |
| commit | 3d3b01abcf033e41ef92441ba6a294c83e41dcab (patch) | |
| tree | 611612b67004f04ed9468af0e8ea6c1d72887add | |
| parent | ca834953e8860043a5ffdb8410538c8791d20191 (diff) | |
Don't allow jobs of stopped apps to run.
Jobs scheduled on behalf of another app (aka "source app") aren't
dropped when the source app is force stopped. In this scenario, we don't
want the job to run until the app comes out of the stopped state. Add
explicit handling of this case to ensure we don't accidentally run these
jobs and potentially give the app a way to get out of the stopped state
in the background.
Bug: 313794821
Test: atest FrameworksMockingServicesTests:BackgroundJobsControllerTest
Test: atest CtsJobSchedulerTestCases:JobThrottlingTest
Change-Id: I26a8efb0b04022a6ac9e9ad716940c8971fd57dd
6 files changed, 418 insertions, 24 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index f97100bf2e9b..dae03b0f4904 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1433,10 +1433,10 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.d(TAG, "Removing jobs for pkg " + pkgName + " at uid " + pkgUid); } synchronized (mLock) { - // Exclude jobs scheduled on behalf of this app for now because SyncManager + // Exclude jobs scheduled on behalf of this app because SyncManager // and other job proxy agents may not know to reschedule the job properly // after force stop. - // TODO(209852664): determine how to best handle syncs & other proxied jobs + // Proxied jobs will not be allowed to run if the source app is stopped. cancelJobsForPackageAndUidLocked(pkgName, pkgUid, /* includeSchedulingApp */ true, /* includeSourceApp */ false, JobParameters.STOP_REASON_USER, @@ -1448,7 +1448,9 @@ public class JobSchedulerService extends com.android.server.SystemService } }; - private String getPackageName(Intent intent) { + /** Returns the package name stored in the intent's data. */ + @Nullable + public static String getPackageName(Intent intent) { Uri uri = intent.getData(); String pkg = uri != null ? uri.getSchemeSpecificPart() : null; return pkg; diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java index cd3ba6b9e13e..4aadc903ba23 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java @@ -17,18 +17,26 @@ package com.android.server.job.controllers; import static com.android.server.job.JobSchedulerService.NEVER_INDEX; +import static com.android.server.job.JobSchedulerService.getPackageName; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManagerInternal; import android.os.SystemClock; import android.os.UserHandle; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; +import android.util.SparseArrayMap; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.GuardedBy; import com.android.server.AppStateTracker; import com.android.server.AppStateTrackerImpl; import com.android.server.AppStateTrackerImpl.Listener; @@ -50,6 +58,8 @@ import java.util.function.Predicate; * * - the uid-active boolean state expressed by the AppStateTracker. Jobs in 'active' * uids are inherently eligible to run jobs regardless of the uid's standby bucket. + * + * - the app's stopped state */ public final class BackgroundJobsController extends StateController { private static final String TAG = "JobScheduler.Background"; @@ -63,9 +73,48 @@ public final class BackgroundJobsController extends StateController { private final ActivityManagerInternal mActivityManagerInternal; private final AppStateTrackerImpl mAppStateTracker; + private final PackageManagerInternal mPackageManagerInternal; + + @GuardedBy("mLock") + private final SparseArrayMap<String, Boolean> mPackageStoppedState = new SparseArrayMap<>(); private final UpdateJobFunctor mUpdateJobFunctor = new UpdateJobFunctor(); + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String pkgName = getPackageName(intent); + final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1); + final String action = intent.getAction(); + if (pkgUid == -1) { + Slog.e(TAG, "Didn't get package UID in intent (" + action + ")"); + return; + } + + if (DEBUG) { + Slog.d(TAG, "Got " + action + " for " + pkgUid + "/" + pkgName); + } + + switch (action) { + case Intent.ACTION_PACKAGE_RESTARTED: { + synchronized (mLock) { + mPackageStoppedState.add(pkgUid, pkgName, Boolean.TRUE); + updateJobRestrictionsForUidLocked(pkgUid, false); + } + } + break; + + case Intent.ACTION_PACKAGE_UNSTOPPED: { + synchronized (mLock) { + mPackageStoppedState.add(pkgUid, pkgName, Boolean.FALSE); + updateJobRestrictionsLocked(pkgUid, UNKNOWN); + } + } + break; + } + } + }; + public BackgroundJobsController(JobSchedulerService service) { super(service); @@ -73,11 +122,18 @@ public final class BackgroundJobsController extends StateController { LocalServices.getService(ActivityManagerInternal.class)); mAppStateTracker = (AppStateTrackerImpl) Objects.requireNonNull( LocalServices.getService(AppStateTracker.class)); + mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); } @Override public void startTrackingLocked() { mAppStateTracker.addListener(mForceAppStandbyListener); + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + filter.addAction(Intent.ACTION_PACKAGE_UNSTOPPED); + filter.addDataScheme("package"); + mContext.registerReceiverAsUser( + mBroadcastReceiver, UserHandle.ALL, filter, null, null); } @Override @@ -99,11 +155,45 @@ public final class BackgroundJobsController extends StateController { } @Override + public void onAppRemovedLocked(String packageName, int uid) { + mPackageStoppedState.delete(uid, packageName); + } + + @Override + public void onUserRemovedLocked(int userId) { + for (int u = mPackageStoppedState.numMaps() - 1; u >= 0; --u) { + final int uid = mPackageStoppedState.keyAt(u); + if (UserHandle.getUserId(uid) == userId) { + mPackageStoppedState.deleteAt(u); + } + } + } + + @Override public void dumpControllerStateLocked(final IndentingPrintWriter pw, final Predicate<JobStatus> predicate) { + pw.println("Aconfig flags:"); + pw.increaseIndent(); + pw.print(android.content.pm.Flags.FLAG_STAY_STOPPED, + android.content.pm.Flags.stayStopped()); + pw.println(); + pw.decreaseIndent(); + pw.println(); + mAppStateTracker.dump(pw); pw.println(); + pw.println("Stopped packages:"); + pw.increaseIndent(); + mPackageStoppedState.forEach((uid, pkgName, isStopped) -> { + pw.print(uid); + pw.print(":"); + pw.print(pkgName); + pw.print("="); + pw.println(isStopped); + }); + pw.println(); + mService.getJobStore().forEachJob(predicate, (jobStatus) -> { final int uid = jobStatus.getSourceUid(); final String sourcePkg = jobStatus.getSourcePackageName(); @@ -205,14 +295,34 @@ public final class BackgroundJobsController extends StateController { } } + private boolean isPackageStopped(String packageName, int uid) { + if (mPackageStoppedState.contains(uid, packageName)) { + return mPackageStoppedState.get(uid, packageName); + } + final boolean isStopped = mPackageManagerInternal.isPackageStopped(packageName, uid); + mPackageStoppedState.add(uid, packageName, isStopped); + return isStopped; + } + boolean updateSingleJobRestrictionLocked(JobStatus jobStatus, final long nowElapsed, int activeState) { final int uid = jobStatus.getSourceUid(); final String packageName = jobStatus.getSourcePackageName(); - final boolean isUserBgRestricted = - !mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled() - && !mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(uid, packageName); + final boolean isSourcePkgStopped = + isPackageStopped(jobStatus.getSourcePackageName(), jobStatus.getSourceUid()); + final boolean isCallingPkgStopped; + if (!jobStatus.isProxyJob()) { + isCallingPkgStopped = isSourcePkgStopped; + } else { + isCallingPkgStopped = + isPackageStopped(jobStatus.getCallingPackageName(), jobStatus.getUid()); + } + final boolean isStopped = android.content.pm.Flags.stayStopped() + && (isCallingPkgStopped || isSourcePkgStopped); + final boolean isUserBgRestricted = isStopped + || (!mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled() + && !mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(uid, packageName)); // If a job started with the foreground flag, it'll cause the UID to stay active // and thus cause areJobsRestricted() to always return false, so if // areJobsRestricted() returns false and the app is BG restricted and not TOP, @@ -233,7 +343,8 @@ public final class BackgroundJobsController extends StateController { && isUserBgRestricted && mService.getUidProcState(uid) > ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE; - final boolean canRun = !shouldStopImmediately + // Don't let jobs (including proxied jobs) run if the app is in the stopped state. + final boolean canRun = !isStopped && !shouldStopImmediately && !mAppStateTracker.areJobsRestricted( uid, packageName, jobStatus.canRunInBatterySaver()); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index b74806494a60..d1f575ef40c8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -1102,6 +1102,12 @@ public final class JobStatus { return job.getService(); } + /** Return the package name of the app that scheduled the job. */ + public String getCallingPackageName() { + return job.getService().getPackageName(); + } + + /** Return the package name of the app on whose behalf the job was scheduled. */ public String getSourcePackageName() { return sourcePackageName; } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 38bcfa220af4..cd2aa1986e5b 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -2805,7 +2805,7 @@ public class Intent implements Parcelable, Cloneable { * and the package in the stopped state cannot self-start for any reason unless there's an * explicit request to start a component in the package. The {@link #ACTION_PACKAGE_UNSTOPPED} * broadcast is sent when such an explicit process start occurs and the package is taken - * out of the stopped state. + * out of the stopped state. The data contains the name of the package. * </p> * <ul> * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. diff --git a/services/core/java/com/android/server/content/SyncJobService.java b/services/core/java/com/android/server/content/SyncJobService.java index cd3f0f0ca5b2..1da7f0c059b0 100644 --- a/services/core/java/com/android/server/content/SyncJobService.java +++ b/services/core/java/com/android/server/content/SyncJobService.java @@ -19,7 +19,6 @@ package com.android.server.content; import android.annotation.Nullable; import android.app.job.JobParameters; import android.app.job.JobService; -import android.content.pm.PackageManagerInternal; import android.os.Message; import android.os.SystemClock; import android.util.Log; @@ -29,7 +28,6 @@ import android.util.SparseBooleanArray; import android.util.SparseLongArray; import com.android.internal.annotations.GuardedBy; -import com.android.server.LocalServices; public class SyncJobService extends JobService { private static final String TAG = "SyncManager"; @@ -99,20 +97,6 @@ public class SyncJobService extends JobService { return true; } - // TODO(b/209852664): remove this logic from here once it's added within JobScheduler. - // JobScheduler should not call onStartJob for syncs whose source packages are stopped. - // Until JS adds the relevant logic, this is a temporary solution to keep deferring syncs - // for packages in the stopped state. - if (android.content.pm.Flags.stayStopped()) { - if (LocalServices.getService(PackageManagerInternal.class) - .isPackageStopped(op.owningPackage, op.target.userId)) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Slog.d(TAG, "Skipping sync for force-stopped package: " + op.owningPackage); - } - return false; - } - } - boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); synchronized (sLock) { final int jobId = params.getJobId(); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java new file mode 100644 index 000000000000..cdae8c6ab004 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/BackgroundJobsControllerTest.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.job.controllers; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; +import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManagerInternal; +import android.app.AppGlobals; +import android.app.job.JobInfo; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.net.Uri; +import android.os.UserHandle; +import android.platform.test.flag.junit.SetFlagsRule; +import android.util.ArraySet; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.AppStateTracker; +import com.android.server.AppStateTrackerImpl; +import com.android.server.LocalServices; +import com.android.server.job.JobSchedulerService; +import com.android.server.job.JobStore; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +@RunWith(AndroidJUnit4.class) +public class BackgroundJobsControllerTest { + private static final int CALLING_UID = 1000; + private static final String CALLING_PACKAGE = "com.test.calling.package"; + private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests"; + private static final int SOURCE_UID = 10001; + private static final int ALTERNATE_UID = 12345; + private static final String ALTERNATE_SOURCE_PACKAGE = "com.test.alternate.package"; + private static final int SOURCE_USER_ID = 0; + + private BackgroundJobsController mBackgroundJobsController; + private BroadcastReceiver mStoppedReceiver; + private JobStore mJobStore; + + private MockitoSession mMockingSession; + @Mock + private Context mContext; + @Mock + private AppStateTrackerImpl mAppStateTrackerImpl; + @Mock + private IPackageManager mIPackageManager; + @Mock + private JobSchedulerService mJobSchedulerService; + @Mock + private PackageManagerInternal mPackageManagerInternal; + @Mock + private PackageManager mPackageManager; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Before + public void setUp() throws Exception { + mMockingSession = mockitoSession() + .initMocks(this) + .mockStatic(AppGlobals.class) + .mockStatic(LocalServices.class) + .strictness(Strictness.LENIENT) + .startMocking(); + + // Called in StateController constructor. + when(mJobSchedulerService.getTestableContext()).thenReturn(mContext); + when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService); + // Called in BackgroundJobsController constructor. + doReturn(mock(ActivityManagerInternal.class)) + .when(() -> LocalServices.getService(ActivityManagerInternal.class)); + doReturn(mAppStateTrackerImpl) + .when(() -> LocalServices.getService(AppStateTracker.class)); + doReturn(mPackageManagerInternal) + .when(() -> LocalServices.getService(PackageManagerInternal.class)); + mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir()); + when(mJobSchedulerService.getJobStore()).thenReturn(mJobStore); + // Called in JobStatus constructor. + doReturn(mIPackageManager).when(AppGlobals::getPackageManager); + + doReturn(false).when(mAppStateTrackerImpl) + .areJobsRestricted(anyInt(), anyString(), anyBoolean()); + doReturn(true).when(mAppStateTrackerImpl) + .isRunAnyInBackgroundAppOpsAllowed(anyInt(), anyString()); + + // Initialize real objects. + // Capture the listeners. + ArgumentCaptor<BroadcastReceiver> receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + + when(mContext.getPackageManager()).thenReturn(mPackageManager); + mBackgroundJobsController = new BackgroundJobsController(mJobSchedulerService); + mBackgroundJobsController.startTrackingLocked(); + + verify(mContext).registerReceiverAsUser(receiverCaptor.capture(), any(), + ArgumentMatchers.argThat(filter -> + filter.hasAction(Intent.ACTION_PACKAGE_RESTARTED) + && filter.hasAction(Intent.ACTION_PACKAGE_UNSTOPPED)), + any(), any()); + mStoppedReceiver = receiverCaptor.getValue(); + + // Need to do this since we're using a mock JS and not a real object. + doReturn(new ArraySet<>(new String[]{SOURCE_PACKAGE})) + .when(mJobSchedulerService).getPackagesForUidLocked(SOURCE_UID); + doReturn(new ArraySet<>(new String[]{ALTERNATE_SOURCE_PACKAGE})) + .when(mJobSchedulerService).getPackagesForUidLocked(ALTERNATE_UID); + setPackageUid(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE); + setPackageUid(SOURCE_UID, SOURCE_PACKAGE); + } + + @After + public void tearDown() { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + private void setPackageUid(final int uid, final String pkgName) throws Exception { + doReturn(uid).when(mIPackageManager) + .getPackageUid(eq(pkgName), anyLong(), eq(UserHandle.getUserId(uid))); + } + + private void setStoppedState(int uid, String pkgName, boolean stopped) { + Intent intent = new Intent( + stopped ? Intent.ACTION_PACKAGE_RESTARTED : Intent.ACTION_PACKAGE_UNSTOPPED); + intent.putExtra(Intent.EXTRA_UID, uid); + intent.setData(Uri.fromParts(IntentFilter.SCHEME_PACKAGE, pkgName, null)); + mStoppedReceiver.onReceive(mContext, intent); + } + + private void setUidBias(int uid, int bias) { + int prevBias = mJobSchedulerService.getUidBias(uid); + doReturn(bias).when(mJobSchedulerService).getUidBias(uid); + synchronized (mBackgroundJobsController.mLock) { + mBackgroundJobsController.onUidBiasChangedLocked(uid, prevBias, bias); + } + } + + private void trackJobs(JobStatus... jobs) { + for (JobStatus job : jobs) { + mJobStore.add(job); + synchronized (mBackgroundJobsController.mLock) { + mBackgroundJobsController.maybeStartTrackingJobLocked(job, null); + } + } + } + + private JobInfo.Builder createBaseJobInfoBuilder(String pkgName, int jobId) { + final ComponentName cn = spy(new ComponentName(pkgName, "TestBJCJobService")); + doReturn("TestBJCJobService").when(cn).flattenToShortString(); + return new JobInfo.Builder(jobId, cn); + } + + private JobStatus createJobStatus(String testTag, String packageName, int callingUid, + JobInfo jobInfo) { + JobStatus js = JobStatus.createFromJobInfo( + jobInfo, callingUid, packageName, SOURCE_USER_ID, "BJCTest", testTag); + js.serviceProcessName = "testProcess"; + // Make sure tests aren't passing just because the default bucket is likely ACTIVE. + js.setStandbyBucket(FREQUENT_INDEX); + return js; + } + + @Test + public void testStopped_disabled() { + mSetFlagsRule.disableFlags(android.content.pm.Flags.FLAG_STAY_STOPPED); + // Scheduled by SOURCE_UID:SOURCE_PACKAGE for itself. + JobStatus directJob1 = createJobStatus("testStopped", SOURCE_PACKAGE, SOURCE_UID, + createBaseJobInfoBuilder(SOURCE_PACKAGE, 1).build()); + // Scheduled by ALTERNATE_UID:ALTERNATE_SOURCE_PACKAGE for itself. + JobStatus directJob2 = createJobStatus("testStopped", + ALTERNATE_SOURCE_PACKAGE, ALTERNATE_UID, + createBaseJobInfoBuilder(ALTERNATE_SOURCE_PACKAGE, 2).build()); + // Scheduled by CALLING_PACKAGE for SOURCE_PACKAGE. + JobStatus proxyJob1 = createJobStatus("testStopped", SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(CALLING_PACKAGE, 3).build()); + // Scheduled by CALLING_PACKAGE for ALTERNATE_SOURCE_PACKAGE. + JobStatus proxyJob2 = createJobStatus("testStopped", + ALTERNATE_SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(CALLING_PACKAGE, 4).build()); + + trackJobs(directJob1, directJob2, proxyJob1, proxyJob2); + + setStoppedState(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE, true); + assertTrue(directJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob1.isUserBgRestricted()); + assertTrue(directJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob2.isUserBgRestricted()); + assertTrue(proxyJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob1.isUserBgRestricted()); + assertTrue(proxyJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob2.isUserBgRestricted()); + + setStoppedState(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE, false); + assertTrue(directJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob1.isUserBgRestricted()); + assertTrue(directJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob2.isUserBgRestricted()); + assertTrue(proxyJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob1.isUserBgRestricted()); + assertTrue(proxyJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob2.isUserBgRestricted()); + } + + @Test + public void testStopped_enabled() { + mSetFlagsRule.enableFlags(android.content.pm.Flags.FLAG_STAY_STOPPED); + // Scheduled by SOURCE_UID:SOURCE_PACKAGE for itself. + JobStatus directJob1 = createJobStatus("testStopped", SOURCE_PACKAGE, SOURCE_UID, + createBaseJobInfoBuilder(SOURCE_PACKAGE, 1).build()); + // Scheduled by ALTERNATE_UID:ALTERNATE_SOURCE_PACKAGE for itself. + JobStatus directJob2 = createJobStatus("testStopped", + ALTERNATE_SOURCE_PACKAGE, ALTERNATE_UID, + createBaseJobInfoBuilder(ALTERNATE_SOURCE_PACKAGE, 2).build()); + // Scheduled by CALLING_PACKAGE for SOURCE_PACKAGE. + JobStatus proxyJob1 = createJobStatus("testStopped", SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(CALLING_PACKAGE, 3).build()); + // Scheduled by CALLING_PACKAGE for ALTERNATE_SOURCE_PACKAGE. + JobStatus proxyJob2 = createJobStatus("testStopped", + ALTERNATE_SOURCE_PACKAGE, CALLING_UID, + createBaseJobInfoBuilder(CALLING_PACKAGE, 4).build()); + + trackJobs(directJob1, directJob2, proxyJob1, proxyJob2); + + setStoppedState(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE, true); + assertTrue(directJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob1.isUserBgRestricted()); + assertFalse(directJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertTrue(directJob2.isUserBgRestricted()); + assertTrue(proxyJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob1.isUserBgRestricted()); + assertFalse(proxyJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertTrue(proxyJob2.isUserBgRestricted()); + + setStoppedState(ALTERNATE_UID, ALTERNATE_SOURCE_PACKAGE, false); + assertTrue(directJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob1.isUserBgRestricted()); + assertTrue(directJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(directJob2.isUserBgRestricted()); + assertTrue(proxyJob1.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob1.isUserBgRestricted()); + assertTrue(proxyJob2.isConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED)); + assertFalse(proxyJob2.isUserBgRestricted()); + } +} |