diff options
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 7e142a5ddd7b..83db4cbb7e43 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 5c6c49a18caa..23a5d4d20a2e 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()); + } +} |