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
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 f97100b..dae03b0 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 @@
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 @@
}
};
- 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 cd3ba6b..4aadc90 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 @@
*
* - 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 @@
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 @@
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 @@
}
@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 @@
}
}
+ 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 @@
&& 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 b7480649..d1f575e 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 @@
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 38bcfa2..cd2aa19 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -2805,7 +2805,7 @@
* 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 cd3f0f0..1da7f0c 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 @@
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.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 @@
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 0000000..cdae8c6
--- /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());
+ }
+}