Merge "Standby exemption for system gallery's backup jobs"
This commit is contained in:
committed by
Android (Google) Code Review
commit
0cf9ed29dc
@@ -896,7 +896,7 @@ public class JobInfo implements Parcelable {
|
||||
* @param flags Flags for the observer.
|
||||
*/
|
||||
public TriggerContentUri(@NonNull Uri uri, @Flags int flags) {
|
||||
mUri = uri;
|
||||
mUri = Objects.requireNonNull(uri);
|
||||
mFlags = flags;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.server.job;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.job.JobInfo;
|
||||
import android.util.proto.ProtoOutputStream;
|
||||
|
||||
@@ -44,6 +45,10 @@ public interface JobSchedulerInternal {
|
||||
void removeBackingUpUid(int uid);
|
||||
void clearAllBackingUpUids();
|
||||
|
||||
/** Returns the package responsible for backing up media on the device. */
|
||||
@NonNull
|
||||
String getMediaBackupPackage();
|
||||
|
||||
/**
|
||||
* The user has started interacting with the app. Take any appropriate action.
|
||||
*/
|
||||
|
||||
@@ -77,6 +77,7 @@ import android.util.SparseIntArray;
|
||||
import android.util.TimeUtils;
|
||||
import android.util.proto.ProtoOutputStream;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.app.IBatteryStats;
|
||||
import com.android.internal.util.ArrayUtils;
|
||||
@@ -248,6 +249,9 @@ public class JobSchedulerService extends com.android.server.SystemService
|
||||
*/
|
||||
private final List<JobRestriction> mJobRestrictions;
|
||||
|
||||
@NonNull
|
||||
private final String mSystemGalleryPackage;
|
||||
|
||||
private final CountQuotaTracker mQuotaTracker;
|
||||
private static final String QUOTA_TRACKER_SCHEDULE_PERSISTED_TAG = ".schedulePersisted()";
|
||||
|
||||
@@ -1394,6 +1398,9 @@ public class JobSchedulerService extends com.android.server.SystemService
|
||||
mJobRestrictions = new ArrayList<>();
|
||||
mJobRestrictions.add(new ThermalStatusRestriction(this));
|
||||
|
||||
mSystemGalleryPackage = Objects.requireNonNull(
|
||||
context.getString(R.string.config_systemGallery));
|
||||
|
||||
// If the job store determined that it can't yet reschedule persisted jobs,
|
||||
// we need to start watching the clock.
|
||||
if (!mJobs.jobTimesInflatedValid()) {
|
||||
@@ -2358,6 +2365,11 @@ public class JobSchedulerService extends com.android.server.SystemService
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMediaBackupPackage() {
|
||||
return mSystemGalleryPackage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportAppUsage(String packageName, int userId) {
|
||||
JobSchedulerService.this.reportAppUsage(packageName, userId);
|
||||
|
||||
@@ -344,7 +344,7 @@ public final class ContentObserverController extends StateController {
|
||||
mContext.getContentResolver().unregisterContentObserver(obs);
|
||||
ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observerOfUser =
|
||||
mObservers.get(obs.mUserId);
|
||||
if (observerOfUser != null) {
|
||||
if (observerOfUser != null) {
|
||||
observerOfUser.remove(obs.mUri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.android.server.job.controllers;
|
||||
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
|
||||
import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
|
||||
import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
|
||||
import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
|
||||
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
|
||||
|
||||
import android.app.AppGlobals;
|
||||
@@ -30,6 +31,7 @@ import android.net.Network;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Pair;
|
||||
@@ -207,6 +209,18 @@ public final class JobStatus {
|
||||
*/
|
||||
private int mDynamicConstraints = 0;
|
||||
|
||||
/**
|
||||
* Indicates whether the job is responsible for backing up media, so we can be lenient in
|
||||
* applying standby throttling.
|
||||
*
|
||||
* Doesn't exempt jobs with a deadline constraint, as they can be started without any content or
|
||||
* network changes, in which case this exemption does not make sense.
|
||||
*
|
||||
* TODO(b/149519887): Use a more explicit signal, maybe an API flag, that the scheduling package
|
||||
* needs to provide at the time of scheduling a job.
|
||||
*/
|
||||
private final boolean mHasMediaBackupExemption;
|
||||
|
||||
// Set to true if doze constraint was satisfied due to app being whitelisted.
|
||||
public boolean dozeWhitelisted;
|
||||
|
||||
@@ -415,9 +429,11 @@ public final class JobStatus {
|
||||
this.mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
|
||||
this.numFailures = numFailures;
|
||||
|
||||
boolean requiresNetwork = false;
|
||||
int requiredConstraints = job.getConstraintFlags();
|
||||
if (job.getRequiredNetwork() != null) {
|
||||
requiredConstraints |= CONSTRAINT_CONNECTIVITY;
|
||||
requiresNetwork = true;
|
||||
}
|
||||
if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME) {
|
||||
requiredConstraints |= CONSTRAINT_TIMING_DELAY;
|
||||
@@ -425,8 +441,16 @@ public final class JobStatus {
|
||||
if (latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) {
|
||||
requiredConstraints |= CONSTRAINT_DEADLINE;
|
||||
}
|
||||
boolean mediaOnly = false;
|
||||
if (job.getTriggerContentUris() != null) {
|
||||
requiredConstraints |= CONSTRAINT_CONTENT_TRIGGER;
|
||||
mediaOnly = true;
|
||||
for (JobInfo.TriggerContentUri uri : job.getTriggerContentUris()) {
|
||||
if (!MediaStore.AUTHORITY.equals(uri.getUri().getAuthority())) {
|
||||
mediaOnly = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.requiredConstraints = requiredConstraints;
|
||||
mRequiredConstraintsOfInterest = requiredConstraints & CONSTRAINTS_OF_INTEREST;
|
||||
@@ -450,6 +474,9 @@ public final class JobStatus {
|
||||
// our source UID into place.
|
||||
job.getRequiredNetwork().networkCapabilities.setSingleUid(this.sourceUid);
|
||||
}
|
||||
final JobSchedulerInternal jsi = LocalServices.getService(JobSchedulerInternal.class);
|
||||
mHasMediaBackupExemption = !job.hasLateConstraint() && mediaOnly && requiresNetwork
|
||||
&& this.sourcePackageName.equals(jsi.getMediaBackupPackage());
|
||||
}
|
||||
|
||||
/** Copy constructor: used specifically when cloning JobStatus objects for persistence,
|
||||
@@ -545,7 +572,6 @@ public final class JobStatus {
|
||||
|
||||
int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage,
|
||||
sourceUserId, elapsedNow);
|
||||
JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class);
|
||||
return new JobStatus(job, callingUid, sourcePkg, sourceUserId,
|
||||
standbyBucket, tag, 0,
|
||||
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
|
||||
@@ -734,7 +760,14 @@ public final class JobStatus {
|
||||
// like other ACTIVE apps.
|
||||
return ACTIVE_INDEX;
|
||||
}
|
||||
return getStandbyBucket();
|
||||
final int actualBucket = getStandbyBucket();
|
||||
if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX
|
||||
&& mHasMediaBackupExemption) {
|
||||
// Cap it at WORKING_INDEX as media back up jobs are important to the user, and the
|
||||
// source package may not have been used directly in a while.
|
||||
return Math.min(WORKING_INDEX, actualBucket);
|
||||
}
|
||||
return actualBucket;
|
||||
}
|
||||
|
||||
/** Returns the real standby bucket of the job. */
|
||||
|
||||
@@ -19,6 +19,12 @@ 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.server.job.JobSchedulerService.ACTIVE_INDEX;
|
||||
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
|
||||
import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
|
||||
import static com.android.server.job.JobSchedulerService.RARE_INDEX;
|
||||
import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
|
||||
import static com.android.server.job.JobSchedulerService.WORKING_INDEX;
|
||||
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED;
|
||||
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW;
|
||||
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING;
|
||||
@@ -34,13 +40,16 @@ import static com.android.server.job.controllers.JobStatus.CONSTRAINT_WITHIN_QUO
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.job.JobInfo;
|
||||
import android.app.usage.UsageStatsManagerInternal;
|
||||
import android.content.ComponentName;
|
||||
import android.content.pm.PackageManagerInternal;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.MediaStore;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
@@ -52,6 +61,7 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoSession;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
@@ -61,7 +71,12 @@ import java.time.ZoneOffset;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class JobStatusTest {
|
||||
private static final double DELTA = 0.00001;
|
||||
private static final String TEST_PACKAGE = "job.test.package";
|
||||
private static final ComponentName TEST_JOB_COMPONENT = new ComponentName(TEST_PACKAGE, "test");
|
||||
private static final Uri TEST_MEDIA_URI = Uri.parse("content://media/path/to/media");
|
||||
|
||||
@Mock
|
||||
private JobSchedulerInternal mJobSchedulerInternal;
|
||||
private MockitoSession mMockingSession;
|
||||
|
||||
@Before
|
||||
@@ -71,7 +86,7 @@ public class JobStatusTest {
|
||||
.strictness(Strictness.LENIENT)
|
||||
.mockStatic(LocalServices.class)
|
||||
.startMocking();
|
||||
doReturn(mock(JobSchedulerInternal.class))
|
||||
doReturn(mJobSchedulerInternal)
|
||||
.when(() -> LocalServices.getService(JobSchedulerInternal.class));
|
||||
doReturn(mock(PackageManagerInternal.class))
|
||||
.when(() -> LocalServices.getService(PackageManagerInternal.class));
|
||||
@@ -94,6 +109,82 @@ public class JobStatusTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertEffectiveBucketForMediaExemption(JobStatus jobStatus,
|
||||
boolean exemptionGranted) {
|
||||
final SparseIntArray effectiveBucket = new SparseIntArray();
|
||||
effectiveBucket.put(ACTIVE_INDEX, ACTIVE_INDEX);
|
||||
effectiveBucket.put(WORKING_INDEX, WORKING_INDEX);
|
||||
effectiveBucket.put(FREQUENT_INDEX, exemptionGranted ? WORKING_INDEX : FREQUENT_INDEX);
|
||||
effectiveBucket.put(RARE_INDEX, exemptionGranted ? WORKING_INDEX : RARE_INDEX);
|
||||
effectiveBucket.put(NEVER_INDEX, NEVER_INDEX);
|
||||
effectiveBucket.put(RESTRICTED_INDEX, RESTRICTED_INDEX);
|
||||
for (int i = 0; i < effectiveBucket.size(); i++) {
|
||||
jobStatus.setStandbyBucket(effectiveBucket.keyAt(i));
|
||||
assertEquals(effectiveBucket.valueAt(i), jobStatus.getEffectiveStandbyBucket());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMediaBackupExemption_lateConstraint() {
|
||||
final JobInfo triggerContentJob = new JobInfo.Builder(42, TEST_JOB_COMPONENT)
|
||||
.addTriggerContentUri(new JobInfo.TriggerContentUri(TEST_MEDIA_URI, 0))
|
||||
.setOverrideDeadline(12)
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
.build();
|
||||
when(mJobSchedulerInternal.getMediaBackupPackage()).thenReturn(TEST_PACKAGE);
|
||||
assertEffectiveBucketForMediaExemption(createJobStatus(triggerContentJob), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMediaBackupExemption_noConnectivityConstraint() {
|
||||
final JobInfo triggerContentJob = new JobInfo.Builder(42, TEST_JOB_COMPONENT)
|
||||
.addTriggerContentUri(new JobInfo.TriggerContentUri(TEST_MEDIA_URI, 0))
|
||||
.build();
|
||||
when(mJobSchedulerInternal.getMediaBackupPackage()).thenReturn(TEST_PACKAGE);
|
||||
assertEffectiveBucketForMediaExemption(createJobStatus(triggerContentJob), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMediaBackupExemption_noContentTriggerConstraint() {
|
||||
final JobInfo networkJob = new JobInfo.Builder(42, TEST_JOB_COMPONENT)
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
.build();
|
||||
when(mJobSchedulerInternal.getMediaBackupPackage()).thenReturn(TEST_PACKAGE);
|
||||
assertEffectiveBucketForMediaExemption(createJobStatus(networkJob), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMediaBackupExemption_wrongSourcePackage() {
|
||||
final JobInfo networkContentJob = new JobInfo.Builder(42, TEST_JOB_COMPONENT)
|
||||
.addTriggerContentUri(new JobInfo.TriggerContentUri(TEST_MEDIA_URI, 0))
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
.build();
|
||||
when(mJobSchedulerInternal.getMediaBackupPackage()).thenReturn("not.test.package");
|
||||
assertEffectiveBucketForMediaExemption(createJobStatus(networkContentJob), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMediaBackupExemption_nonMediaUri() {
|
||||
final Uri nonMediaUri = Uri.parse("content://not-media/any/path");
|
||||
final JobInfo networkContentJob = new JobInfo.Builder(42, TEST_JOB_COMPONENT)
|
||||
.addTriggerContentUri(new JobInfo.TriggerContentUri(TEST_MEDIA_URI, 0))
|
||||
.addTriggerContentUri(new JobInfo.TriggerContentUri(nonMediaUri, 0))
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
.build();
|
||||
when(mJobSchedulerInternal.getMediaBackupPackage()).thenReturn(TEST_PACKAGE);
|
||||
assertEffectiveBucketForMediaExemption(createJobStatus(networkContentJob), false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMediaBackupExemptionGranted() {
|
||||
final JobInfo networkContentJob = new JobInfo.Builder(42, TEST_JOB_COMPONENT)
|
||||
.addTriggerContentUri(new JobInfo.TriggerContentUri(TEST_MEDIA_URI, 0))
|
||||
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||
.build();
|
||||
when(mJobSchedulerInternal.getMediaBackupPackage()).thenReturn(TEST_PACKAGE);
|
||||
assertEffectiveBucketForMediaExemption(createJobStatus(networkContentJob), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFraction() throws Exception {
|
||||
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
|
||||
|
||||
Reference in New Issue
Block a user