Move the job scheduler service code to its own jar file.
- Also remove the dependency from SystemServiceRegistry to JobScheduler
See apex/jobscheduler/README_js-mainline.md for the details.
Bug: 137763703
Test: build and boot
Test: atest CtsJobSchedulerTestCases
Change-Id: I2386c78b7a6085d6e543a63f22cb620c4cabd06a
diff --git a/apex/jobscheduler/README_js-mainline.md b/apex/jobscheduler/README_js-mainline.md
new file mode 100644
index 0000000..b5fea5e
--- /dev/null
+++ b/apex/jobscheduler/README_js-mainline.md
@@ -0,0 +1,51 @@
+# Making Job Scheduler into a Mainline Module
+
+## TODOs
+
+See also:
+- http://go/moving-js-code-for-mainline
+- http://go/jobscheduler-code-dependencies-2019-07
+
+- [ ] Move client code
+ - [ ] Move code
+ - [ ] Make build file
+ - [ ] "m jobscheduler-framework" pass
+ - [ ] "m framework" pass
+ - [ ] "m service" pass
+- [ ] Move proto
+ - No, couldn't do it, because it's referred to by incidentd_proto
+- [ ] Move service
+ - [X] Move code (done, but it won't compile yet)
+ - [X] Make build file
+ - [X] "m service" pass
+ - [X] "m jobscheduler-service" pass
+ - To make it pass, jobscheduler-service has to link services.jar too. Many dependencies.
+- [ ] Move this into `frameworks/apex/jobscheduler/...`. Currently it's in `frameworks/base/apex/...`
+because `frameworks/apex/` is not a part of any git projects. (and also working on multiple
+projects is a pain.)
+
+
+## Problems
+- Couldn't move dumpsys proto files. They are used by incidentd_proto, which is in the platform
+ (not updatable).
+ - One idea is *not* to move the proto files into apex but keep them in the platform.
+ Then we make sure to extend the proto files in a backward-compat way (which we do anyway)
+ and always use the latest file from the JS apex.
+
+- There are a lot of build tasks that use "framework.jar". (Examples: hiddenapi-greylist.txt check,
+ update-api / public API check and SDK stub (android.jar) creation)
+ To make the downstream build modules buildable, we need to include js-framework.jar in
+ framework.jar. However it turned out to be tricky because soong has special logic for "framework"
+ and "framework.jar".
+ i.e. Conceptually, we can do it by renaming `framework` to `framework-minus-jobscheduler`, build
+ `jobscheduler-framework` with `framework-minus-jobscheduler`, and create `framework` by merging
+ `framework-minus-jobscheduler` and `jobscheduler-framework`.
+ However it didn't quite work because of the special casing.
+
+- JS-service uses a lot of other code in `services`, so it needs to link services.core.jar e.g.
+ - Common system service code, e.g. `com.android.server.SystemService`
+ - Common utility code, e.g. `FgThread` and `IoThread`
+ - Other system services such as `DeviceIdleController` and `ActivityManagerService`
+ - Server side singleton. `AppStateTracker`
+ - `DeviceIdleController.LocalService`, which is a local service but there's no interface class.
+ - `XxxInternal` interfaces that are not in the framework side. -> We should be able to move them.
diff --git a/apex/jobscheduler/service/Android.bp b/apex/jobscheduler/service/Android.bp
new file mode 100644
index 0000000..ca6dc45
--- /dev/null
+++ b/apex/jobscheduler/service/Android.bp
@@ -0,0 +1,15 @@
+// Job Scheduler Service jar, which will eventually be put in the jobscheduler mainline apex.
+// jobscheduler-service needs to be added to PRODUCT_SYSTEM_SERVER_JARS.
+java_library {
+ name: "jobscheduler-service",
+ installable: true,
+
+ srcs: [
+ "java/**/*.java",
+ ],
+
+ libs: [
+ "framework",
+ "services.core",
+ ],
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java b/apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java
new file mode 100644
index 0000000..005b189
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/GrantedUriPermissions.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.app.IActivityManager;
+import android.app.UriGrantsManager;
+import android.content.ClipData;
+import android.content.ContentProvider;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+import com.android.server.LocalServices;
+import com.android.server.uri.UriGrantsManagerInternal;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+public final class GrantedUriPermissions {
+ private final int mGrantFlags;
+ private final int mSourceUserId;
+ private final String mTag;
+ private final IBinder mPermissionOwner;
+ private final ArrayList<Uri> mUris = new ArrayList<>();
+
+ private GrantedUriPermissions(IActivityManager am, int grantFlags, int uid, String tag)
+ throws RemoteException {
+ mGrantFlags = grantFlags;
+ mSourceUserId = UserHandle.getUserId(uid);
+ mTag = tag;
+ mPermissionOwner = LocalServices
+ .getService(UriGrantsManagerInternal.class).newUriPermissionOwner("job: " + tag);
+ }
+
+ public void revoke(IActivityManager am) {
+ for (int i = mUris.size()-1; i >= 0; i--) {
+ LocalServices.getService(UriGrantsManagerInternal.class).revokeUriPermissionFromOwner(
+ mPermissionOwner, mUris.get(i), mGrantFlags, mSourceUserId);
+ }
+ mUris.clear();
+ }
+
+ public static boolean checkGrantFlags(int grantFlags) {
+ return (grantFlags & (Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ |Intent.FLAG_GRANT_READ_URI_PERMISSION)) != 0;
+ }
+
+ public static GrantedUriPermissions createFromIntent(IActivityManager am, Intent intent,
+ int sourceUid, String targetPackage, int targetUserId, String tag) {
+ int grantFlags = intent.getFlags();
+ if (!checkGrantFlags(grantFlags)) {
+ return null;
+ }
+
+ GrantedUriPermissions perms = null;
+
+ Uri data = intent.getData();
+ if (data != null) {
+ perms = grantUri(am, data, sourceUid, targetPackage, targetUserId, grantFlags, tag,
+ perms);
+ }
+
+ ClipData clip = intent.getClipData();
+ if (clip != null) {
+ perms = grantClip(am, clip, sourceUid, targetPackage, targetUserId, grantFlags, tag,
+ perms);
+ }
+
+ return perms;
+ }
+
+ public static GrantedUriPermissions createFromClip(IActivityManager am, ClipData clip,
+ int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag) {
+ if (!checkGrantFlags(grantFlags)) {
+ return null;
+ }
+ GrantedUriPermissions perms = null;
+ if (clip != null) {
+ perms = grantClip(am, clip, sourceUid, targetPackage, targetUserId, grantFlags,
+ tag, perms);
+ }
+ return perms;
+ }
+
+ private static GrantedUriPermissions grantClip(IActivityManager am, ClipData clip,
+ int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag,
+ GrantedUriPermissions curPerms) {
+ final int N = clip.getItemCount();
+ for (int i = 0; i < N; i++) {
+ curPerms = grantItem(am, clip.getItemAt(i), sourceUid, targetPackage, targetUserId,
+ grantFlags, tag, curPerms);
+ }
+ return curPerms;
+ }
+
+ private static GrantedUriPermissions grantUri(IActivityManager am, Uri uri,
+ int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag,
+ GrantedUriPermissions curPerms) {
+ try {
+ int sourceUserId = ContentProvider.getUserIdFromUri(uri,
+ UserHandle.getUserId(sourceUid));
+ uri = ContentProvider.getUriWithoutUserId(uri);
+ if (curPerms == null) {
+ curPerms = new GrantedUriPermissions(am, grantFlags, sourceUid, tag);
+ }
+ UriGrantsManager.getService().grantUriPermissionFromOwner(curPerms.mPermissionOwner,
+ sourceUid, targetPackage, uri, grantFlags, sourceUserId, targetUserId);
+ curPerms.mUris.add(uri);
+ } catch (RemoteException e) {
+ Slog.e("JobScheduler", "AM dead");
+ }
+ return curPerms;
+ }
+
+ private static GrantedUriPermissions grantItem(IActivityManager am, ClipData.Item item,
+ int sourceUid, String targetPackage, int targetUserId, int grantFlags, String tag,
+ GrantedUriPermissions curPerms) {
+ if (item.getUri() != null) {
+ curPerms = grantUri(am, item.getUri(), sourceUid, targetPackage, targetUserId,
+ grantFlags, tag, curPerms);
+ }
+ Intent intent = item.getIntent();
+ if (intent != null && intent.getData() != null) {
+ curPerms = grantUri(am, intent.getData(), sourceUid, targetPackage, targetUserId,
+ grantFlags, tag, curPerms);
+ }
+ return curPerms;
+ }
+
+ // Dumpsys infrastructure
+ public void dump(PrintWriter pw, String prefix) {
+ pw.print(prefix); pw.print("mGrantFlags=0x"); pw.print(Integer.toHexString(mGrantFlags));
+ pw.print(" mSourceUserId="); pw.println(mSourceUserId);
+ pw.print(prefix); pw.print("mTag="); pw.println(mTag);
+ pw.print(prefix); pw.print("mPermissionOwner="); pw.println(mPermissionOwner);
+ for (int i = 0; i < mUris.size(); i++) {
+ pw.print(prefix); pw.print("#"); pw.print(i); pw.print(": ");
+ pw.println(mUris.get(i));
+ }
+ }
+
+ public void dump(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(GrantedUriPermissionsDumpProto.FLAGS, mGrantFlags);
+ proto.write(GrantedUriPermissionsDumpProto.SOURCE_USER_ID, mSourceUserId);
+ proto.write(GrantedUriPermissionsDumpProto.TAG, mTag);
+ proto.write(GrantedUriPermissionsDumpProto.PERMISSION_OWNER, mPermissionOwner.toString());
+ for (int i = 0; i < mUris.size(); i++) {
+ Uri u = mUris.get(i);
+ if (u != null) {
+ proto.write(GrantedUriPermissionsDumpProto.URIS, u.toString());
+ }
+ }
+
+ proto.end(token);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java
new file mode 100644
index 0000000..34ba753b3
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobCompletedListener.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 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;
+
+import com.android.server.job.controllers.JobStatus;
+
+/**
+ * Used for communication between {@link com.android.server.job.JobServiceContext} and the
+ * {@link com.android.server.job.JobSchedulerService}.
+ */
+public interface JobCompletedListener {
+ /**
+ * Callback for when a job is completed.
+ * @param needsReschedule Whether the implementing class should reschedule this job.
+ */
+ void onJobCompletedLocked(JobStatus jobStatus, boolean needsReschedule);
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
new file mode 100644
index 0000000..bec1947
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -0,0 +1,722 @@
+/*
+ * 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;
+
+import android.app.ActivityManager;
+import android.app.job.JobInfo;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.procstats.ProcessStats;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.StatLogger;
+import com.android.server.job.JobSchedulerService.Constants;
+import com.android.server.job.JobSchedulerService.MaxJobCountsPerMemoryTrimLevel;
+import com.android.server.job.controllers.JobStatus;
+import com.android.server.job.controllers.StateController;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * This class decides, given the various configuration and the system status, how many more jobs
+ * can start.
+ */
+class JobConcurrencyManager {
+ private static final String TAG = JobSchedulerService.TAG;
+ private static final boolean DEBUG = JobSchedulerService.DEBUG;
+
+ private final Object mLock;
+ private final JobSchedulerService mService;
+ private final JobSchedulerService.Constants mConstants;
+ private final Context mContext;
+ private final Handler mHandler;
+
+ private PowerManager mPowerManager;
+
+ private boolean mCurrentInteractiveState;
+ private boolean mEffectiveInteractiveState;
+
+ private long mLastScreenOnRealtime;
+ private long mLastScreenOffRealtime;
+
+ private static final int MAX_JOB_CONTEXTS_COUNT = JobSchedulerService.MAX_JOB_CONTEXTS_COUNT;
+
+ /**
+ * This array essentially stores the state of mActiveServices array.
+ * The ith index stores the job present on the ith JobServiceContext.
+ * We manipulate this array until we arrive at what jobs should be running on
+ * what JobServiceContext.
+ */
+ JobStatus[] mRecycledAssignContextIdToJobMap = new JobStatus[MAX_JOB_CONTEXTS_COUNT];
+
+ boolean[] mRecycledSlotChanged = new boolean[MAX_JOB_CONTEXTS_COUNT];
+
+ int[] mRecycledPreferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT];
+
+ /** Max job counts according to the current system state. */
+ private JobSchedulerService.MaxJobCounts mMaxJobCounts;
+
+ private final JobCountTracker mJobCountTracker = new JobCountTracker();
+
+ /** Current memory trim level. */
+ private int mLastMemoryTrimLevel;
+
+ /** Used to throttle heavy API calls. */
+ private long mNextSystemStateRefreshTime;
+ private static final int SYSTEM_STATE_REFRESH_MIN_INTERVAL = 1000;
+
+ private final StatLogger mStatLogger = new StatLogger(new String[]{
+ "assignJobsToContexts",
+ "refreshSystemState",
+ });
+
+ interface Stats {
+ int ASSIGN_JOBS_TO_CONTEXTS = 0;
+ int REFRESH_SYSTEM_STATE = 1;
+
+ int COUNT = REFRESH_SYSTEM_STATE + 1;
+ }
+
+ JobConcurrencyManager(JobSchedulerService service) {
+ mService = service;
+ mLock = mService.mLock;
+ mConstants = service.mConstants;
+ mContext = service.getContext();
+
+ mHandler = BackgroundThread.getHandler();
+ }
+
+ public void onSystemReady() {
+ mPowerManager = mContext.getSystemService(PowerManager.class);
+
+ final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ mContext.registerReceiver(mReceiver, filter);
+
+ onInteractiveStateChanged(mPowerManager.isInteractive());
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case Intent.ACTION_SCREEN_ON:
+ onInteractiveStateChanged(true);
+ break;
+ case Intent.ACTION_SCREEN_OFF:
+ onInteractiveStateChanged(false);
+ break;
+ }
+ }
+ };
+
+ /**
+ * Called when the screen turns on / off.
+ */
+ private void onInteractiveStateChanged(boolean interactive) {
+ synchronized (mLock) {
+ if (mCurrentInteractiveState == interactive) {
+ return;
+ }
+ mCurrentInteractiveState = interactive;
+ if (DEBUG) {
+ Slog.d(TAG, "Interactive: " + interactive);
+ }
+
+ final long nowRealtime = JobSchedulerService.sElapsedRealtimeClock.millis();
+ if (interactive) {
+ mLastScreenOnRealtime = nowRealtime;
+ mEffectiveInteractiveState = true;
+
+ mHandler.removeCallbacks(mRampUpForScreenOff);
+ } else {
+ mLastScreenOffRealtime = nowRealtime;
+
+ // Set mEffectiveInteractiveState to false after the delay, when we may increase
+ // the concurrency.
+ // We don't need a wakeup alarm here. When there's a pending job, there should
+ // also be jobs running too, meaning the device should be awake.
+
+ // Note: we can't directly do postDelayed(this::rampUpForScreenOn), because
+ // we need the exact same instance for removeCallbacks().
+ mHandler.postDelayed(mRampUpForScreenOff,
+ mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue());
+ }
+ }
+ }
+
+ private final Runnable mRampUpForScreenOff = this::rampUpForScreenOff;
+
+ /**
+ * Called in {@link Constants#SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS} after
+ * the screen turns off, in order to increase concurrency.
+ */
+ private void rampUpForScreenOff() {
+ synchronized (mLock) {
+ // Make sure the screen has really been off for the configured duration.
+ // (There could be a race.)
+ if (!mEffectiveInteractiveState) {
+ return;
+ }
+ if (mLastScreenOnRealtime > mLastScreenOffRealtime) {
+ return;
+ }
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ if ((mLastScreenOffRealtime
+ + mConstants.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.getValue())
+ > now) {
+ return;
+ }
+
+ mEffectiveInteractiveState = false;
+
+ if (DEBUG) {
+ Slog.d(TAG, "Ramping up concurrency");
+ }
+
+ mService.maybeRunPendingJobsLocked();
+ }
+ }
+
+ private boolean isFgJob(JobStatus job) {
+ return job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP;
+ }
+
+ @GuardedBy("mLock")
+ private void refreshSystemStateLocked() {
+ final long nowUptime = JobSchedulerService.sUptimeMillisClock.millis();
+
+ // Only refresh the information every so often.
+ if (nowUptime < mNextSystemStateRefreshTime) {
+ return;
+ }
+
+ final long start = mStatLogger.getTime();
+ mNextSystemStateRefreshTime = nowUptime + SYSTEM_STATE_REFRESH_MIN_INTERVAL;
+
+ mLastMemoryTrimLevel = ProcessStats.ADJ_MEM_FACTOR_NORMAL;
+ try {
+ mLastMemoryTrimLevel = ActivityManager.getService().getMemoryTrimLevel();
+ } catch (RemoteException e) {
+ }
+
+ mStatLogger.logDurationStat(Stats.REFRESH_SYSTEM_STATE, start);
+ }
+
+ @GuardedBy("mLock")
+ private void updateMaxCountsLocked() {
+ refreshSystemStateLocked();
+
+ final MaxJobCountsPerMemoryTrimLevel jobCounts = mEffectiveInteractiveState
+ ? mConstants.MAX_JOB_COUNTS_SCREEN_ON
+ : mConstants.MAX_JOB_COUNTS_SCREEN_OFF;
+
+
+ switch (mLastMemoryTrimLevel) {
+ case ProcessStats.ADJ_MEM_FACTOR_MODERATE:
+ mMaxJobCounts = jobCounts.moderate;
+ break;
+ case ProcessStats.ADJ_MEM_FACTOR_LOW:
+ mMaxJobCounts = jobCounts.low;
+ break;
+ case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
+ mMaxJobCounts = jobCounts.critical;
+ break;
+ default:
+ mMaxJobCounts = jobCounts.normal;
+ break;
+ }
+ }
+
+ /**
+ * Takes jobs from pending queue and runs them on available contexts.
+ * If no contexts are available, preempts lower priority jobs to
+ * run higher priority ones.
+ * Lock on mJobs before calling this function.
+ */
+ @GuardedBy("mLock")
+ void assignJobsToContextsLocked() {
+ final long start = mStatLogger.getTime();
+
+ assignJobsToContextsInternalLocked();
+
+ mStatLogger.logDurationStat(Stats.ASSIGN_JOBS_TO_CONTEXTS, start);
+ }
+
+ @GuardedBy("mLock")
+ private void assignJobsToContextsInternalLocked() {
+ if (DEBUG) {
+ Slog.d(TAG, printPendingQueueLocked());
+ }
+
+ final JobPackageTracker tracker = mService.mJobPackageTracker;
+ final List<JobStatus> pendingJobs = mService.mPendingJobs;
+ final List<JobServiceContext> activeServices = mService.mActiveServices;
+ final List<StateController> controllers = mService.mControllers;
+
+ updateMaxCountsLocked();
+
+ // To avoid GC churn, we recycle the arrays.
+ JobStatus[] contextIdToJobMap = mRecycledAssignContextIdToJobMap;
+ boolean[] slotChanged = mRecycledSlotChanged;
+ int[] preferredUidForContext = mRecycledPreferredUidForContext;
+
+
+ // Initialize the work variables and also count running jobs.
+ mJobCountTracker.reset(
+ mMaxJobCounts.getMaxTotal(),
+ mMaxJobCounts.getMaxBg(),
+ mMaxJobCounts.getMinBg());
+
+ for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
+ final JobServiceContext js = mService.mActiveServices.get(i);
+ final JobStatus status = js.getRunningJobLocked();
+
+ if ((contextIdToJobMap[i] = status) != null) {
+ mJobCountTracker.incrementRunningJobCount(isFgJob(status));
+ }
+
+ slotChanged[i] = false;
+ preferredUidForContext[i] = js.getPreferredUid();
+ }
+ if (DEBUG) {
+ Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial"));
+ }
+
+ // Next, update the job priorities, and also count the pending FG / BG jobs.
+ for (int i = 0; i < pendingJobs.size(); i++) {
+ final JobStatus pending = pendingJobs.get(i);
+
+ // If job is already running, go to next job.
+ int jobRunningContext = findJobContextIdFromMap(pending, contextIdToJobMap);
+ if (jobRunningContext != -1) {
+ continue;
+ }
+
+ final int priority = mService.evaluateJobPriorityLocked(pending);
+ pending.lastEvaluatedPriority = priority;
+
+ mJobCountTracker.incrementPendingJobCount(isFgJob(pending));
+ }
+
+ mJobCountTracker.onCountDone();
+
+ for (int i = 0; i < pendingJobs.size(); i++) {
+ final JobStatus nextPending = pendingJobs.get(i);
+
+ // Unfortunately we need to repeat this relatively expensive check.
+ int jobRunningContext = findJobContextIdFromMap(nextPending, contextIdToJobMap);
+ if (jobRunningContext != -1) {
+ continue;
+ }
+
+ final boolean isPendingFg = isFgJob(nextPending);
+
+ // Find an available slot for nextPending. The context should be available OR
+ // it should have lowest priority among all running jobs
+ // (sharing the same Uid as nextPending)
+ int minPriorityForPreemption = Integer.MAX_VALUE;
+ int selectedContextId = -1;
+ boolean startingJob = false;
+ for (int j=0; j<MAX_JOB_CONTEXTS_COUNT; j++) {
+ JobStatus job = contextIdToJobMap[j];
+ int preferredUid = preferredUidForContext[j];
+ if (job == null) {
+ final boolean preferredUidOkay = (preferredUid == nextPending.getUid())
+ || (preferredUid == JobServiceContext.NO_PREFERRED_UID);
+
+ if (preferredUidOkay && mJobCountTracker.canJobStart(isPendingFg)) {
+ // This slot is free, and we haven't yet hit the limit on
+ // concurrent jobs... we can just throw the job in to here.
+ selectedContextId = j;
+ startingJob = true;
+ break;
+ }
+ // No job on this context, but nextPending can't run here because
+ // the context has a preferred Uid or we have reached the limit on
+ // concurrent jobs.
+ continue;
+ }
+ if (job.getUid() != nextPending.getUid()) {
+ continue;
+ }
+
+ final int jobPriority = mService.evaluateJobPriorityLocked(job);
+ if (jobPriority >= nextPending.lastEvaluatedPriority) {
+ continue;
+ }
+
+ // TODO lastEvaluatedPriority should be evaluateJobPriorityLocked. (double check it)
+ if (minPriorityForPreemption > nextPending.lastEvaluatedPriority) {
+ minPriorityForPreemption = nextPending.lastEvaluatedPriority;
+ selectedContextId = j;
+ // In this case, we're just going to preempt a low priority job, we're not
+ // actually starting a job, so don't set startingJob.
+ }
+ }
+ if (selectedContextId != -1) {
+ contextIdToJobMap[selectedContextId] = nextPending;
+ slotChanged[selectedContextId] = true;
+ }
+ if (startingJob) {
+ // Increase the counters when we're going to start a job.
+ mJobCountTracker.onStartingNewJob(isPendingFg);
+ }
+ }
+ if (DEBUG) {
+ Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs final"));
+ }
+
+ mJobCountTracker.logStatus();
+
+ tracker.noteConcurrency(mJobCountTracker.getTotalRunningJobCountToNote(),
+ mJobCountTracker.getFgRunningJobCountToNote());
+
+ for (int i=0; i<MAX_JOB_CONTEXTS_COUNT; i++) {
+ boolean preservePreferredUid = false;
+ if (slotChanged[i]) {
+ JobStatus js = activeServices.get(i).getRunningJobLocked();
+ if (js != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "preempting job: "
+ + activeServices.get(i).getRunningJobLocked());
+ }
+ // preferredUid will be set to uid of currently running job.
+ activeServices.get(i).preemptExecutingJobLocked();
+ preservePreferredUid = true;
+ } else {
+ final JobStatus pendingJob = contextIdToJobMap[i];
+ if (DEBUG) {
+ Slog.d(TAG, "About to run job on context "
+ + i + ", job: " + pendingJob);
+ }
+ for (int ic=0; ic<controllers.size(); ic++) {
+ controllers.get(ic).prepareForExecutionLocked(pendingJob);
+ }
+ if (!activeServices.get(i).executeRunnableJob(pendingJob)) {
+ Slog.d(TAG, "Error executing " + pendingJob);
+ }
+ if (pendingJobs.remove(pendingJob)) {
+ tracker.noteNonpending(pendingJob);
+ }
+ }
+ }
+ if (!preservePreferredUid) {
+ activeServices.get(i).clearPreferredUid();
+ }
+ }
+ }
+
+ private static int findJobContextIdFromMap(JobStatus jobStatus, JobStatus[] map) {
+ for (int i=0; i<map.length; i++) {
+ if (map[i] != null && map[i].matches(jobStatus.getUid(), jobStatus.getJobId())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @GuardedBy("mLock")
+ private String printPendingQueueLocked() {
+ StringBuilder s = new StringBuilder("Pending queue: ");
+ Iterator<JobStatus> it = mService.mPendingJobs.iterator();
+ while (it.hasNext()) {
+ JobStatus js = it.next();
+ s.append("(")
+ .append(js.getJob().getId())
+ .append(", ")
+ .append(js.getUid())
+ .append(") ");
+ }
+ return s.toString();
+ }
+
+ private static String printContextIdToJobMap(JobStatus[] map, String initial) {
+ StringBuilder s = new StringBuilder(initial + ": ");
+ for (int i=0; i<map.length; i++) {
+ s.append("(")
+ .append(map[i] == null? -1: map[i].getJobId())
+ .append(map[i] == null? -1: map[i].getUid())
+ .append(")" );
+ }
+ return s.toString();
+ }
+
+
+ public void dumpLocked(IndentingPrintWriter pw, long now, long nowRealtime) {
+ pw.println("Concurrency:");
+
+ pw.increaseIndent();
+ try {
+ pw.print("Screen state: current ");
+ pw.print(mCurrentInteractiveState ? "ON" : "OFF");
+ pw.print(" effective ");
+ pw.print(mEffectiveInteractiveState ? "ON" : "OFF");
+ pw.println();
+
+ pw.print("Last screen ON : ");
+ TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOnRealtime, now);
+ pw.println();
+
+ pw.print("Last screen OFF: ");
+ TimeUtils.dumpTimeWithDelta(pw, now - nowRealtime + mLastScreenOffRealtime, now);
+ pw.println();
+
+ pw.println();
+
+ pw.println("Current max jobs:");
+ pw.println(" ");
+ pw.println(mJobCountTracker);
+
+ pw.println();
+
+ pw.print("mLastMemoryTrimLevel: ");
+ pw.print(mLastMemoryTrimLevel);
+ pw.println();
+
+ mStatLogger.dump(pw);
+ } finally {
+ pw.decreaseIndent();
+ }
+ }
+
+ public void dumpProtoLocked(ProtoOutputStream proto, long tag, long now, long nowRealtime) {
+ final long token = proto.start(tag);
+
+ proto.write(JobConcurrencyManagerProto.CURRENT_INTERACTIVE,
+ mCurrentInteractiveState);
+ proto.write(JobConcurrencyManagerProto.EFFECTIVE_INTERACTIVE,
+ mEffectiveInteractiveState);
+
+ proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_ON_MS,
+ nowRealtime - mLastScreenOnRealtime);
+ proto.write(JobConcurrencyManagerProto.TIME_SINCE_LAST_SCREEN_OFF_MS,
+ nowRealtime - mLastScreenOffRealtime);
+
+ mJobCountTracker.dumpProto(proto, JobConcurrencyManagerProto.JOB_COUNT_TRACKER);
+
+ proto.write(JobConcurrencyManagerProto.MEMORY_TRIM_LEVEL,
+ mLastMemoryTrimLevel);
+
+ proto.end(token);
+ }
+
+ /**
+ * This class decides, taking into account {@link #mMaxJobCounts} and how mny jos are running /
+ * pending, how many more job can start.
+ *
+ * Extracted for testing and logging.
+ */
+ @VisibleForTesting
+ static class JobCountTracker {
+ private int mConfigNumMaxTotalJobs;
+ private int mConfigNumMaxBgJobs;
+ private int mConfigNumMinBgJobs;
+
+ private int mNumRunningFgJobs;
+ private int mNumRunningBgJobs;
+
+ private int mNumPendingFgJobs;
+ private int mNumPendingBgJobs;
+
+ private int mNumStartingFgJobs;
+ private int mNumStartingBgJobs;
+
+ private int mNumReservedForBg;
+ private int mNumActualMaxFgJobs;
+ private int mNumActualMaxBgJobs;
+
+ void reset(int numTotalMaxJobs, int numMaxBgJobs, int numMinBgJobs) {
+ mConfigNumMaxTotalJobs = numTotalMaxJobs;
+ mConfigNumMaxBgJobs = numMaxBgJobs;
+ mConfigNumMinBgJobs = numMinBgJobs;
+
+ mNumRunningFgJobs = 0;
+ mNumRunningBgJobs = 0;
+
+ mNumPendingFgJobs = 0;
+ mNumPendingBgJobs = 0;
+
+ mNumStartingFgJobs = 0;
+ mNumStartingBgJobs = 0;
+
+ mNumReservedForBg = 0;
+ mNumActualMaxFgJobs = 0;
+ mNumActualMaxBgJobs = 0;
+ }
+
+ void incrementRunningJobCount(boolean isFg) {
+ if (isFg) {
+ mNumRunningFgJobs++;
+ } else {
+ mNumRunningBgJobs++;
+ }
+ }
+
+ void incrementPendingJobCount(boolean isFg) {
+ if (isFg) {
+ mNumPendingFgJobs++;
+ } else {
+ mNumPendingBgJobs++;
+ }
+ }
+
+ void onStartingNewJob(boolean isFg) {
+ if (isFg) {
+ mNumStartingFgJobs++;
+ } else {
+ mNumStartingBgJobs++;
+ }
+ }
+
+ void onCountDone() {
+ // Note some variables are used only here but are made class members in order to have
+ // them on logcat / dumpsys.
+
+ // How many slots should we allocate to BG jobs at least?
+ // That's basically "getMinBg()", but if there are less jobs, decrease it.
+ // (e.g. even if min-bg is 2, if there's only 1 running+pending job, this has to be 1.)
+ final int reservedForBg = Math.min(
+ mConfigNumMinBgJobs,
+ mNumRunningBgJobs + mNumPendingBgJobs);
+
+ // However, if there are FG jobs already running, we have to adjust it.
+ mNumReservedForBg = Math.min(reservedForBg,
+ mConfigNumMaxTotalJobs - mNumRunningFgJobs);
+
+ // Max FG is [total - [number needed for BG jobs]]
+ // [number needed for BG jobs] is the bigger one of [running BG] or [reserved BG]
+ final int maxFg =
+ mConfigNumMaxTotalJobs - Math.max(mNumRunningBgJobs, mNumReservedForBg);
+
+ // The above maxFg is the theoretical max. If there are less FG jobs, the actual
+ // max FG will be lower accordingly.
+ mNumActualMaxFgJobs = Math.min(
+ maxFg,
+ mNumRunningFgJobs + mNumPendingFgJobs);
+
+ // Max BG is [total - actual max FG], but cap at [config max BG].
+ final int maxBg = Math.min(
+ mConfigNumMaxBgJobs,
+ mConfigNumMaxTotalJobs - mNumActualMaxFgJobs);
+
+ // If there are less BG jobs than maxBg, then reduce the actual max BG accordingly.
+ // This isn't needed for the logic to work, but this will give consistent output
+ // on logcat and dumpsys.
+ mNumActualMaxBgJobs = Math.min(
+ maxBg,
+ mNumRunningBgJobs + mNumPendingBgJobs);
+ }
+
+ boolean canJobStart(boolean isFg) {
+ if (isFg) {
+ return mNumRunningFgJobs + mNumStartingFgJobs < mNumActualMaxFgJobs;
+ } else {
+ return mNumRunningBgJobs + mNumStartingBgJobs < mNumActualMaxBgJobs;
+ }
+ }
+
+ public int getNumStartingFgJobs() {
+ return mNumStartingFgJobs;
+ }
+
+ public int getNumStartingBgJobs() {
+ return mNumStartingBgJobs;
+ }
+
+ int getTotalRunningJobCountToNote() {
+ return mNumRunningFgJobs + mNumRunningBgJobs
+ + mNumStartingFgJobs + mNumStartingBgJobs;
+ }
+
+ int getFgRunningJobCountToNote() {
+ return mNumRunningFgJobs + mNumStartingFgJobs;
+ }
+
+ void logStatus() {
+ if (DEBUG) {
+ Slog.d(TAG, "assignJobsToContexts: " + this);
+ }
+ }
+
+ public String toString() {
+ final int totalFg = mNumRunningFgJobs + mNumStartingFgJobs;
+ final int totalBg = mNumRunningBgJobs + mNumStartingBgJobs;
+ return String.format(
+ "Config={tot=%d bg min/max=%d/%d}"
+ + " Running[FG/BG (total)]: %d / %d (%d)"
+ + " Pending: %d / %d (%d)"
+ + " Actual max: %d%s / %d%s (%d%s)"
+ + " Res BG: %d"
+ + " Starting: %d / %d (%d)"
+ + " Total: %d%s / %d%s (%d%s)",
+ mConfigNumMaxTotalJobs,
+ mConfigNumMinBgJobs,
+ mConfigNumMaxBgJobs,
+
+ mNumRunningFgJobs, mNumRunningBgJobs,
+ mNumRunningFgJobs + mNumRunningBgJobs,
+
+ mNumPendingFgJobs, mNumPendingBgJobs,
+ mNumPendingFgJobs + mNumPendingBgJobs,
+
+ mNumActualMaxFgJobs, (totalFg <= mConfigNumMaxTotalJobs) ? "" : "*",
+ mNumActualMaxBgJobs, (totalBg <= mConfigNumMaxBgJobs) ? "" : "*",
+
+ mNumActualMaxFgJobs + mNumActualMaxBgJobs,
+ (mNumActualMaxFgJobs + mNumActualMaxBgJobs <= mConfigNumMaxTotalJobs)
+ ? "" : "*",
+
+ mNumReservedForBg,
+
+ mNumStartingFgJobs, mNumStartingBgJobs, mNumStartingFgJobs + mNumStartingBgJobs,
+
+ totalFg, (totalFg <= mNumActualMaxFgJobs) ? "" : "*",
+ totalBg, (totalBg <= mNumActualMaxBgJobs) ? "" : "*",
+ totalFg + totalBg, (totalFg + totalBg <= mConfigNumMaxTotalJobs) ? "" : "*"
+ );
+ }
+
+ public void dumpProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_TOTAL_JOBS, mConfigNumMaxTotalJobs);
+ proto.write(JobCountTrackerProto.CONFIG_NUM_MAX_BG_JOBS, mConfigNumMaxBgJobs);
+ proto.write(JobCountTrackerProto.CONFIG_NUM_MIN_BG_JOBS, mConfigNumMinBgJobs);
+
+ proto.write(JobCountTrackerProto.NUM_RUNNING_FG_JOBS, mNumRunningFgJobs);
+ proto.write(JobCountTrackerProto.NUM_RUNNING_BG_JOBS, mNumRunningBgJobs);
+
+ proto.write(JobCountTrackerProto.NUM_PENDING_FG_JOBS, mNumPendingFgJobs);
+ proto.write(JobCountTrackerProto.NUM_PENDING_BG_JOBS, mNumPendingBgJobs);
+
+ proto.end(token);
+ }
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java b/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java
new file mode 100644
index 0000000..e28e5bd
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobPackageTracker.java
@@ -0,0 +1,653 @@
+/*
+ * Copyright (C) 2016 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;
+
+import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import static com.android.server.job.JobSchedulerService.sSystemClock;
+import static com.android.server.job.JobSchedulerService.sUptimeMillisClock;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.os.UserHandle;
+import android.text.format.DateFormat;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.RingBufferIndices;
+import com.android.server.job.controllers.JobStatus;
+
+import java.io.PrintWriter;
+
+public final class JobPackageTracker {
+ // We batch every 30 minutes.
+ static final long BATCHING_TIME = 30*60*1000;
+ // Number of historical data sets we keep.
+ static final int NUM_HISTORY = 5;
+
+ private static final int EVENT_BUFFER_SIZE = 100;
+
+ public static final int EVENT_CMD_MASK = 0xff;
+ public static final int EVENT_STOP_REASON_SHIFT = 8;
+ public static final int EVENT_STOP_REASON_MASK = 0xff << EVENT_STOP_REASON_SHIFT;
+ public static final int EVENT_NULL = 0;
+ public static final int EVENT_START_JOB = 1;
+ public static final int EVENT_STOP_JOB = 2;
+ public static final int EVENT_START_PERIODIC_JOB = 3;
+ public static final int EVENT_STOP_PERIODIC_JOB = 4;
+
+ private final RingBufferIndices mEventIndices = new RingBufferIndices(EVENT_BUFFER_SIZE);
+ private final int[] mEventCmds = new int[EVENT_BUFFER_SIZE];
+ private final long[] mEventTimes = new long[EVENT_BUFFER_SIZE];
+ private final int[] mEventUids = new int[EVENT_BUFFER_SIZE];
+ private final String[] mEventTags = new String[EVENT_BUFFER_SIZE];
+ private final int[] mEventJobIds = new int[EVENT_BUFFER_SIZE];
+ private final String[] mEventReasons = new String[EVENT_BUFFER_SIZE];
+
+ public void addEvent(int cmd, int uid, String tag, int jobId, int stopReason,
+ String debugReason) {
+ int index = mEventIndices.add();
+ mEventCmds[index] = cmd | ((stopReason<<EVENT_STOP_REASON_SHIFT) & EVENT_STOP_REASON_MASK);
+ mEventTimes[index] = sElapsedRealtimeClock.millis();
+ mEventUids[index] = uid;
+ mEventTags[index] = tag;
+ mEventJobIds[index] = jobId;
+ mEventReasons[index] = debugReason;
+ }
+
+ DataSet mCurDataSet = new DataSet();
+ DataSet[] mLastDataSets = new DataSet[NUM_HISTORY];
+
+ final static class PackageEntry {
+ long pastActiveTime;
+ long activeStartTime;
+ int activeNesting;
+ int activeCount;
+ boolean hadActive;
+ long pastActiveTopTime;
+ long activeTopStartTime;
+ int activeTopNesting;
+ int activeTopCount;
+ boolean hadActiveTop;
+ long pastPendingTime;
+ long pendingStartTime;
+ int pendingNesting;
+ int pendingCount;
+ boolean hadPending;
+ final SparseIntArray stopReasons = new SparseIntArray();
+
+ public long getActiveTime(long now) {
+ long time = pastActiveTime;
+ if (activeNesting > 0) {
+ time += now - activeStartTime;
+ }
+ return time;
+ }
+
+ public long getActiveTopTime(long now) {
+ long time = pastActiveTopTime;
+ if (activeTopNesting > 0) {
+ time += now - activeTopStartTime;
+ }
+ return time;
+ }
+
+ public long getPendingTime(long now) {
+ long time = pastPendingTime;
+ if (pendingNesting > 0) {
+ time += now - pendingStartTime;
+ }
+ return time;
+ }
+ }
+
+ final static class DataSet {
+ final SparseArray<ArrayMap<String, PackageEntry>> mEntries = new SparseArray<>();
+ final long mStartUptimeTime;
+ final long mStartElapsedTime;
+ final long mStartClockTime;
+ long mSummedTime;
+ int mMaxTotalActive;
+ int mMaxFgActive;
+
+ public DataSet(DataSet otherTimes) {
+ mStartUptimeTime = otherTimes.mStartUptimeTime;
+ mStartElapsedTime = otherTimes.mStartElapsedTime;
+ mStartClockTime = otherTimes.mStartClockTime;
+ }
+
+ public DataSet() {
+ mStartUptimeTime = sUptimeMillisClock.millis();
+ mStartElapsedTime = sElapsedRealtimeClock.millis();
+ mStartClockTime = sSystemClock.millis();
+ }
+
+ private PackageEntry getOrCreateEntry(int uid, String pkg) {
+ ArrayMap<String, PackageEntry> uidMap = mEntries.get(uid);
+ if (uidMap == null) {
+ uidMap = new ArrayMap<>();
+ mEntries.put(uid, uidMap);
+ }
+ PackageEntry entry = uidMap.get(pkg);
+ if (entry == null) {
+ entry = new PackageEntry();
+ uidMap.put(pkg, entry);
+ }
+ return entry;
+ }
+
+ public PackageEntry getEntry(int uid, String pkg) {
+ ArrayMap<String, PackageEntry> uidMap = mEntries.get(uid);
+ if (uidMap == null) {
+ return null;
+ }
+ return uidMap.get(pkg);
+ }
+
+ long getTotalTime(long now) {
+ if (mSummedTime > 0) {
+ return mSummedTime;
+ }
+ return now - mStartUptimeTime;
+ }
+
+ void incPending(int uid, String pkg, long now) {
+ PackageEntry pe = getOrCreateEntry(uid, pkg);
+ if (pe.pendingNesting == 0) {
+ pe.pendingStartTime = now;
+ pe.pendingCount++;
+ }
+ pe.pendingNesting++;
+ }
+
+ void decPending(int uid, String pkg, long now) {
+ PackageEntry pe = getOrCreateEntry(uid, pkg);
+ if (pe.pendingNesting == 1) {
+ pe.pastPendingTime += now - pe.pendingStartTime;
+ }
+ pe.pendingNesting--;
+ }
+
+ void incActive(int uid, String pkg, long now) {
+ PackageEntry pe = getOrCreateEntry(uid, pkg);
+ if (pe.activeNesting == 0) {
+ pe.activeStartTime = now;
+ pe.activeCount++;
+ }
+ pe.activeNesting++;
+ }
+
+ void decActive(int uid, String pkg, long now, int stopReason) {
+ PackageEntry pe = getOrCreateEntry(uid, pkg);
+ if (pe.activeNesting == 1) {
+ pe.pastActiveTime += now - pe.activeStartTime;
+ }
+ pe.activeNesting--;
+ int count = pe.stopReasons.get(stopReason, 0);
+ pe.stopReasons.put(stopReason, count+1);
+ }
+
+ void incActiveTop(int uid, String pkg, long now) {
+ PackageEntry pe = getOrCreateEntry(uid, pkg);
+ if (pe.activeTopNesting == 0) {
+ pe.activeTopStartTime = now;
+ pe.activeTopCount++;
+ }
+ pe.activeTopNesting++;
+ }
+
+ void decActiveTop(int uid, String pkg, long now, int stopReason) {
+ PackageEntry pe = getOrCreateEntry(uid, pkg);
+ if (pe.activeTopNesting == 1) {
+ pe.pastActiveTopTime += now - pe.activeTopStartTime;
+ }
+ pe.activeTopNesting--;
+ int count = pe.stopReasons.get(stopReason, 0);
+ pe.stopReasons.put(stopReason, count+1);
+ }
+
+ void finish(DataSet next, long now) {
+ for (int i = mEntries.size() - 1; i >= 0; i--) {
+ ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i);
+ for (int j = uidMap.size() - 1; j >= 0; j--) {
+ PackageEntry pe = uidMap.valueAt(j);
+ if (pe.activeNesting > 0 || pe.activeTopNesting > 0 || pe.pendingNesting > 0) {
+ // Propagate existing activity in to next data set.
+ PackageEntry nextPe = next.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j));
+ nextPe.activeStartTime = now;
+ nextPe.activeNesting = pe.activeNesting;
+ nextPe.activeTopStartTime = now;
+ nextPe.activeTopNesting = pe.activeTopNesting;
+ nextPe.pendingStartTime = now;
+ nextPe.pendingNesting = pe.pendingNesting;
+ // Finish it off.
+ if (pe.activeNesting > 0) {
+ pe.pastActiveTime += now - pe.activeStartTime;
+ pe.activeNesting = 0;
+ }
+ if (pe.activeTopNesting > 0) {
+ pe.pastActiveTopTime += now - pe.activeTopStartTime;
+ pe.activeTopNesting = 0;
+ }
+ if (pe.pendingNesting > 0) {
+ pe.pastPendingTime += now - pe.pendingStartTime;
+ pe.pendingNesting = 0;
+ }
+ }
+ }
+ }
+ }
+
+ void addTo(DataSet out, long now) {
+ out.mSummedTime += getTotalTime(now);
+ for (int i = mEntries.size() - 1; i >= 0; i--) {
+ ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i);
+ for (int j = uidMap.size() - 1; j >= 0; j--) {
+ PackageEntry pe = uidMap.valueAt(j);
+ PackageEntry outPe = out.getOrCreateEntry(mEntries.keyAt(i), uidMap.keyAt(j));
+ outPe.pastActiveTime += pe.pastActiveTime;
+ outPe.activeCount += pe.activeCount;
+ outPe.pastActiveTopTime += pe.pastActiveTopTime;
+ outPe.activeTopCount += pe.activeTopCount;
+ outPe.pastPendingTime += pe.pastPendingTime;
+ outPe.pendingCount += pe.pendingCount;
+ if (pe.activeNesting > 0) {
+ outPe.pastActiveTime += now - pe.activeStartTime;
+ outPe.hadActive = true;
+ }
+ if (pe.activeTopNesting > 0) {
+ outPe.pastActiveTopTime += now - pe.activeTopStartTime;
+ outPe.hadActiveTop = true;
+ }
+ if (pe.pendingNesting > 0) {
+ outPe.pastPendingTime += now - pe.pendingStartTime;
+ outPe.hadPending = true;
+ }
+ for (int k = pe.stopReasons.size()-1; k >= 0; k--) {
+ int type = pe.stopReasons.keyAt(k);
+ outPe.stopReasons.put(type, outPe.stopReasons.get(type, 0)
+ + pe.stopReasons.valueAt(k));
+ }
+ }
+ }
+ if (mMaxTotalActive > out.mMaxTotalActive) {
+ out.mMaxTotalActive = mMaxTotalActive;
+ }
+ if (mMaxFgActive > out.mMaxFgActive) {
+ out.mMaxFgActive = mMaxFgActive;
+ }
+ }
+
+ void printDuration(PrintWriter pw, long period, long duration, int count, String suffix) {
+ float fraction = duration / (float) period;
+ int percent = (int) ((fraction * 100) + .5f);
+ if (percent > 0) {
+ pw.print(" ");
+ pw.print(percent);
+ pw.print("% ");
+ pw.print(count);
+ pw.print("x ");
+ pw.print(suffix);
+ } else if (count > 0) {
+ pw.print(" ");
+ pw.print(count);
+ pw.print("x ");
+ pw.print(suffix);
+ }
+ }
+
+ void dump(PrintWriter pw, String header, String prefix, long now, long nowElapsed,
+ int filterUid) {
+ final long period = getTotalTime(now);
+ pw.print(prefix); pw.print(header); pw.print(" at ");
+ pw.print(DateFormat.format("yyyy-MM-dd-HH-mm-ss", mStartClockTime).toString());
+ pw.print(" (");
+ TimeUtils.formatDuration(mStartElapsedTime, nowElapsed, pw);
+ pw.print(") over ");
+ TimeUtils.formatDuration(period, pw);
+ pw.println(":");
+ final int NE = mEntries.size();
+ for (int i = 0; i < NE; i++) {
+ int uid = mEntries.keyAt(i);
+ if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
+ continue;
+ }
+ ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i);
+ final int NP = uidMap.size();
+ for (int j = 0; j < NP; j++) {
+ PackageEntry pe = uidMap.valueAt(j);
+ pw.print(prefix); pw.print(" ");
+ UserHandle.formatUid(pw, uid);
+ pw.print(" / "); pw.print(uidMap.keyAt(j));
+ pw.println(":");
+ pw.print(prefix); pw.print(" ");
+ printDuration(pw, period, pe.getPendingTime(now), pe.pendingCount, "pending");
+ printDuration(pw, period, pe.getActiveTime(now), pe.activeCount, "active");
+ printDuration(pw, period, pe.getActiveTopTime(now), pe.activeTopCount,
+ "active-top");
+ if (pe.pendingNesting > 0 || pe.hadPending) {
+ pw.print(" (pending)");
+ }
+ if (pe.activeNesting > 0 || pe.hadActive) {
+ pw.print(" (active)");
+ }
+ if (pe.activeTopNesting > 0 || pe.hadActiveTop) {
+ pw.print(" (active-top)");
+ }
+ pw.println();
+ if (pe.stopReasons.size() > 0) {
+ pw.print(prefix); pw.print(" ");
+ for (int k = 0; k < pe.stopReasons.size(); k++) {
+ if (k > 0) {
+ pw.print(", ");
+ }
+ pw.print(pe.stopReasons.valueAt(k));
+ pw.print("x ");
+ pw.print(JobParameters.getReasonName(pe.stopReasons.keyAt(k)));
+ }
+ pw.println();
+ }
+ }
+ }
+ pw.print(prefix); pw.print(" Max concurrency: ");
+ pw.print(mMaxTotalActive); pw.print(" total, ");
+ pw.print(mMaxFgActive); pw.println(" foreground");
+ }
+
+ private void printPackageEntryState(ProtoOutputStream proto, long fieldId,
+ long duration, int count) {
+ final long token = proto.start(fieldId);
+ proto.write(DataSetProto.PackageEntryProto.State.DURATION_MS, duration);
+ proto.write(DataSetProto.PackageEntryProto.State.COUNT, count);
+ proto.end(token);
+ }
+
+ void dump(ProtoOutputStream proto, long fieldId, long now, long nowElapsed, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long period = getTotalTime(now);
+
+ proto.write(DataSetProto.START_CLOCK_TIME_MS, mStartClockTime);
+ proto.write(DataSetProto.ELAPSED_TIME_MS, nowElapsed - mStartElapsedTime);
+ proto.write(DataSetProto.PERIOD_MS, period);
+
+ final int NE = mEntries.size();
+ for (int i = 0; i < NE; i++) {
+ int uid = mEntries.keyAt(i);
+ if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
+ continue;
+ }
+ ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i);
+ final int NP = uidMap.size();
+ for (int j = 0; j < NP; j++) {
+ final long peToken = proto.start(DataSetProto.PACKAGE_ENTRIES);
+ PackageEntry pe = uidMap.valueAt(j);
+
+ proto.write(DataSetProto.PackageEntryProto.UID, uid);
+ proto.write(DataSetProto.PackageEntryProto.PACKAGE_NAME, uidMap.keyAt(j));
+
+ printPackageEntryState(proto, DataSetProto.PackageEntryProto.PENDING_STATE,
+ pe.getPendingTime(now), pe.pendingCount);
+ printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_STATE,
+ pe.getActiveTime(now), pe.activeCount);
+ printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_TOP_STATE,
+ pe.getActiveTopTime(now), pe.activeTopCount);
+
+ proto.write(DataSetProto.PackageEntryProto.PENDING,
+ pe.pendingNesting > 0 || pe.hadPending);
+ proto.write(DataSetProto.PackageEntryProto.ACTIVE,
+ pe.activeNesting > 0 || pe.hadActive);
+ proto.write(DataSetProto.PackageEntryProto.ACTIVE_TOP,
+ pe.activeTopNesting > 0 || pe.hadActiveTop);
+
+ for (int k = 0; k < pe.stopReasons.size(); k++) {
+ final long srcToken =
+ proto.start(DataSetProto.PackageEntryProto.STOP_REASONS);
+
+ proto.write(DataSetProto.PackageEntryProto.StopReasonCount.REASON,
+ pe.stopReasons.keyAt(k));
+ proto.write(DataSetProto.PackageEntryProto.StopReasonCount.COUNT,
+ pe.stopReasons.valueAt(k));
+
+ proto.end(srcToken);
+ }
+
+ proto.end(peToken);
+ }
+ }
+
+ proto.write(DataSetProto.MAX_CONCURRENCY, mMaxTotalActive);
+ proto.write(DataSetProto.MAX_FOREGROUND_CONCURRENCY, mMaxFgActive);
+
+ proto.end(token);
+ }
+ }
+
+ void rebatchIfNeeded(long now) {
+ long totalTime = mCurDataSet.getTotalTime(now);
+ if (totalTime > BATCHING_TIME) {
+ DataSet last = mCurDataSet;
+ last.mSummedTime = totalTime;
+ mCurDataSet = new DataSet();
+ last.finish(mCurDataSet, now);
+ System.arraycopy(mLastDataSets, 0, mLastDataSets, 1, mLastDataSets.length-1);
+ mLastDataSets[0] = last;
+ }
+ }
+
+ public void notePending(JobStatus job) {
+ final long now = sUptimeMillisClock.millis();
+ job.madePending = now;
+ rebatchIfNeeded(now);
+ mCurDataSet.incPending(job.getSourceUid(), job.getSourcePackageName(), now);
+ }
+
+ public void noteNonpending(JobStatus job) {
+ final long now = sUptimeMillisClock.millis();
+ mCurDataSet.decPending(job.getSourceUid(), job.getSourcePackageName(), now);
+ rebatchIfNeeded(now);
+ }
+
+ public void noteActive(JobStatus job) {
+ final long now = sUptimeMillisClock.millis();
+ job.madeActive = now;
+ rebatchIfNeeded(now);
+ if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) {
+ mCurDataSet.incActiveTop(job.getSourceUid(), job.getSourcePackageName(), now);
+ } else {
+ mCurDataSet.incActive(job.getSourceUid(), job.getSourcePackageName(), now);
+ }
+ addEvent(job.getJob().isPeriodic() ? EVENT_START_PERIODIC_JOB : EVENT_START_JOB,
+ job.getSourceUid(), job.getBatteryName(), job.getJobId(), 0, null);
+ }
+
+ public void noteInactive(JobStatus job, int stopReason, String debugReason) {
+ final long now = sUptimeMillisClock.millis();
+ if (job.lastEvaluatedPriority >= JobInfo.PRIORITY_TOP_APP) {
+ mCurDataSet.decActiveTop(job.getSourceUid(), job.getSourcePackageName(), now,
+ stopReason);
+ } else {
+ mCurDataSet.decActive(job.getSourceUid(), job.getSourcePackageName(), now, stopReason);
+ }
+ rebatchIfNeeded(now);
+ addEvent(job.getJob().isPeriodic() ? EVENT_STOP_JOB : EVENT_STOP_PERIODIC_JOB,
+ job.getSourceUid(), job.getBatteryName(), job.getJobId(), stopReason, debugReason);
+ }
+
+ public void noteConcurrency(int totalActive, int fgActive) {
+ if (totalActive > mCurDataSet.mMaxTotalActive) {
+ mCurDataSet.mMaxTotalActive = totalActive;
+ }
+ if (fgActive > mCurDataSet.mMaxFgActive) {
+ mCurDataSet.mMaxFgActive = fgActive;
+ }
+ }
+
+ public float getLoadFactor(JobStatus job) {
+ final int uid = job.getSourceUid();
+ final String pkg = job.getSourcePackageName();
+ PackageEntry cur = mCurDataSet.getEntry(uid, pkg);
+ PackageEntry last = mLastDataSets[0] != null ? mLastDataSets[0].getEntry(uid, pkg) : null;
+ if (cur == null && last == null) {
+ return 0;
+ }
+ final long now = sUptimeMillisClock.millis();
+ long time = 0;
+ if (cur != null) {
+ time += cur.getActiveTime(now) + cur.getPendingTime(now);
+ }
+ long period = mCurDataSet.getTotalTime(now);
+ if (last != null) {
+ time += last.getActiveTime(now) + last.getPendingTime(now);
+ period += mLastDataSets[0].getTotalTime(now);
+ }
+ return time / (float)period;
+ }
+
+ public void dump(PrintWriter pw, String prefix, int filterUid) {
+ final long now = sUptimeMillisClock.millis();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final DataSet total;
+ if (mLastDataSets[0] != null) {
+ total = new DataSet(mLastDataSets[0]);
+ mLastDataSets[0].addTo(total, now);
+ } else {
+ total = new DataSet(mCurDataSet);
+ }
+ mCurDataSet.addTo(total, now);
+ for (int i = 1; i < mLastDataSets.length; i++) {
+ if (mLastDataSets[i] != null) {
+ mLastDataSets[i].dump(pw, "Historical stats", prefix, now, nowElapsed, filterUid);
+ pw.println();
+ }
+ }
+ total.dump(pw, "Current stats", prefix, now, nowElapsed, filterUid);
+ }
+
+ public void dump(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long now = sUptimeMillisClock.millis();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+
+ final DataSet total;
+ if (mLastDataSets[0] != null) {
+ total = new DataSet(mLastDataSets[0]);
+ mLastDataSets[0].addTo(total, now);
+ } else {
+ total = new DataSet(mCurDataSet);
+ }
+ mCurDataSet.addTo(total, now);
+
+ for (int i = 1; i < mLastDataSets.length; i++) {
+ if (mLastDataSets[i] != null) {
+ mLastDataSets[i].dump(proto, JobPackageTrackerDumpProto.HISTORICAL_STATS,
+ now, nowElapsed, filterUid);
+ }
+ }
+ total.dump(proto, JobPackageTrackerDumpProto.CURRENT_STATS,
+ now, nowElapsed, filterUid);
+
+ proto.end(token);
+ }
+
+ public boolean dumpHistory(PrintWriter pw, String prefix, int filterUid) {
+ final int size = mEventIndices.size();
+ if (size <= 0) {
+ return false;
+ }
+ pw.println(" Job history:");
+ final long now = sElapsedRealtimeClock.millis();
+ for (int i=0; i<size; i++) {
+ final int index = mEventIndices.indexOf(i);
+ final int uid = mEventUids[index];
+ if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
+ continue;
+ }
+ final int cmd = mEventCmds[index] & EVENT_CMD_MASK;
+ if (cmd == EVENT_NULL) {
+ continue;
+ }
+ final String label;
+ switch (cmd) {
+ case EVENT_START_JOB: label = " START"; break;
+ case EVENT_STOP_JOB: label = " STOP"; break;
+ case EVENT_START_PERIODIC_JOB: label = "START-P"; break;
+ case EVENT_STOP_PERIODIC_JOB: label = " STOP-P"; break;
+ default: label = " ??"; break;
+ }
+ pw.print(prefix);
+ TimeUtils.formatDuration(mEventTimes[index]-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN);
+ pw.print(" ");
+ pw.print(label);
+ pw.print(": #");
+ UserHandle.formatUid(pw, uid);
+ pw.print("/");
+ pw.print(mEventJobIds[index]);
+ pw.print(" ");
+ pw.print(mEventTags[index]);
+ if (cmd == EVENT_STOP_JOB || cmd == EVENT_STOP_PERIODIC_JOB) {
+ pw.print(" ");
+ final String reason = mEventReasons[index];
+ if (reason != null) {
+ pw.print(mEventReasons[index]);
+ } else {
+ pw.print(JobParameters.getReasonName((mEventCmds[index] & EVENT_STOP_REASON_MASK)
+ >> EVENT_STOP_REASON_SHIFT));
+ }
+ }
+ pw.println();
+ }
+ return true;
+ }
+
+ public void dumpHistory(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final int size = mEventIndices.size();
+ if (size == 0) {
+ return;
+ }
+ final long token = proto.start(fieldId);
+
+ final long now = sElapsedRealtimeClock.millis();
+ for (int i = 0; i < size; i++) {
+ final int index = mEventIndices.indexOf(i);
+ final int uid = mEventUids[index];
+ if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
+ continue;
+ }
+ final int cmd = mEventCmds[index] & EVENT_CMD_MASK;
+ if (cmd == EVENT_NULL) {
+ continue;
+ }
+ final long heToken = proto.start(JobPackageHistoryProto.HISTORY_EVENT);
+
+ proto.write(JobPackageHistoryProto.HistoryEvent.EVENT, cmd);
+ proto.write(JobPackageHistoryProto.HistoryEvent.TIME_SINCE_EVENT_MS, now - mEventTimes[index]);
+ proto.write(JobPackageHistoryProto.HistoryEvent.UID, uid);
+ proto.write(JobPackageHistoryProto.HistoryEvent.JOB_ID, mEventJobIds[index]);
+ proto.write(JobPackageHistoryProto.HistoryEvent.TAG, mEventTags[index]);
+ if (cmd == EVENT_STOP_JOB || cmd == EVENT_STOP_PERIODIC_JOB) {
+ proto.write(JobPackageHistoryProto.HistoryEvent.STOP_REASON,
+ (mEventCmds[index] & EVENT_STOP_REASON_MASK) >> EVENT_STOP_REASON_SHIFT);
+ }
+
+ proto.end(heToken);
+ }
+
+ proto.end(token);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
new file mode 100644
index 0000000..e44e902
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -0,0 +1,3657 @@
+/*
+ * Copyright (C) 2014 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;
+
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.AlarmManager;
+import android.app.AppGlobals;
+import android.app.IUidObserver;
+import android.app.job.IJobScheduler;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobProtoEnums;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.app.job.JobSnapshot;
+import android.app.job.JobWorkItem;
+import android.app.usage.UsageStatsManager;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+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.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ParceledListSlice;
+import android.content.pm.ServiceInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.BatteryStats;
+import android.os.BatteryStatsInternal;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IThermalService;
+import android.os.IThermalStatusListener;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceManager;
+import android.os.ShellCallback;
+import android.os.SystemClock;
+import android.os.Temperature;
+import android.os.UserHandle;
+import android.os.UserManagerInternal;
+import android.os.WorkSource;
+import android.provider.Settings;
+import android.text.format.DateUtils;
+import android.util.KeyValueListParser;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.util.StatsLog;
+import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IBatteryStats;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
+import com.android.server.AppStateTracker;
+import com.android.server.DeviceIdleController;
+import com.android.server.FgThread;
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerServiceDumpProto.ActiveJob;
+import com.android.server.job.JobSchedulerServiceDumpProto.PendingJob;
+import com.android.server.job.JobSchedulerServiceDumpProto.RegisteredJob;
+import com.android.server.job.controllers.BackgroundJobsController;
+import com.android.server.job.controllers.BatteryController;
+import com.android.server.job.controllers.ConnectivityController;
+import com.android.server.job.controllers.ContentObserverController;
+import com.android.server.job.controllers.DeviceIdleJobsController;
+import com.android.server.job.controllers.IdleController;
+import com.android.server.job.controllers.JobStatus;
+import com.android.server.job.controllers.QuotaController;
+import com.android.server.job.controllers.StateController;
+import com.android.server.job.controllers.StorageController;
+import com.android.server.job.controllers.TimeController;
+
+import libcore.util.EmptyArray;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.time.Clock;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Responsible for taking jobs representing work to be performed by a client app, and determining
+ * based on the criteria specified when that job should be run against the client application's
+ * endpoint.
+ * Implements logic for scheduling, and rescheduling jobs. The JobSchedulerService knows nothing
+ * about constraints, or the state of active jobs. It receives callbacks from the various
+ * controllers and completed jobs and operates accordingly.
+ *
+ * Note on locking: Any operations that manipulate {@link #mJobs} need to lock on that object.
+ * Any function with the suffix 'Locked' also needs to lock on {@link #mJobs}.
+ * @hide
+ */
+public class JobSchedulerService extends com.android.server.SystemService
+ implements StateChangedListener, JobCompletedListener {
+ public static final String TAG = "JobScheduler";
+ public static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ public static final boolean DEBUG_STANDBY = DEBUG || false;
+
+ /** The maximum number of concurrent jobs we run at one time. */
+ static final int MAX_JOB_CONTEXTS_COUNT = 16;
+ /** Enforce a per-app limit on scheduled jobs? */
+ private static final boolean ENFORCE_MAX_JOBS = true;
+ /** The maximum number of jobs that we allow an unprivileged app to schedule */
+ private static final int MAX_JOBS_PER_APP = 100;
+
+ @VisibleForTesting
+ public static Clock sSystemClock = Clock.systemUTC();
+ @VisibleForTesting
+ public static Clock sUptimeMillisClock = SystemClock.uptimeClock();
+ @VisibleForTesting
+ public static Clock sElapsedRealtimeClock = SystemClock.elapsedRealtimeClock();
+
+ /** Global local for all job scheduler state. */
+ final Object mLock = new Object();
+ /** Master list of jobs. */
+ final JobStore mJobs;
+ /** Tracking the standby bucket state of each app */
+ final StandbyTracker mStandbyTracker;
+ /** Tracking amount of time each package runs for. */
+ final JobPackageTracker mJobPackageTracker = new JobPackageTracker();
+ final JobConcurrencyManager mConcurrencyManager;
+
+ static final int MSG_JOB_EXPIRED = 0;
+ static final int MSG_CHECK_JOB = 1;
+ static final int MSG_STOP_JOB = 2;
+ static final int MSG_CHECK_JOB_GREEDY = 3;
+ static final int MSG_UID_STATE_CHANGED = 4;
+ static final int MSG_UID_GONE = 5;
+ static final int MSG_UID_ACTIVE = 6;
+ static final int MSG_UID_IDLE = 7;
+
+ /**
+ * Track Services that have currently active or pending jobs. The index is provided by
+ * {@link JobStatus#getServiceToken()}
+ */
+ final List<JobServiceContext> mActiveServices = new ArrayList<>();
+
+ /** List of controllers that will notify this service of updates to jobs. */
+ final List<StateController> mControllers;
+ /** Need direct access to this for testing. */
+ private final BatteryController mBatteryController;
+ /** Need direct access to this for testing. */
+ private final StorageController mStorageController;
+ /** Need directly for sending uid state changes */
+ private final DeviceIdleJobsController mDeviceIdleJobsController;
+ /** Needed to get remaining quota time. */
+ private final QuotaController mQuotaController;
+
+ /** Need directly for receiving thermal events */
+ private IThermalService mThermalService;
+ /** Thermal constraint. */
+ @GuardedBy("mLock")
+ private boolean mThermalConstraint = false;
+
+ /**
+ * Queue of pending jobs. The JobServiceContext class will receive jobs from this list
+ * when ready to execute them.
+ */
+ final ArrayList<JobStatus> mPendingJobs = new ArrayList<>();
+
+ int[] mStartedUsers = EmptyArray.INT;
+
+ final JobHandler mHandler;
+ final JobSchedulerStub mJobSchedulerStub;
+
+ PackageManagerInternal mLocalPM;
+ ActivityManagerInternal mActivityManagerInternal;
+ IBatteryStats mBatteryStats;
+ DeviceIdleController.LocalService mLocalDeviceIdleController;
+ AppStateTracker mAppStateTracker;
+ final UsageStatsManagerInternal mUsageStats;
+
+ /**
+ * Set to true once we are allowed to run third party apps.
+ */
+ boolean mReadyToRock;
+
+ /**
+ * What we last reported to DeviceIdleController about whether we are active.
+ */
+ boolean mReportedActive;
+
+ /**
+ * Are we currently in device-wide standby parole?
+ */
+ volatile boolean mInParole;
+
+ /**
+ * A mapping of which uids are currently in the foreground to their effective priority.
+ */
+ final SparseIntArray mUidPriorityOverride = new SparseIntArray();
+
+ /**
+ * Which uids are currently performing backups, so we shouldn't allow their jobs to run.
+ */
+ final SparseIntArray mBackingUpUids = new SparseIntArray();
+
+ /**
+ * Count standby heartbeats, and keep track of which beat each bucket's jobs will
+ * next become runnable. Index into this array is by normalized bucket:
+ * { ACTIVE, WORKING, FREQUENT, RARE, NEVER }. The ACTIVE and NEVER bucket
+ * milestones are not updated: ACTIVE apps get jobs whenever they ask for them,
+ * and NEVER apps don't get them at all.
+ */
+ final long[] mNextBucketHeartbeat = { 0, 0, 0, 0, Long.MAX_VALUE };
+ long mHeartbeat = 0;
+ long mLastHeartbeatTime = sElapsedRealtimeClock.millis();
+
+ /**
+ * Named indices into the STANDBY_BEATS array, for clarity in referring to
+ * specific buckets' bookkeeping.
+ */
+ public static final int ACTIVE_INDEX = 0;
+ public static final int WORKING_INDEX = 1;
+ public static final int FREQUENT_INDEX = 2;
+ public static final int RARE_INDEX = 3;
+ public static final int NEVER_INDEX = 4;
+
+ /**
+ * Bookkeeping about when jobs last run. We keep our own record in heartbeat time,
+ * rather than rely on Usage Stats' timestamps, because heartbeat time can be
+ * manipulated for testing purposes and we need job runnability to track that rather
+ * than real time.
+ *
+ * Outer SparseArray slices by user handle; inner map of package name to heartbeat
+ * is a HashMap<> rather than ArrayMap<> because we expect O(hundreds) of keys
+ * and it will be accessed in a known-hot code path.
+ */
+ final SparseArray<HashMap<String, Long>> mLastJobHeartbeats = new SparseArray<>();
+
+ static final String HEARTBEAT_TAG = "*job.heartbeat*";
+ final HeartbeatAlarmListener mHeartbeatAlarm = new HeartbeatAlarmListener();
+
+ // -- Pre-allocated temporaries only for use in assignJobsToContextsLocked --
+
+ private class ConstantsObserver extends ContentObserver {
+ private ContentResolver mResolver;
+
+ public ConstantsObserver(Handler handler) {
+ super(handler);
+ }
+
+ public void start(ContentResolver resolver) {
+ mResolver = resolver;
+ mResolver.registerContentObserver(Settings.Global.getUriFor(
+ Settings.Global.JOB_SCHEDULER_CONSTANTS), false, this);
+ updateConstants();
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ updateConstants();
+ }
+
+ private void updateConstants() {
+ synchronized (mLock) {
+ try {
+ mConstants.updateConstantsLocked(Settings.Global.getString(mResolver,
+ Settings.Global.JOB_SCHEDULER_CONSTANTS));
+ for (int controller = 0; controller < mControllers.size(); controller++) {
+ final StateController sc = mControllers.get(controller);
+ sc.onConstantsUpdatedLocked();
+ }
+ } catch (IllegalArgumentException e) {
+ // Failed to parse the settings string, log this and move on
+ // with defaults.
+ Slog.e(TAG, "Bad jobscheduler settings", e);
+ }
+ }
+
+ if (mConstants.USE_HEARTBEATS) {
+ // Reset the heartbeat alarm based on the new heartbeat duration
+ setNextHeartbeatAlarm();
+ }
+ }
+ }
+
+ /**
+ * Thermal event received from Thermal Service
+ */
+ private final class ThermalStatusListener extends IThermalStatusListener.Stub {
+ @Override public void onStatusChange(int status) {
+ // Throttle for Temperature.THROTTLING_SEVERE and above
+ synchronized (mLock) {
+ mThermalConstraint = status >= Temperature.THROTTLING_SEVERE;
+ }
+ onControllerStateChanged();
+ }
+ }
+
+ static class MaxJobCounts {
+ private final KeyValueListParser.IntValue mTotal;
+ private final KeyValueListParser.IntValue mMaxBg;
+ private final KeyValueListParser.IntValue mMinBg;
+
+ MaxJobCounts(int totalDefault, String totalKey,
+ int maxBgDefault, String maxBgKey, int minBgDefault, String minBgKey) {
+ mTotal = new KeyValueListParser.IntValue(totalKey, totalDefault);
+ mMaxBg = new KeyValueListParser.IntValue(maxBgKey, maxBgDefault);
+ mMinBg = new KeyValueListParser.IntValue(minBgKey, minBgDefault);
+ }
+
+ public void parse(KeyValueListParser parser) {
+ mTotal.parse(parser);
+ mMaxBg.parse(parser);
+ mMinBg.parse(parser);
+
+ if (mTotal.getValue() < 1) {
+ mTotal.setValue(1);
+ } else if (mTotal.getValue() > MAX_JOB_CONTEXTS_COUNT) {
+ mTotal.setValue(MAX_JOB_CONTEXTS_COUNT);
+ }
+
+ if (mMaxBg.getValue() < 1) {
+ mMaxBg.setValue(1);
+ } else if (mMaxBg.getValue() > mTotal.getValue()) {
+ mMaxBg.setValue(mTotal.getValue());
+ }
+ if (mMinBg.getValue() < 0) {
+ mMinBg.setValue(0);
+ } else {
+ if (mMinBg.getValue() > mMaxBg.getValue()) {
+ mMinBg.setValue(mMaxBg.getValue());
+ }
+ if (mMinBg.getValue() >= mTotal.getValue()) {
+ mMinBg.setValue(mTotal.getValue() - 1);
+ }
+ }
+ }
+
+ /** Total number of jobs to run simultaneously. */
+ public int getMaxTotal() {
+ return mTotal.getValue();
+ }
+
+ /** Max number of BG (== owned by non-TOP apps) jobs to run simultaneously. */
+ public int getMaxBg() {
+ return mMaxBg.getValue();
+ }
+
+ /**
+ * We try to run at least this many BG (== owned by non-TOP apps) jobs, when there are any
+ * pending, rather than always running the TOTAL number of FG jobs.
+ */
+ public int getMinBg() {
+ return mMinBg.getValue();
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ mTotal.dump(pw, prefix);
+ mMaxBg.dump(pw, prefix);
+ mMinBg.dump(pw, prefix);
+ }
+
+ public void dumpProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ mTotal.dumpProto(proto, MaxJobCountsProto.TOTAL_JOBS);
+ mMaxBg.dumpProto(proto, MaxJobCountsProto.MAX_BG);
+ mMinBg.dumpProto(proto, MaxJobCountsProto.MIN_BG);
+ proto.end(token);
+ }
+ }
+
+ /** {@link MaxJobCounts} for each memory trim level. */
+ static class MaxJobCountsPerMemoryTrimLevel {
+ public final MaxJobCounts normal;
+ public final MaxJobCounts moderate;
+ public final MaxJobCounts low;
+ public final MaxJobCounts critical;
+
+ MaxJobCountsPerMemoryTrimLevel(
+ MaxJobCounts normal,
+ MaxJobCounts moderate, MaxJobCounts low,
+ MaxJobCounts critical) {
+ this.normal = normal;
+ this.moderate = moderate;
+ this.low = low;
+ this.critical = critical;
+ }
+
+ public void dumpProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ normal.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.NORMAL);
+ moderate.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.MODERATE);
+ low.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.LOW);
+ critical.dumpProto(proto, MaxJobCountsPerMemoryTrimLevelProto.CRITICAL);
+ proto.end(token);
+ }
+ }
+
+ /**
+ * All times are in milliseconds. These constants are kept synchronized with the system
+ * global Settings. Any access to this class or its fields should be done while
+ * holding the JobSchedulerService.mLock lock.
+ */
+ public static class Constants {
+ // Key names stored in the settings value.
+ private static final String KEY_MIN_IDLE_COUNT = "min_idle_count";
+ private static final String KEY_MIN_CHARGING_COUNT = "min_charging_count";
+ private static final String KEY_MIN_BATTERY_NOT_LOW_COUNT = "min_battery_not_low_count";
+ private static final String KEY_MIN_STORAGE_NOT_LOW_COUNT = "min_storage_not_low_count";
+ private static final String KEY_MIN_CONNECTIVITY_COUNT = "min_connectivity_count";
+ private static final String KEY_MIN_CONTENT_COUNT = "min_content_count";
+ private static final String KEY_MIN_READY_JOBS_COUNT = "min_ready_jobs_count";
+ private static final String KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT =
+ "min_ready_non_active_jobs_count";
+ private static final String KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS =
+ "max_non_active_job_batch_delay_ms";
+ private static final String KEY_HEAVY_USE_FACTOR = "heavy_use_factor";
+ private static final String KEY_MODERATE_USE_FACTOR = "moderate_use_factor";
+
+ // The following values used to be used on P and below. Do not reuse them.
+ private static final String DEPRECATED_KEY_FG_JOB_COUNT = "fg_job_count";
+ private static final String DEPRECATED_KEY_BG_NORMAL_JOB_COUNT = "bg_normal_job_count";
+ private static final String DEPRECATED_KEY_BG_MODERATE_JOB_COUNT = "bg_moderate_job_count";
+ private static final String DEPRECATED_KEY_BG_LOW_JOB_COUNT = "bg_low_job_count";
+ private static final String DEPRECATED_KEY_BG_CRITICAL_JOB_COUNT = "bg_critical_job_count";
+
+ private static final String KEY_MAX_STANDARD_RESCHEDULE_COUNT
+ = "max_standard_reschedule_count";
+ private static final String KEY_MAX_WORK_RESCHEDULE_COUNT = "max_work_reschedule_count";
+ private static final String KEY_MIN_LINEAR_BACKOFF_TIME = "min_linear_backoff_time";
+ private static final String KEY_MIN_EXP_BACKOFF_TIME = "min_exp_backoff_time";
+ private static final String KEY_STANDBY_HEARTBEAT_TIME = "standby_heartbeat_time";
+ private static final String KEY_STANDBY_WORKING_BEATS = "standby_working_beats";
+ private static final String KEY_STANDBY_FREQUENT_BEATS = "standby_frequent_beats";
+ private static final String KEY_STANDBY_RARE_BEATS = "standby_rare_beats";
+ private static final String KEY_CONN_CONGESTION_DELAY_FRAC = "conn_congestion_delay_frac";
+ private static final String KEY_CONN_PREFETCH_RELAX_FRAC = "conn_prefetch_relax_frac";
+ private static final String KEY_USE_HEARTBEATS = "use_heartbeats";
+
+ private static final int DEFAULT_MIN_IDLE_COUNT = 1;
+ private static final int DEFAULT_MIN_CHARGING_COUNT = 1;
+ private static final int DEFAULT_MIN_BATTERY_NOT_LOW_COUNT = 1;
+ private static final int DEFAULT_MIN_STORAGE_NOT_LOW_COUNT = 1;
+ private static final int DEFAULT_MIN_CONNECTIVITY_COUNT = 1;
+ private static final int DEFAULT_MIN_CONTENT_COUNT = 1;
+ private static final int DEFAULT_MIN_READY_JOBS_COUNT = 1;
+ private static final int DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT = 5;
+ private static final long DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = 31 * MINUTE_IN_MILLIS;
+ private static final float DEFAULT_HEAVY_USE_FACTOR = .9f;
+ private static final float DEFAULT_MODERATE_USE_FACTOR = .5f;
+ private static final int DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT = Integer.MAX_VALUE;
+ private static final int DEFAULT_MAX_WORK_RESCHEDULE_COUNT = Integer.MAX_VALUE;
+ private static final long DEFAULT_MIN_LINEAR_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS;
+ private static final long DEFAULT_MIN_EXP_BACKOFF_TIME = JobInfo.MIN_BACKOFF_MILLIS;
+ private static final long DEFAULT_STANDBY_HEARTBEAT_TIME = 11 * 60 * 1000L;
+ private static final int DEFAULT_STANDBY_WORKING_BEATS = 11; // ~ 2 hours, with 11min beats
+ private static final int DEFAULT_STANDBY_FREQUENT_BEATS = 43; // ~ 8 hours
+ private static final int DEFAULT_STANDBY_RARE_BEATS = 130; // ~ 24 hours
+ private static final float DEFAULT_CONN_CONGESTION_DELAY_FRAC = 0.5f;
+ private static final float DEFAULT_CONN_PREFETCH_RELAX_FRAC = 0.5f;
+ private static final boolean DEFAULT_USE_HEARTBEATS = false;
+
+ /**
+ * Minimum # of idle jobs that must be ready in order to force the JMS to schedule things
+ * early.
+ */
+ int MIN_IDLE_COUNT = DEFAULT_MIN_IDLE_COUNT;
+ /**
+ * Minimum # of charging jobs that must be ready in order to force the JMS to schedule
+ * things early.
+ */
+ int MIN_CHARGING_COUNT = DEFAULT_MIN_CHARGING_COUNT;
+ /**
+ * Minimum # of "battery not low" jobs that must be ready in order to force the JMS to
+ * schedule things early.
+ */
+ int MIN_BATTERY_NOT_LOW_COUNT = DEFAULT_MIN_BATTERY_NOT_LOW_COUNT;
+ /**
+ * Minimum # of "storage not low" jobs that must be ready in order to force the JMS to
+ * schedule things early.
+ */
+ int MIN_STORAGE_NOT_LOW_COUNT = DEFAULT_MIN_STORAGE_NOT_LOW_COUNT;
+ /**
+ * Minimum # of connectivity jobs that must be ready in order to force the JMS to schedule
+ * things early. 1 == Run connectivity jobs as soon as ready.
+ */
+ int MIN_CONNECTIVITY_COUNT = DEFAULT_MIN_CONNECTIVITY_COUNT;
+ /**
+ * Minimum # of content trigger jobs that must be ready in order to force the JMS to
+ * schedule things early.
+ */
+ int MIN_CONTENT_COUNT = DEFAULT_MIN_CONTENT_COUNT;
+ /**
+ * Minimum # of jobs (with no particular constraints) for which the JMS will be happy
+ * running some work early. This (and thus the other min counts) is now set to 1, to
+ * prevent any batching at this level. Since we now do batching through doze, that is
+ * a much better mechanism.
+ */
+ int MIN_READY_JOBS_COUNT = DEFAULT_MIN_READY_JOBS_COUNT;
+
+ /**
+ * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early.
+ */
+ int MIN_READY_NON_ACTIVE_JOBS_COUNT = DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT;
+
+ /**
+ * Don't batch a non-ACTIVE job if it's been delayed due to force batching attempts for
+ * at least this amount of time.
+ */
+ long MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS;
+
+ /**
+ * This is the job execution factor that is considered to be heavy use of the system.
+ */
+ float HEAVY_USE_FACTOR = DEFAULT_HEAVY_USE_FACTOR;
+ /**
+ * This is the job execution factor that is considered to be moderate use of the system.
+ */
+ float MODERATE_USE_FACTOR = DEFAULT_MODERATE_USE_FACTOR;
+
+ // Max job counts for screen on / off, for each memory trim level.
+ final MaxJobCountsPerMemoryTrimLevel MAX_JOB_COUNTS_SCREEN_ON =
+ new MaxJobCountsPerMemoryTrimLevel(
+ new MaxJobCounts(
+ 8, "max_job_total_on_normal",
+ 6, "max_job_max_bg_on_normal",
+ 2, "max_job_min_bg_on_normal"),
+ new MaxJobCounts(
+ 8, "max_job_total_on_moderate",
+ 4, "max_job_max_bg_on_moderate",
+ 2, "max_job_min_bg_on_moderate"),
+ new MaxJobCounts(
+ 5, "max_job_total_on_low",
+ 1, "max_job_max_bg_on_low",
+ 1, "max_job_min_bg_on_low"),
+ new MaxJobCounts(
+ 5, "max_job_total_on_critical",
+ 1, "max_job_max_bg_on_critical",
+ 1, "max_job_min_bg_on_critical"));
+
+ final MaxJobCountsPerMemoryTrimLevel MAX_JOB_COUNTS_SCREEN_OFF =
+ new MaxJobCountsPerMemoryTrimLevel(
+ new MaxJobCounts(
+ 10, "max_job_total_off_normal",
+ 6, "max_job_max_bg_off_normal",
+ 2, "max_job_min_bg_off_normal"),
+ new MaxJobCounts(
+ 10, "max_job_total_off_moderate",
+ 4, "max_job_max_bg_off_moderate",
+ 2, "max_job_min_bg_off_moderate"),
+ new MaxJobCounts(
+ 5, "max_job_total_off_low",
+ 1, "max_job_max_bg_off_low",
+ 1, "max_job_min_bg_off_low"),
+ new MaxJobCounts(
+ 5, "max_job_total_off_critical",
+ 1, "max_job_max_bg_off_critical",
+ 1, "max_job_min_bg_off_critical"));
+
+
+ /** Wait for this long after screen off before increasing the job concurrency. */
+ final KeyValueListParser.IntValue SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS =
+ new KeyValueListParser.IntValue(
+ "screen_off_job_concurrency_increase_delay_ms", 30_000);
+
+ /**
+ * The maximum number of times we allow a job to have itself rescheduled before
+ * giving up on it, for standard jobs.
+ */
+ int MAX_STANDARD_RESCHEDULE_COUNT = DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT;
+ /**
+ * The maximum number of times we allow a job to have itself rescheduled before
+ * giving up on it, for jobs that are executing work.
+ */
+ int MAX_WORK_RESCHEDULE_COUNT = DEFAULT_MAX_WORK_RESCHEDULE_COUNT;
+ /**
+ * The minimum backoff time to allow for linear backoff.
+ */
+ long MIN_LINEAR_BACKOFF_TIME = DEFAULT_MIN_LINEAR_BACKOFF_TIME;
+ /**
+ * The minimum backoff time to allow for exponential backoff.
+ */
+ long MIN_EXP_BACKOFF_TIME = DEFAULT_MIN_EXP_BACKOFF_TIME;
+ /**
+ * How often we recalculate runnability based on apps' standby bucket assignment.
+ * This should be prime relative to common time interval lengths such as a quarter-
+ * hour or day, so that the heartbeat drifts relative to wall-clock milestones.
+ */
+ long STANDBY_HEARTBEAT_TIME = DEFAULT_STANDBY_HEARTBEAT_TIME;
+ /**
+ * Mapping: standby bucket -> number of heartbeats between each sweep of that
+ * bucket's jobs.
+ *
+ * Bucket assignments as recorded in the JobStatus objects are normalized to be
+ * indices into this array, rather than the raw constants used
+ * by AppIdleHistory.
+ */
+ final int[] STANDBY_BEATS = {
+ 0,
+ DEFAULT_STANDBY_WORKING_BEATS,
+ DEFAULT_STANDBY_FREQUENT_BEATS,
+ DEFAULT_STANDBY_RARE_BEATS
+ };
+ /**
+ * The fraction of a job's running window that must pass before we
+ * consider running it when the network is congested.
+ */
+ public float CONN_CONGESTION_DELAY_FRAC = DEFAULT_CONN_CONGESTION_DELAY_FRAC;
+ /**
+ * The fraction of a prefetch job's running window that must pass before
+ * we consider matching it against a metered network.
+ */
+ public float CONN_PREFETCH_RELAX_FRAC = DEFAULT_CONN_PREFETCH_RELAX_FRAC;
+ /**
+ * Whether to use heartbeats or rolling window for quota management. True will use
+ * heartbeats, false will use a rolling window.
+ */
+ public boolean USE_HEARTBEATS = DEFAULT_USE_HEARTBEATS;
+
+ private final KeyValueListParser mParser = new KeyValueListParser(',');
+
+ void updateConstantsLocked(String value) {
+ try {
+ mParser.setString(value);
+ } catch (Exception e) {
+ // Failed to parse the settings string, log this and move on
+ // with defaults.
+ Slog.e(TAG, "Bad jobscheduler settings", e);
+ }
+
+ MIN_IDLE_COUNT = mParser.getInt(KEY_MIN_IDLE_COUNT,
+ DEFAULT_MIN_IDLE_COUNT);
+ MIN_CHARGING_COUNT = mParser.getInt(KEY_MIN_CHARGING_COUNT,
+ DEFAULT_MIN_CHARGING_COUNT);
+ MIN_BATTERY_NOT_LOW_COUNT = mParser.getInt(KEY_MIN_BATTERY_NOT_LOW_COUNT,
+ DEFAULT_MIN_BATTERY_NOT_LOW_COUNT);
+ MIN_STORAGE_NOT_LOW_COUNT = mParser.getInt(KEY_MIN_STORAGE_NOT_LOW_COUNT,
+ DEFAULT_MIN_STORAGE_NOT_LOW_COUNT);
+ MIN_CONNECTIVITY_COUNT = mParser.getInt(KEY_MIN_CONNECTIVITY_COUNT,
+ DEFAULT_MIN_CONNECTIVITY_COUNT);
+ MIN_CONTENT_COUNT = mParser.getInt(KEY_MIN_CONTENT_COUNT,
+ DEFAULT_MIN_CONTENT_COUNT);
+ MIN_READY_JOBS_COUNT = mParser.getInt(KEY_MIN_READY_JOBS_COUNT,
+ DEFAULT_MIN_READY_JOBS_COUNT);
+ MIN_READY_NON_ACTIVE_JOBS_COUNT = mParser.getInt(
+ KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT,
+ DEFAULT_MIN_READY_NON_ACTIVE_JOBS_COUNT);
+ MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS = mParser.getLong(
+ KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS,
+ DEFAULT_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS);
+ HEAVY_USE_FACTOR = mParser.getFloat(KEY_HEAVY_USE_FACTOR,
+ DEFAULT_HEAVY_USE_FACTOR);
+ MODERATE_USE_FACTOR = mParser.getFloat(KEY_MODERATE_USE_FACTOR,
+ DEFAULT_MODERATE_USE_FACTOR);
+
+ MAX_JOB_COUNTS_SCREEN_ON.normal.parse(mParser);
+ MAX_JOB_COUNTS_SCREEN_ON.moderate.parse(mParser);
+ MAX_JOB_COUNTS_SCREEN_ON.low.parse(mParser);
+ MAX_JOB_COUNTS_SCREEN_ON.critical.parse(mParser);
+
+ MAX_JOB_COUNTS_SCREEN_OFF.normal.parse(mParser);
+ MAX_JOB_COUNTS_SCREEN_OFF.moderate.parse(mParser);
+ MAX_JOB_COUNTS_SCREEN_OFF.low.parse(mParser);
+ MAX_JOB_COUNTS_SCREEN_OFF.critical.parse(mParser);
+
+ SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.parse(mParser);
+
+ MAX_STANDARD_RESCHEDULE_COUNT = mParser.getInt(KEY_MAX_STANDARD_RESCHEDULE_COUNT,
+ DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT);
+ MAX_WORK_RESCHEDULE_COUNT = mParser.getInt(KEY_MAX_WORK_RESCHEDULE_COUNT,
+ DEFAULT_MAX_WORK_RESCHEDULE_COUNT);
+ MIN_LINEAR_BACKOFF_TIME = mParser.getDurationMillis(KEY_MIN_LINEAR_BACKOFF_TIME,
+ DEFAULT_MIN_LINEAR_BACKOFF_TIME);
+ MIN_EXP_BACKOFF_TIME = mParser.getDurationMillis(KEY_MIN_EXP_BACKOFF_TIME,
+ DEFAULT_MIN_EXP_BACKOFF_TIME);
+ STANDBY_HEARTBEAT_TIME = mParser.getDurationMillis(KEY_STANDBY_HEARTBEAT_TIME,
+ DEFAULT_STANDBY_HEARTBEAT_TIME);
+ STANDBY_BEATS[WORKING_INDEX] = mParser.getInt(KEY_STANDBY_WORKING_BEATS,
+ DEFAULT_STANDBY_WORKING_BEATS);
+ STANDBY_BEATS[FREQUENT_INDEX] = mParser.getInt(KEY_STANDBY_FREQUENT_BEATS,
+ DEFAULT_STANDBY_FREQUENT_BEATS);
+ STANDBY_BEATS[RARE_INDEX] = mParser.getInt(KEY_STANDBY_RARE_BEATS,
+ DEFAULT_STANDBY_RARE_BEATS);
+ CONN_CONGESTION_DELAY_FRAC = mParser.getFloat(KEY_CONN_CONGESTION_DELAY_FRAC,
+ DEFAULT_CONN_CONGESTION_DELAY_FRAC);
+ CONN_PREFETCH_RELAX_FRAC = mParser.getFloat(KEY_CONN_PREFETCH_RELAX_FRAC,
+ DEFAULT_CONN_PREFETCH_RELAX_FRAC);
+ USE_HEARTBEATS = mParser.getBoolean(KEY_USE_HEARTBEATS, DEFAULT_USE_HEARTBEATS);
+ }
+
+ void dump(IndentingPrintWriter pw) {
+ pw.println("Settings:");
+ pw.increaseIndent();
+ pw.printPair(KEY_MIN_IDLE_COUNT, MIN_IDLE_COUNT).println();
+ pw.printPair(KEY_MIN_CHARGING_COUNT, MIN_CHARGING_COUNT).println();
+ pw.printPair(KEY_MIN_BATTERY_NOT_LOW_COUNT, MIN_BATTERY_NOT_LOW_COUNT).println();
+ pw.printPair(KEY_MIN_STORAGE_NOT_LOW_COUNT, MIN_STORAGE_NOT_LOW_COUNT).println();
+ pw.printPair(KEY_MIN_CONNECTIVITY_COUNT, MIN_CONNECTIVITY_COUNT).println();
+ pw.printPair(KEY_MIN_CONTENT_COUNT, MIN_CONTENT_COUNT).println();
+ pw.printPair(KEY_MIN_READY_JOBS_COUNT, MIN_READY_JOBS_COUNT).println();
+ pw.printPair(KEY_MIN_READY_NON_ACTIVE_JOBS_COUNT,
+ MIN_READY_NON_ACTIVE_JOBS_COUNT).println();
+ pw.printPair(KEY_MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS,
+ MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS).println();
+ pw.printPair(KEY_HEAVY_USE_FACTOR, HEAVY_USE_FACTOR).println();
+ pw.printPair(KEY_MODERATE_USE_FACTOR, MODERATE_USE_FACTOR).println();
+
+ MAX_JOB_COUNTS_SCREEN_ON.normal.dump(pw, "");
+ MAX_JOB_COUNTS_SCREEN_ON.moderate.dump(pw, "");
+ MAX_JOB_COUNTS_SCREEN_ON.low.dump(pw, "");
+ MAX_JOB_COUNTS_SCREEN_ON.critical.dump(pw, "");
+
+ MAX_JOB_COUNTS_SCREEN_OFF.normal.dump(pw, "");
+ MAX_JOB_COUNTS_SCREEN_OFF.moderate.dump(pw, "");
+ MAX_JOB_COUNTS_SCREEN_OFF.low.dump(pw, "");
+ MAX_JOB_COUNTS_SCREEN_OFF.critical.dump(pw, "");
+
+ SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.dump(pw, "");
+
+ pw.printPair(KEY_MAX_STANDARD_RESCHEDULE_COUNT, MAX_STANDARD_RESCHEDULE_COUNT).println();
+ pw.printPair(KEY_MAX_WORK_RESCHEDULE_COUNT, MAX_WORK_RESCHEDULE_COUNT).println();
+ pw.printPair(KEY_MIN_LINEAR_BACKOFF_TIME, MIN_LINEAR_BACKOFF_TIME).println();
+ pw.printPair(KEY_MIN_EXP_BACKOFF_TIME, MIN_EXP_BACKOFF_TIME).println();
+ pw.printPair(KEY_STANDBY_HEARTBEAT_TIME, STANDBY_HEARTBEAT_TIME).println();
+ pw.print("standby_beats={");
+ pw.print(STANDBY_BEATS[0]);
+ for (int i = 1; i < STANDBY_BEATS.length; i++) {
+ pw.print(", ");
+ pw.print(STANDBY_BEATS[i]);
+ }
+ pw.println('}');
+ pw.printPair(KEY_CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC).println();
+ pw.printPair(KEY_CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC).println();
+ pw.printPair(KEY_USE_HEARTBEATS, USE_HEARTBEATS).println();
+
+ pw.decreaseIndent();
+ }
+
+ void dump(ProtoOutputStream proto) {
+ proto.write(ConstantsProto.MIN_IDLE_COUNT, MIN_IDLE_COUNT);
+ proto.write(ConstantsProto.MIN_CHARGING_COUNT, MIN_CHARGING_COUNT);
+ proto.write(ConstantsProto.MIN_BATTERY_NOT_LOW_COUNT, MIN_BATTERY_NOT_LOW_COUNT);
+ proto.write(ConstantsProto.MIN_STORAGE_NOT_LOW_COUNT, MIN_STORAGE_NOT_LOW_COUNT);
+ proto.write(ConstantsProto.MIN_CONNECTIVITY_COUNT, MIN_CONNECTIVITY_COUNT);
+ proto.write(ConstantsProto.MIN_CONTENT_COUNT, MIN_CONTENT_COUNT);
+ proto.write(ConstantsProto.MIN_READY_JOBS_COUNT, MIN_READY_JOBS_COUNT);
+ proto.write(ConstantsProto.MIN_READY_NON_ACTIVE_JOBS_COUNT,
+ MIN_READY_NON_ACTIVE_JOBS_COUNT);
+ proto.write(ConstantsProto.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS,
+ MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS);
+ proto.write(ConstantsProto.HEAVY_USE_FACTOR, HEAVY_USE_FACTOR);
+ proto.write(ConstantsProto.MODERATE_USE_FACTOR, MODERATE_USE_FACTOR);
+
+ MAX_JOB_COUNTS_SCREEN_ON.dumpProto(proto, ConstantsProto.MAX_JOB_COUNTS_SCREEN_ON);
+ MAX_JOB_COUNTS_SCREEN_OFF.dumpProto(proto, ConstantsProto.MAX_JOB_COUNTS_SCREEN_OFF);
+
+ SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS.dumpProto(proto,
+ ConstantsProto.SCREEN_OFF_JOB_CONCURRENCY_INCREASE_DELAY_MS);
+
+ proto.write(ConstantsProto.MAX_STANDARD_RESCHEDULE_COUNT, MAX_STANDARD_RESCHEDULE_COUNT);
+ proto.write(ConstantsProto.MAX_WORK_RESCHEDULE_COUNT, MAX_WORK_RESCHEDULE_COUNT);
+ proto.write(ConstantsProto.MIN_LINEAR_BACKOFF_TIME_MS, MIN_LINEAR_BACKOFF_TIME);
+ proto.write(ConstantsProto.MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME);
+ proto.write(ConstantsProto.STANDBY_HEARTBEAT_TIME_MS, STANDBY_HEARTBEAT_TIME);
+ for (int period : STANDBY_BEATS) {
+ proto.write(ConstantsProto.STANDBY_BEATS, period);
+ }
+ proto.write(ConstantsProto.CONN_CONGESTION_DELAY_FRAC, CONN_CONGESTION_DELAY_FRAC);
+ proto.write(ConstantsProto.CONN_PREFETCH_RELAX_FRAC, CONN_PREFETCH_RELAX_FRAC);
+ proto.write(ConstantsProto.USE_HEARTBEATS, USE_HEARTBEATS);
+ }
+ }
+
+ final Constants mConstants;
+ final ConstantsObserver mConstantsObserver;
+
+ static final Comparator<JobStatus> mEnqueueTimeComparator = (o1, o2) -> {
+ if (o1.enqueueTime < o2.enqueueTime) {
+ return -1;
+ }
+ return o1.enqueueTime > o2.enqueueTime ? 1 : 0;
+ };
+
+ static <T> void addOrderedItem(ArrayList<T> array, T newItem, Comparator<T> comparator) {
+ int where = Collections.binarySearch(array, newItem, comparator);
+ if (where < 0) {
+ where = ~where;
+ }
+ array.add(where, newItem);
+ }
+
+ /**
+ * Cleans up outstanding jobs when a package is removed. Even if it's being replaced later we
+ * still clean up. On reinstall the package will have a new uid.
+ */
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (DEBUG) {
+ Slog.d(TAG, "Receieved: " + action);
+ }
+ final String pkgName = getPackageName(intent);
+ final int pkgUid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+
+ if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+ // Purge the app's jobs if the whole package was just disabled. When this is
+ // the case the component name will be a bare package name.
+ if (pkgName != null && pkgUid != -1) {
+ final String[] changedComponents = intent.getStringArrayExtra(
+ Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST);
+ if (changedComponents != null) {
+ for (String component : changedComponents) {
+ if (component.equals(pkgName)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Package state change: " + pkgName);
+ }
+ try {
+ final int userId = UserHandle.getUserId(pkgUid);
+ IPackageManager pm = AppGlobals.getPackageManager();
+ final int state = pm.getApplicationEnabledSetting(pkgName, userId);
+ if (state == COMPONENT_ENABLED_STATE_DISABLED
+ || state == COMPONENT_ENABLED_STATE_DISABLED_USER) {
+ if (DEBUG) {
+ Slog.d(TAG, "Removing jobs for package " + pkgName
+ + " in user " + userId);
+ }
+ cancelJobsForPackageAndUid(pkgName, pkgUid,
+ "app disabled");
+ }
+ } catch (RemoteException|IllegalArgumentException e) {
+ /*
+ * IllegalArgumentException means that the package doesn't exist.
+ * This arises when PACKAGE_CHANGED broadcast delivery has lagged
+ * behind outright uninstall, so by the time we try to act it's gone.
+ * We don't need to act on this PACKAGE_CHANGED when this happens;
+ * we'll get a PACKAGE_REMOVED later and clean up then.
+ *
+ * RemoteException can't actually happen; the package manager is
+ * running in this same process.
+ */
+ }
+ break;
+ }
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "Something in " + pkgName
+ + " changed. Reevaluating controller states.");
+ }
+ synchronized (mLock) {
+ for (int c = mControllers.size() - 1; c >= 0; --c) {
+ mControllers.get(c).reevaluateStateLocked(pkgUid);
+ }
+ }
+ }
+ } else {
+ Slog.w(TAG, "PACKAGE_CHANGED for " + pkgName + " / uid " + pkgUid);
+ }
+ } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+ // If this is an outright uninstall rather than the first half of an
+ // app update sequence, cancel the jobs associated with the app.
+ if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ int uidRemoved = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ if (DEBUG) {
+ Slog.d(TAG, "Removing jobs for uid: " + uidRemoved);
+ }
+ cancelJobsForPackageAndUid(pkgName, uidRemoved, "app uninstalled");
+ synchronized (mLock) {
+ for (int c = 0; c < mControllers.size(); ++c) {
+ mControllers.get(c).onAppRemovedLocked(pkgName, pkgUid);
+ }
+ }
+ }
+ } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0);
+ if (DEBUG) {
+ Slog.d(TAG, "Removing jobs for user: " + userId);
+ }
+ cancelJobsForUser(userId);
+ synchronized (mLock) {
+ for (int c = 0; c < mControllers.size(); ++c) {
+ mControllers.get(c).onUserRemovedLocked(userId);
+ }
+ }
+ } else if (Intent.ACTION_QUERY_PACKAGE_RESTART.equals(action)) {
+ // Has this package scheduled any jobs, such that we will take action
+ // if it were to be force-stopped?
+ if (pkgUid != -1) {
+ List<JobStatus> jobsForUid;
+ synchronized (mLock) {
+ jobsForUid = mJobs.getJobsByUid(pkgUid);
+ }
+ for (int i = jobsForUid.size() - 1; i >= 0; i--) {
+ if (jobsForUid.get(i).getSourcePackageName().equals(pkgName)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Restart query: package " + pkgName + " at uid "
+ + pkgUid + " has jobs");
+ }
+ setResultCode(Activity.RESULT_OK);
+ break;
+ }
+ }
+ }
+ } else if (Intent.ACTION_PACKAGE_RESTARTED.equals(action)) {
+ // possible force-stop
+ if (pkgUid != -1) {
+ if (DEBUG) {
+ Slog.d(TAG, "Removing jobs for pkg " + pkgName + " at uid " + pkgUid);
+ }
+ cancelJobsForPackageAndUid(pkgName, pkgUid, "app force stopped");
+ }
+ }
+ }
+ };
+
+ private String getPackageName(Intent intent) {
+ Uri uri = intent.getData();
+ String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
+ return pkg;
+ }
+
+ final private IUidObserver mUidObserver = new IUidObserver.Stub() {
+ @Override public void onUidStateChanged(int uid, int procState, long procStateSeq) {
+ mHandler.obtainMessage(MSG_UID_STATE_CHANGED, uid, procState).sendToTarget();
+ }
+
+ @Override public void onUidGone(int uid, boolean disabled) {
+ mHandler.obtainMessage(MSG_UID_GONE, uid, disabled ? 1 : 0).sendToTarget();
+ }
+
+ @Override public void onUidActive(int uid) throws RemoteException {
+ mHandler.obtainMessage(MSG_UID_ACTIVE, uid, 0).sendToTarget();
+ }
+
+ @Override public void onUidIdle(int uid, boolean disabled) {
+ mHandler.obtainMessage(MSG_UID_IDLE, uid, disabled ? 1 : 0).sendToTarget();
+ }
+
+ @Override public void onUidCachedChanged(int uid, boolean cached) {
+ }
+ };
+
+ public Context getTestableContext() {
+ return getContext();
+ }
+
+ public Object getLock() {
+ return mLock;
+ }
+
+ public JobStore getJobStore() {
+ return mJobs;
+ }
+
+ public Constants getConstants() {
+ return mConstants;
+ }
+
+ public boolean isChainedAttributionEnabled() {
+ return WorkSource.isChainedBatteryAttributionEnabled(getContext());
+ }
+
+ @Override
+ public void onStartUser(int userHandle) {
+ synchronized (mLock) {
+ mStartedUsers = ArrayUtils.appendInt(mStartedUsers, userHandle);
+ }
+ // Let's kick any outstanding jobs for this user.
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+
+ @Override
+ public void onUnlockUser(int userHandle) {
+ // Let's kick any outstanding jobs for this user.
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+
+ @Override
+ public void onStopUser(int userHandle) {
+ synchronized (mLock) {
+ mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle);
+ }
+ }
+
+ /**
+ * Return whether an UID is active or idle.
+ */
+ private boolean isUidActive(int uid) {
+ return mAppStateTracker.isUidActiveSynced(uid);
+ }
+
+ private final Predicate<Integer> mIsUidActivePredicate = this::isUidActive;
+
+ public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
+ int userId, String tag) {
+ try {
+ if (ActivityManager.getService().isAppStartModeDisabled(uId,
+ job.getService().getPackageName())) {
+ Slog.w(TAG, "Not scheduling job " + uId + ":" + job.toString()
+ + " -- package not allowed to start");
+ return JobScheduler.RESULT_FAILURE;
+ }
+ } catch (RemoteException e) {
+ }
+
+ synchronized (mLock) {
+ final JobStatus toCancel = mJobs.getJobByUidAndJobId(uId, job.getId());
+
+ if (work != null && toCancel != null) {
+ // Fast path: we are adding work to an existing job, and the JobInfo is not
+ // changing. We can just directly enqueue this work in to the job.
+ if (toCancel.getJob().equals(job)) {
+
+ toCancel.enqueueWorkLocked(ActivityManager.getService(), work);
+
+ // If any of work item is enqueued when the source is in the foreground,
+ // exempt the entire job.
+ toCancel.maybeAddForegroundExemption(mIsUidActivePredicate);
+
+ return JobScheduler.RESULT_SUCCESS;
+ }
+ }
+
+ JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);
+
+ // Give exemption if the source is in the foreground just now.
+ // Note if it's a sync job, this method is called on the handler so it's not exactly
+ // the state when requestSync() was called, but that should be fine because of the
+ // 1 minute foreground grace period.
+ jobStatus.maybeAddForegroundExemption(mIsUidActivePredicate);
+
+ if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString());
+ // Jobs on behalf of others don't apply to the per-app job cap
+ if (ENFORCE_MAX_JOBS && packageName == null) {
+ if (mJobs.countJobsForUid(uId) > MAX_JOBS_PER_APP) {
+ Slog.w(TAG, "Too many jobs for uid " + uId);
+ throw new IllegalStateException("Apps may not schedule more than "
+ + MAX_JOBS_PER_APP + " distinct jobs");
+ }
+ }
+
+ // This may throw a SecurityException.
+ jobStatus.prepareLocked(ActivityManager.getService());
+
+ if (work != null) {
+ // If work has been supplied, enqueue it into the new job.
+ jobStatus.enqueueWorkLocked(ActivityManager.getService(), work);
+ }
+
+ if (toCancel != null) {
+ // Implicitly replaces the existing job record with the new instance
+ cancelJobImplLocked(toCancel, jobStatus, "job rescheduled by app");
+ } else {
+ startTrackingJobLocked(jobStatus, null);
+ }
+ StatsLog.write_non_chained(StatsLog.SCHEDULED_JOB_STATE_CHANGED,
+ uId, null, jobStatus.getBatteryName(),
+ StatsLog.SCHEDULED_JOB_STATE_CHANGED__STATE__SCHEDULED,
+ JobProtoEnums.STOP_REASON_CANCELLED, jobStatus.getStandbyBucket(),
+ jobStatus.getJobId());
+
+ // If the job is immediately ready to run, then we can just immediately
+ // put it in the pending list and try to schedule it. This is especially
+ // important for jobs with a 0 deadline constraint, since they will happen a fair
+ // amount, we want to handle them as quickly as possible, and semantically we want to
+ // make sure we have started holding the wake lock for the job before returning to
+ // the caller.
+ // If the job is not yet ready to run, there is nothing more to do -- we are
+ // now just waiting for one of its controllers to change state and schedule
+ // the job appropriately.
+ if (isReadyToBeExecutedLocked(jobStatus)) {
+ // This is a new job, we can just immediately put it on the pending
+ // list and try to run it.
+ mJobPackageTracker.notePending(jobStatus);
+ addOrderedItem(mPendingJobs, jobStatus, mEnqueueTimeComparator);
+ maybeRunPendingJobsLocked();
+ } else {
+ evaluateControllerStatesLocked(jobStatus);
+ }
+ }
+ return JobScheduler.RESULT_SUCCESS;
+ }
+
+ public List<JobInfo> getPendingJobs(int uid) {
+ synchronized (mLock) {
+ List<JobStatus> jobs = mJobs.getJobsByUid(uid);
+ ArrayList<JobInfo> outList = new ArrayList<JobInfo>(jobs.size());
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus job = jobs.get(i);
+ outList.add(job.getJob());
+ }
+ return outList;
+ }
+ }
+
+ public JobInfo getPendingJob(int uid, int jobId) {
+ synchronized (mLock) {
+ List<JobStatus> jobs = mJobs.getJobsByUid(uid);
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus job = jobs.get(i);
+ if (job.getJobId() == jobId) {
+ return job.getJob();
+ }
+ }
+ return null;
+ }
+ }
+
+ void cancelJobsForUser(int userHandle) {
+ synchronized (mLock) {
+ final List<JobStatus> jobsForUser = mJobs.getJobsByUser(userHandle);
+ for (int i=0; i<jobsForUser.size(); i++) {
+ JobStatus toRemove = jobsForUser.get(i);
+ cancelJobImplLocked(toRemove, null, "user removed");
+ }
+ }
+ }
+
+ private void cancelJobsForNonExistentUsers() {
+ UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
+ synchronized (mLock) {
+ mJobs.removeJobsOfNonUsers(umi.getUserIds());
+ }
+ }
+
+ void cancelJobsForPackageAndUid(String pkgName, int uid, String reason) {
+ if ("android".equals(pkgName)) {
+ Slog.wtfStack(TAG, "Can't cancel all jobs for system package");
+ return;
+ }
+ synchronized (mLock) {
+ final List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid);
+ for (int i = jobsForUid.size() - 1; i >= 0; i--) {
+ final JobStatus job = jobsForUid.get(i);
+ if (job.getSourcePackageName().equals(pkgName)) {
+ cancelJobImplLocked(job, null, reason);
+ }
+ }
+ }
+ }
+
+ /**
+ * Entry point from client to cancel all jobs originating from their uid.
+ * This will remove the job from the master list, and cancel the job if it was staged for
+ * execution or being executed.
+ * @param uid Uid to check against for removal of a job.
+ *
+ */
+ public boolean cancelJobsForUid(int uid, String reason) {
+ if (uid == Process.SYSTEM_UID) {
+ Slog.wtfStack(TAG, "Can't cancel all jobs for system uid");
+ return false;
+ }
+
+ boolean jobsCanceled = false;
+ synchronized (mLock) {
+ final List<JobStatus> jobsForUid = mJobs.getJobsByUid(uid);
+ for (int i=0; i<jobsForUid.size(); i++) {
+ JobStatus toRemove = jobsForUid.get(i);
+ cancelJobImplLocked(toRemove, null, reason);
+ jobsCanceled = true;
+ }
+ }
+ return jobsCanceled;
+ }
+
+ /**
+ * Entry point from client to cancel the job corresponding to the jobId provided.
+ * This will remove the job from the master list, and cancel the job if it was staged for
+ * execution or being executed.
+ * @param uid Uid of the calling client.
+ * @param jobId Id of the job, provided at schedule-time.
+ */
+ public boolean cancelJob(int uid, int jobId, int callingUid) {
+ JobStatus toCancel;
+ synchronized (mLock) {
+ toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
+ if (toCancel != null) {
+ cancelJobImplLocked(toCancel, null,
+ "cancel() called by app, callingUid=" + callingUid
+ + " uid=" + uid + " jobId=" + jobId);
+ }
+ return (toCancel != null);
+ }
+ }
+
+ /**
+ * Cancel the given job, stopping it if it's currently executing. If {@code incomingJob}
+ * is null, the cancelled job is removed outright from the system. If
+ * {@code incomingJob} is non-null, it replaces {@code cancelled} in the store of
+ * currently scheduled jobs.
+ */
+ private void cancelJobImplLocked(JobStatus cancelled, JobStatus incomingJob, String reason) {
+ if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
+ cancelled.unprepareLocked(ActivityManager.getService());
+ stopTrackingJobLocked(cancelled, incomingJob, true /* writeBack */);
+ // Remove from pending queue.
+ if (mPendingJobs.remove(cancelled)) {
+ mJobPackageTracker.noteNonpending(cancelled);
+ }
+ // Cancel if running.
+ stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED, reason);
+ // If this is a replacement, bring in the new version of the job
+ if (incomingJob != null) {
+ if (DEBUG) Slog.i(TAG, "Tracking replacement job " + incomingJob.toShortString());
+ startTrackingJobLocked(incomingJob, cancelled);
+ }
+ reportActiveLocked();
+ }
+
+ void updateUidState(int uid, int procState) {
+ synchronized (mLock) {
+ if (procState == ActivityManager.PROCESS_STATE_TOP) {
+ // Only use this if we are exactly the top app. All others can live
+ // with just the foreground priority. This means that persistent processes
+ // can never be the top app priority... that is fine.
+ mUidPriorityOverride.put(uid, JobInfo.PRIORITY_TOP_APP);
+ } else if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ mUidPriorityOverride.put(uid, JobInfo.PRIORITY_FOREGROUND_SERVICE);
+ } else if (procState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
+ mUidPriorityOverride.put(uid, JobInfo.PRIORITY_BOUND_FOREGROUND_SERVICE);
+ } else {
+ mUidPriorityOverride.delete(uid);
+ }
+ }
+ }
+
+ @Override
+ public void onDeviceIdleStateChanged(boolean deviceIdle) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slog.d(TAG, "Doze state changed: " + deviceIdle);
+ }
+ if (deviceIdle) {
+ // When becoming idle, make sure no jobs are actively running,
+ // except those using the idle exemption flag.
+ for (int i=0; i<mActiveServices.size(); i++) {
+ JobServiceContext jsc = mActiveServices.get(i);
+ final JobStatus executing = jsc.getRunningJobLocked();
+ if (executing != null
+ && (executing.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) == 0) {
+ jsc.cancelExecutingJobLocked(JobParameters.REASON_DEVICE_IDLE,
+ "cancelled due to doze");
+ }
+ }
+ } else {
+ // When coming out of idle, allow thing to start back up.
+ if (mReadyToRock) {
+ if (mLocalDeviceIdleController != null) {
+ if (!mReportedActive) {
+ mReportedActive = true;
+ mLocalDeviceIdleController.setJobsActive(true);
+ }
+ }
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+ }
+ }
+ }
+
+ void reportActiveLocked() {
+ // active is true if pending queue contains jobs OR some job is running.
+ boolean active = mPendingJobs.size() > 0;
+ if (mPendingJobs.size() <= 0) {
+ for (int i=0; i<mActiveServices.size(); i++) {
+ final JobServiceContext jsc = mActiveServices.get(i);
+ final JobStatus job = jsc.getRunningJobLocked();
+ if (job != null
+ && (job.getJob().getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) == 0
+ && !job.dozeWhitelisted
+ && !job.uidActive) {
+ // We will report active if we have a job running and it is not an exception
+ // due to being in the foreground or whitelisted.
+ active = true;
+ break;
+ }
+ }
+ }
+
+ if (mReportedActive != active) {
+ mReportedActive = active;
+ if (mLocalDeviceIdleController != null) {
+ mLocalDeviceIdleController.setJobsActive(active);
+ }
+ }
+ }
+
+ void reportAppUsage(String packageName, int userId) {
+ // This app just transitioned into interactive use or near equivalent, so we should
+ // take a look at its job state for feedback purposes.
+ }
+
+ /**
+ * Initializes the system service.
+ * <p>
+ * Subclasses must define a single argument constructor that accepts the context
+ * and passes it to super.
+ * </p>
+ *
+ * @param context The system server context.
+ */
+ public JobSchedulerService(Context context) {
+ super(context);
+
+ mLocalPM = LocalServices.getService(PackageManagerInternal.class);
+ mActivityManagerInternal = Preconditions.checkNotNull(
+ LocalServices.getService(ActivityManagerInternal.class));
+
+ mHandler = new JobHandler(context.getMainLooper());
+ mConstants = new Constants();
+ mConstantsObserver = new ConstantsObserver(mHandler);
+ mJobSchedulerStub = new JobSchedulerStub();
+
+ mConcurrencyManager = new JobConcurrencyManager(this);
+
+ // Set up the app standby bucketing tracker
+ mStandbyTracker = new StandbyTracker();
+ mUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
+ mUsageStats.addAppIdleStateChangeListener(mStandbyTracker);
+
+ // The job store needs to call back
+ publishLocalService(JobSchedulerInternal.class, new LocalService());
+
+ // Initialize the job store and set up any persisted jobs
+ mJobs = JobStore.initAndGet(this);
+
+ // Create the controllers.
+ mControllers = new ArrayList<StateController>();
+ mControllers.add(new ConnectivityController(this));
+ mControllers.add(new TimeController(this));
+ mControllers.add(new IdleController(this));
+ mBatteryController = new BatteryController(this);
+ mControllers.add(mBatteryController);
+ mStorageController = new StorageController(this);
+ mControllers.add(mStorageController);
+ mControllers.add(new BackgroundJobsController(this));
+ mControllers.add(new ContentObserverController(this));
+ mDeviceIdleJobsController = new DeviceIdleJobsController(this);
+ mControllers.add(mDeviceIdleJobsController);
+ mQuotaController = new QuotaController(this);
+ mControllers.add(mQuotaController);
+
+ // If the job store determined that it can't yet reschedule persisted jobs,
+ // we need to start watching the clock.
+ if (!mJobs.jobTimesInflatedValid()) {
+ Slog.w(TAG, "!!! RTC not yet good; tracking time updates for job scheduling");
+ context.registerReceiver(mTimeSetReceiver, new IntentFilter(Intent.ACTION_TIME_CHANGED));
+ }
+ }
+
+ private final BroadcastReceiver mTimeSetReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_TIME_CHANGED.equals(intent.getAction())) {
+ // When we reach clock sanity, recalculate the temporal windows
+ // of all affected jobs.
+ if (mJobs.clockNowValidToInflate(sSystemClock.millis())) {
+ Slog.i(TAG, "RTC now valid; recalculating persisted job windows");
+
+ // We've done our job now, so stop watching the time.
+ context.unregisterReceiver(this);
+
+ // And kick off the work to update the affected jobs, using a secondary
+ // thread instead of chugging away here on the main looper thread.
+ FgThread.getHandler().post(mJobTimeUpdater);
+ }
+ }
+ }
+ };
+
+ private final Runnable mJobTimeUpdater = () -> {
+ final ArrayList<JobStatus> toRemove = new ArrayList<>();
+ final ArrayList<JobStatus> toAdd = new ArrayList<>();
+ synchronized (mLock) {
+ // Note: we intentionally both look up the existing affected jobs and replace them
+ // with recalculated ones inside the same lock lifetime.
+ getJobStore().getRtcCorrectedJobsLocked(toAdd, toRemove);
+
+ // Now, at each position [i], we have both the existing JobStatus
+ // and the one that replaces it.
+ final int N = toAdd.size();
+ for (int i = 0; i < N; i++) {
+ final JobStatus oldJob = toRemove.get(i);
+ final JobStatus newJob = toAdd.get(i);
+ if (DEBUG) {
+ Slog.v(TAG, " replacing " + oldJob + " with " + newJob);
+ }
+ cancelJobImplLocked(oldJob, newJob, "deferred rtc calculation");
+ }
+ }
+ };
+
+ @Override
+ public void onStart() {
+ publishBinderService(Context.JOB_SCHEDULER_SERVICE, mJobSchedulerStub);
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (PHASE_SYSTEM_SERVICES_READY == phase) {
+ mConstantsObserver.start(getContext().getContentResolver());
+ for (StateController controller : mControllers) {
+ controller.onSystemServicesReady();
+ }
+
+ mAppStateTracker = Preconditions.checkNotNull(
+ LocalServices.getService(AppStateTracker.class));
+ if (mConstants.USE_HEARTBEATS) {
+ setNextHeartbeatAlarm();
+ }
+
+ // Register br for package removals and user removals.
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ filter.addAction(Intent.ACTION_QUERY_PACKAGE_RESTART);
+ filter.addDataScheme("package");
+ getContext().registerReceiverAsUser(
+ mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+ final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
+ getContext().registerReceiverAsUser(
+ mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
+ try {
+ ActivityManager.getService().registerUidObserver(mUidObserver,
+ ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE
+ | ActivityManager.UID_OBSERVER_IDLE | ActivityManager.UID_OBSERVER_ACTIVE,
+ ActivityManager.PROCESS_STATE_UNKNOWN, null);
+ } catch (RemoteException e) {
+ // ignored; both services live in system_server
+ }
+
+ mConcurrencyManager.onSystemReady();
+
+ // Remove any jobs that are not associated with any of the current users.
+ cancelJobsForNonExistentUsers();
+ // Register thermal callback
+ mThermalService = IThermalService.Stub.asInterface(
+ ServiceManager.getService(Context.THERMAL_SERVICE));
+ if (mThermalService != null) {
+ try {
+ mThermalService.registerThermalStatusListener(new ThermalStatusListener());
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to register thermal callback.", e);
+ }
+ }
+ } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
+ synchronized (mLock) {
+ // Let's go!
+ mReadyToRock = true;
+ mBatteryStats = IBatteryStats.Stub.asInterface(ServiceManager.getService(
+ BatteryStats.SERVICE_NAME));
+ mLocalDeviceIdleController
+ = LocalServices.getService(DeviceIdleController.LocalService.class);
+ // Create the "runners".
+ for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {
+ mActiveServices.add(
+ new JobServiceContext(this, mBatteryStats, mJobPackageTracker,
+ getContext().getMainLooper()));
+ }
+ // Attach jobs to their controllers.
+ mJobs.forEachJob((job) -> {
+ for (int controller = 0; controller < mControllers.size(); controller++) {
+ final StateController sc = mControllers.get(controller);
+ sc.maybeStartTrackingJobLocked(job, null);
+ }
+ });
+ // GO GO GO!
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+ }
+ }
+
+ /**
+ * Called when we have a job status object that we need to insert in our
+ * {@link com.android.server.job.JobStore}, and make sure all the relevant controllers know
+ * about.
+ */
+ private void startTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+ if (!jobStatus.isPreparedLocked()) {
+ Slog.wtf(TAG, "Not yet prepared when started tracking: " + jobStatus);
+ }
+ jobStatus.enqueueTime = sElapsedRealtimeClock.millis();
+ final boolean update = mJobs.add(jobStatus);
+ if (mReadyToRock) {
+ for (int i = 0; i < mControllers.size(); i++) {
+ StateController controller = mControllers.get(i);
+ if (update) {
+ controller.maybeStopTrackingJobLocked(jobStatus, null, true);
+ }
+ controller.maybeStartTrackingJobLocked(jobStatus, lastJob);
+ }
+ }
+ }
+
+ /**
+ * Called when we want to remove a JobStatus object that we've finished executing.
+ * @return true if the job was removed.
+ */
+ private boolean stopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+ boolean removeFromPersisted) {
+ // Deal with any remaining work items in the old job.
+ jobStatus.stopTrackingJobLocked(ActivityManager.getService(), incomingJob);
+
+ // Remove from store as well as controllers.
+ final boolean removed = mJobs.remove(jobStatus, removeFromPersisted);
+ if (removed && mReadyToRock) {
+ for (int i=0; i<mControllers.size(); i++) {
+ StateController controller = mControllers.get(i);
+ controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false);
+ }
+ }
+ return removed;
+ }
+
+ private boolean stopJobOnServiceContextLocked(JobStatus job, int reason, String debugReason) {
+ for (int i=0; i<mActiveServices.size(); i++) {
+ JobServiceContext jsc = mActiveServices.get(i);
+ final JobStatus executing = jsc.getRunningJobLocked();
+ if (executing != null && executing.matches(job.getUid(), job.getJobId())) {
+ jsc.cancelExecutingJobLocked(reason, debugReason);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param job JobStatus we are querying against.
+ * @return Whether or not the job represented by the status object is currently being run or
+ * is pending.
+ */
+ private boolean isCurrentlyActiveLocked(JobStatus job) {
+ for (int i=0; i<mActiveServices.size(); i++) {
+ JobServiceContext serviceContext = mActiveServices.get(i);
+ final JobStatus running = serviceContext.getRunningJobLocked();
+ if (running != null && running.matches(job.getUid(), job.getJobId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void noteJobsPending(List<JobStatus> jobs) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus job = jobs.get(i);
+ mJobPackageTracker.notePending(job);
+ }
+ }
+
+ void noteJobsNonpending(List<JobStatus> jobs) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus job = jobs.get(i);
+ mJobPackageTracker.noteNonpending(job);
+ }
+ }
+
+ /**
+ * Reschedules the given job based on the job's backoff policy. It doesn't make sense to
+ * specify an override deadline on a failed job (the failed job will run even though it's not
+ * ready), so we reschedule it with {@link JobStatus#NO_LATEST_RUNTIME}, but specify that any
+ * ready job with {@link JobStatus#getNumFailures()} > 0 will be executed.
+ *
+ * @param failureToReschedule Provided job status that we will reschedule.
+ * @return A newly instantiated JobStatus with the same constraints as the last job except
+ * with adjusted timing constraints.
+ *
+ * @see #maybeQueueReadyJobsForExecutionLocked
+ */
+ @VisibleForTesting
+ JobStatus getRescheduleJobForFailureLocked(JobStatus failureToReschedule) {
+ final long elapsedNowMillis = sElapsedRealtimeClock.millis();
+ final JobInfo job = failureToReschedule.getJob();
+
+ final long initialBackoffMillis = job.getInitialBackoffMillis();
+ final int backoffAttempts = failureToReschedule.getNumFailures() + 1;
+ long delayMillis;
+
+ if (failureToReschedule.hasWorkLocked()) {
+ if (backoffAttempts > mConstants.MAX_WORK_RESCHEDULE_COUNT) {
+ Slog.w(TAG, "Not rescheduling " + failureToReschedule + ": attempt #"
+ + backoffAttempts + " > work limit "
+ + mConstants.MAX_STANDARD_RESCHEDULE_COUNT);
+ return null;
+ }
+ } else if (backoffAttempts > mConstants.MAX_STANDARD_RESCHEDULE_COUNT) {
+ Slog.w(TAG, "Not rescheduling " + failureToReschedule + ": attempt #"
+ + backoffAttempts + " > std limit " + mConstants.MAX_STANDARD_RESCHEDULE_COUNT);
+ return null;
+ }
+
+ switch (job.getBackoffPolicy()) {
+ case JobInfo.BACKOFF_POLICY_LINEAR: {
+ long backoff = initialBackoffMillis;
+ if (backoff < mConstants.MIN_LINEAR_BACKOFF_TIME) {
+ backoff = mConstants.MIN_LINEAR_BACKOFF_TIME;
+ }
+ delayMillis = backoff * backoffAttempts;
+ } break;
+ default:
+ if (DEBUG) {
+ Slog.v(TAG, "Unrecognised back-off policy, defaulting to exponential.");
+ }
+ case JobInfo.BACKOFF_POLICY_EXPONENTIAL: {
+ long backoff = initialBackoffMillis;
+ if (backoff < mConstants.MIN_EXP_BACKOFF_TIME) {
+ backoff = mConstants.MIN_EXP_BACKOFF_TIME;
+ }
+ delayMillis = (long) Math.scalb(backoff, backoffAttempts - 1);
+ } break;
+ }
+ delayMillis =
+ Math.min(delayMillis, JobInfo.MAX_BACKOFF_DELAY_MILLIS);
+ JobStatus newJob = new JobStatus(failureToReschedule, getCurrentHeartbeat(),
+ elapsedNowMillis + delayMillis,
+ JobStatus.NO_LATEST_RUNTIME, backoffAttempts,
+ failureToReschedule.getLastSuccessfulRunTime(), sSystemClock.millis());
+ if (job.isPeriodic()) {
+ newJob.setOriginalLatestRunTimeElapsed(
+ failureToReschedule.getOriginalLatestRunTimeElapsed());
+ }
+ for (int ic=0; ic<mControllers.size(); ic++) {
+ StateController controller = mControllers.get(ic);
+ controller.rescheduleForFailureLocked(newJob, failureToReschedule);
+ }
+ return newJob;
+ }
+
+ /**
+ * Maximum time buffer in which JobScheduler will try to optimize periodic job scheduling. This
+ * does not cause a job's period to be larger than requested (eg: if the requested period is
+ * shorter than this buffer). This is used to put a limit on when JobScheduler will intervene
+ * and try to optimize scheduling if the current job finished less than this amount of time to
+ * the start of the next period
+ */
+ private static final long PERIODIC_JOB_WINDOW_BUFFER = 30 * MINUTE_IN_MILLIS;
+
+ /** The maximum period a periodic job can have. Anything higher will be clamped down to this. */
+ public static final long MAX_ALLOWED_PERIOD_MS = 365 * 24 * 60 * 60 * 1000L;
+
+ /**
+ * Called after a periodic has executed so we can reschedule it. We take the last execution
+ * time of the job to be the time of completion (i.e. the time at which this function is
+ * called).
+ * <p>This could be inaccurate b/c the job can run for as long as
+ * {@link com.android.server.job.JobServiceContext#EXECUTING_TIMESLICE_MILLIS}, but will lead
+ * to underscheduling at least, rather than if we had taken the last execution time to be the
+ * start of the execution.
+ * <p>Unlike a reschedule prior to execution, in this case we advance the next-heartbeat
+ * tracking as though the job were newly-scheduled.
+ * @return A new job representing the execution criteria for this instantiation of the
+ * recurring job.
+ */
+ @VisibleForTesting
+ JobStatus getRescheduleJobForPeriodic(JobStatus periodicToReschedule) {
+ final long elapsedNow = sElapsedRealtimeClock.millis();
+ final long newLatestRuntimeElapsed;
+ // Make sure period is in the interval [min_possible_period, max_possible_period].
+ final long period = Math.max(JobInfo.getMinPeriodMillis(),
+ Math.min(MAX_ALLOWED_PERIOD_MS, periodicToReschedule.getJob().getIntervalMillis()));
+ // Make sure flex is in the interval [min_possible_flex, period].
+ final long flex = Math.max(JobInfo.getMinFlexMillis(),
+ Math.min(period, periodicToReschedule.getJob().getFlexMillis()));
+ long rescheduleBuffer = 0;
+
+ long olrte = periodicToReschedule.getOriginalLatestRunTimeElapsed();
+ if (olrte < 0 || olrte == JobStatus.NO_LATEST_RUNTIME) {
+ Slog.wtf(TAG, "Invalid periodic job original latest run time: " + olrte);
+ olrte = elapsedNow;
+ }
+ final long latestRunTimeElapsed = olrte;
+
+ final long diffMs = Math.abs(elapsedNow - latestRunTimeElapsed);
+ if (elapsedNow > latestRunTimeElapsed) {
+ // The job ran past its expected run window. Have it count towards the current window
+ // and schedule a new job for the next window.
+ if (DEBUG) {
+ Slog.i(TAG, "Periodic job ran after its intended window.");
+ }
+ long numSkippedWindows = (diffMs / period) + 1; // +1 to include original window
+ if (period != flex && diffMs > Math.min(PERIODIC_JOB_WINDOW_BUFFER,
+ (period - flex) / 2)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Custom flex job ran too close to next window.");
+ }
+ // For custom flex periods, if the job was run too close to the next window,
+ // skip the next window and schedule for the following one.
+ numSkippedWindows += 1;
+ }
+ newLatestRuntimeElapsed = latestRunTimeElapsed + (period * numSkippedWindows);
+ } else {
+ newLatestRuntimeElapsed = latestRunTimeElapsed + period;
+ if (diffMs < PERIODIC_JOB_WINDOW_BUFFER && diffMs < period / 6) {
+ // Add a little buffer to the start of the next window so the job doesn't run
+ // too soon after this completed one.
+ rescheduleBuffer = Math.min(PERIODIC_JOB_WINDOW_BUFFER, period / 6 - diffMs);
+ }
+ }
+
+ if (newLatestRuntimeElapsed < elapsedNow) {
+ Slog.wtf(TAG, "Rescheduling calculated latest runtime in the past: "
+ + newLatestRuntimeElapsed);
+ return new JobStatus(periodicToReschedule, getCurrentHeartbeat(),
+ elapsedNow + period - flex, elapsedNow + period,
+ 0 /* backoffAttempt */,
+ sSystemClock.millis() /* lastSuccessfulRunTime */,
+ periodicToReschedule.getLastFailedRunTime());
+ }
+
+ final long newEarliestRunTimeElapsed = newLatestRuntimeElapsed
+ - Math.min(flex, period - rescheduleBuffer);
+
+ if (DEBUG) {
+ Slog.v(TAG, "Rescheduling executed periodic. New execution window [" +
+ newEarliestRunTimeElapsed / 1000 + ", " + newLatestRuntimeElapsed / 1000
+ + "]s");
+ }
+ return new JobStatus(periodicToReschedule, getCurrentHeartbeat(),
+ newEarliestRunTimeElapsed, newLatestRuntimeElapsed,
+ 0 /* backoffAttempt */,
+ sSystemClock.millis() /* lastSuccessfulRunTime */,
+ periodicToReschedule.getLastFailedRunTime());
+ }
+
+ /*
+ * We default to "long enough ago that every bucket's jobs are immediately runnable" to
+ * avoid starvation of apps in uncommon-use buckets that might arise from repeated
+ * reboot behavior.
+ */
+ long heartbeatWhenJobsLastRun(String packageName, final @UserIdInt int userId) {
+ // The furthest back in pre-boot time that we need to bother with
+ long heartbeat = -mConstants.STANDBY_BEATS[RARE_INDEX];
+ boolean cacheHit = false;
+ synchronized (mLock) {
+ HashMap<String, Long> jobPackages = mLastJobHeartbeats.get(userId);
+ if (jobPackages != null) {
+ long cachedValue = jobPackages.getOrDefault(packageName, Long.MAX_VALUE);
+ if (cachedValue < Long.MAX_VALUE) {
+ cacheHit = true;
+ heartbeat = cachedValue;
+ }
+ }
+ if (!cacheHit) {
+ // We haven't seen it yet; ask usage stats about it
+ final long timeSinceJob = mUsageStats.getTimeSinceLastJobRun(packageName, userId);
+ if (timeSinceJob < Long.MAX_VALUE) {
+ // Usage stats knows about it from before, so calculate back from that
+ // and go from there.
+ heartbeat = mHeartbeat - (timeSinceJob / mConstants.STANDBY_HEARTBEAT_TIME);
+ }
+ // If usage stats returned its "not found" MAX_VALUE, we still have the
+ // negative default 'heartbeat' value we established above
+ setLastJobHeartbeatLocked(packageName, userId, heartbeat);
+ }
+ }
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, "Last job heartbeat " + heartbeat + " for "
+ + packageName + "/" + userId);
+ }
+ return heartbeat;
+ }
+
+ long heartbeatWhenJobsLastRun(JobStatus job) {
+ return heartbeatWhenJobsLastRun(job.getSourcePackageName(), job.getSourceUserId());
+ }
+
+ void setLastJobHeartbeatLocked(String packageName, int userId, long heartbeat) {
+ HashMap<String, Long> jobPackages = mLastJobHeartbeats.get(userId);
+ if (jobPackages == null) {
+ jobPackages = new HashMap<>();
+ mLastJobHeartbeats.put(userId, jobPackages);
+ }
+ jobPackages.put(packageName, heartbeat);
+ }
+
+ // JobCompletedListener implementations.
+
+ /**
+ * A job just finished executing. We fetch the
+ * {@link com.android.server.job.controllers.JobStatus} from the store and depending on
+ * whether we want to reschedule we re-add it to the controllers.
+ * @param jobStatus Completed job.
+ * @param needsReschedule Whether the implementing class should reschedule this job.
+ */
+ @Override
+ public void onJobCompletedLocked(JobStatus jobStatus, boolean needsReschedule) {
+ if (DEBUG) {
+ Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule);
+ }
+
+ // If the job wants to be rescheduled, we first need to make the next upcoming
+ // job so we can transfer any appropriate state over from the previous job when
+ // we stop it.
+ final JobStatus rescheduledJob = needsReschedule
+ ? getRescheduleJobForFailureLocked(jobStatus) : null;
+
+ // Do not write back immediately if this is a periodic job. The job may get lost if system
+ // shuts down before it is added back.
+ if (!stopTrackingJobLocked(jobStatus, rescheduledJob, !jobStatus.getJob().isPeriodic())) {
+ if (DEBUG) {
+ Slog.d(TAG, "Could not find job to remove. Was job removed while executing?");
+ }
+ // We still want to check for jobs to execute, because this job may have
+ // scheduled a new job under the same job id, and now we can run it.
+ mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
+ return;
+ }
+
+ if (rescheduledJob != null) {
+ try {
+ rescheduledJob.prepareLocked(ActivityManager.getService());
+ } catch (SecurityException e) {
+ Slog.w(TAG, "Unable to regrant job permissions for " + rescheduledJob);
+ }
+ startTrackingJobLocked(rescheduledJob, jobStatus);
+ } else if (jobStatus.getJob().isPeriodic()) {
+ JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
+ try {
+ rescheduledPeriodic.prepareLocked(ActivityManager.getService());
+ } catch (SecurityException e) {
+ Slog.w(TAG, "Unable to regrant job permissions for " + rescheduledPeriodic);
+ }
+ startTrackingJobLocked(rescheduledPeriodic, jobStatus);
+ }
+ jobStatus.unprepareLocked(ActivityManager.getService());
+ reportActiveLocked();
+ mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
+ }
+
+ // StateChangedListener implementations.
+
+ /**
+ * Posts a message to the {@link com.android.server.job.JobSchedulerService.JobHandler} that
+ * some controller's state has changed, so as to run through the list of jobs and start/stop
+ * any that are eligible.
+ */
+ @Override
+ public void onControllerStateChanged() {
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+
+ @Override
+ public void onRunJobNow(JobStatus jobStatus) {
+ mHandler.obtainMessage(MSG_JOB_EXPIRED, jobStatus).sendToTarget();
+ }
+
+ final private class JobHandler extends Handler {
+
+ public JobHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ synchronized (mLock) {
+ if (!mReadyToRock) {
+ return;
+ }
+ switch (message.what) {
+ case MSG_JOB_EXPIRED: {
+ JobStatus runNow = (JobStatus) message.obj;
+ // runNow can be null, which is a controller's way of indicating that its
+ // state is such that all ready jobs should be run immediately.
+ if (runNow != null && isReadyToBeExecutedLocked(runNow)) {
+ mJobPackageTracker.notePending(runNow);
+ addOrderedItem(mPendingJobs, runNow, mEnqueueTimeComparator);
+ } else {
+ queueReadyJobsForExecutionLocked();
+ }
+ } break;
+ case MSG_CHECK_JOB:
+ if (DEBUG) {
+ Slog.d(TAG, "MSG_CHECK_JOB");
+ }
+ removeMessages(MSG_CHECK_JOB);
+ if (mReportedActive) {
+ // if jobs are currently being run, queue all ready jobs for execution.
+ queueReadyJobsForExecutionLocked();
+ } else {
+ // Check the list of jobs and run some of them if we feel inclined.
+ maybeQueueReadyJobsForExecutionLocked();
+ }
+ break;
+ case MSG_CHECK_JOB_GREEDY:
+ if (DEBUG) {
+ Slog.d(TAG, "MSG_CHECK_JOB_GREEDY");
+ }
+ queueReadyJobsForExecutionLocked();
+ break;
+ case MSG_STOP_JOB:
+ cancelJobImplLocked((JobStatus) message.obj, null,
+ "app no longer allowed to run");
+ break;
+
+ case MSG_UID_STATE_CHANGED: {
+ final int uid = message.arg1;
+ final int procState = message.arg2;
+ updateUidState(uid, procState);
+ break;
+ }
+ case MSG_UID_GONE: {
+ final int uid = message.arg1;
+ final boolean disabled = message.arg2 != 0;
+ updateUidState(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
+ if (disabled) {
+ cancelJobsForUid(uid, "uid gone");
+ }
+ synchronized (mLock) {
+ mDeviceIdleJobsController.setUidActiveLocked(uid, false);
+ }
+ break;
+ }
+ case MSG_UID_ACTIVE: {
+ final int uid = message.arg1;
+ synchronized (mLock) {
+ mDeviceIdleJobsController.setUidActiveLocked(uid, true);
+ }
+ break;
+ }
+ case MSG_UID_IDLE: {
+ final int uid = message.arg1;
+ final boolean disabled = message.arg2 != 0;
+ if (disabled) {
+ cancelJobsForUid(uid, "app uid idle");
+ }
+ synchronized (mLock) {
+ mDeviceIdleJobsController.setUidActiveLocked(uid, false);
+ }
+ break;
+ }
+
+ }
+ maybeRunPendingJobsLocked();
+ // Don't remove JOB_EXPIRED in case one came along while processing the queue.
+ }
+ }
+ }
+
+ private boolean isJobThermalConstrainedLocked(JobStatus job) {
+ return mThermalConstraint && job.hasConnectivityConstraint()
+ && (evaluateJobPriorityLocked(job) < JobInfo.PRIORITY_FOREGROUND_APP);
+ }
+
+ private void stopNonReadyActiveJobsLocked() {
+ for (int i=0; i<mActiveServices.size(); i++) {
+ JobServiceContext serviceContext = mActiveServices.get(i);
+ final JobStatus running = serviceContext.getRunningJobLocked();
+ if (running == null) {
+ continue;
+ }
+ if (!running.isReady()) {
+ serviceContext.cancelExecutingJobLocked(
+ JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED,
+ "cancelled due to unsatisfied constraints");
+ } else if (isJobThermalConstrainedLocked(running)) {
+ serviceContext.cancelExecutingJobLocked(
+ JobParameters.REASON_DEVICE_THERMAL,
+ "cancelled due to thermal condition");
+ }
+ }
+ }
+
+ /**
+ * Run through list of jobs and execute all possible - at least one is expired so we do
+ * as many as we can.
+ */
+ private void queueReadyJobsForExecutionLocked() {
+ if (DEBUG) {
+ Slog.d(TAG, "queuing all ready jobs for execution:");
+ }
+ noteJobsNonpending(mPendingJobs);
+ mPendingJobs.clear();
+ stopNonReadyActiveJobsLocked();
+ mJobs.forEachJob(mReadyQueueFunctor);
+ mReadyQueueFunctor.postProcess();
+
+ if (DEBUG) {
+ final int queuedJobs = mPendingJobs.size();
+ if (queuedJobs == 0) {
+ Slog.d(TAG, "No jobs pending.");
+ } else {
+ Slog.d(TAG, queuedJobs + " jobs queued.");
+ }
+ }
+ }
+
+ final class ReadyJobQueueFunctor implements Consumer<JobStatus> {
+ final ArrayList<JobStatus> newReadyJobs = new ArrayList<>();
+
+ @Override
+ public void accept(JobStatus job) {
+ if (isReadyToBeExecutedLocked(job)) {
+ if (DEBUG) {
+ Slog.d(TAG, " queued " + job.toShortString());
+ }
+ newReadyJobs.add(job);
+ } else {
+ evaluateControllerStatesLocked(job);
+ }
+ }
+
+ public void postProcess() {
+ noteJobsPending(newReadyJobs);
+ mPendingJobs.addAll(newReadyJobs);
+ if (mPendingJobs.size() > 1) {
+ mPendingJobs.sort(mEnqueueTimeComparator);
+ }
+
+ newReadyJobs.clear();
+ }
+ }
+ private final ReadyJobQueueFunctor mReadyQueueFunctor = new ReadyJobQueueFunctor();
+
+ /**
+ * The state of at least one job has changed. Here is where we could enforce various
+ * policies on when we want to execute jobs.
+ */
+ final class MaybeReadyJobQueueFunctor implements Consumer<JobStatus> {
+ int chargingCount;
+ int batteryNotLowCount;
+ int storageNotLowCount;
+ int idleCount;
+ int backoffCount;
+ int connectivityCount;
+ int contentCount;
+ int forceBatchedCount;
+ int unbatchedCount;
+ final List<JobStatus> runnableJobs = new ArrayList<>();
+
+ public MaybeReadyJobQueueFunctor() {
+ reset();
+ }
+
+ // Functor method invoked for each job via JobStore.forEachJob()
+ @Override
+ public void accept(JobStatus job) {
+ if (isReadyToBeExecutedLocked(job)) {
+ try {
+ if (ActivityManager.getService().isAppStartModeDisabled(job.getUid(),
+ job.getJob().getService().getPackageName())) {
+ Slog.w(TAG, "Aborting job " + job.getUid() + ":"
+ + job.getJob().toString() + " -- package not allowed to start");
+ mHandler.obtainMessage(MSG_STOP_JOB, job).sendToTarget();
+ return;
+ }
+ } catch (RemoteException e) {
+ }
+ if (mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT > 1
+ && job.getStandbyBucket() != ACTIVE_INDEX
+ && (job.getFirstForceBatchedTimeElapsed() == 0
+ || sElapsedRealtimeClock.millis() - job.getFirstForceBatchedTimeElapsed()
+ < mConstants.MAX_NON_ACTIVE_JOB_BATCH_DELAY_MS)) {
+ // Force batching non-ACTIVE jobs. Don't include them in the other counts.
+ forceBatchedCount++;
+ if (job.getFirstForceBatchedTimeElapsed() == 0) {
+ job.setFirstForceBatchedTimeElapsed(sElapsedRealtimeClock.millis());
+ }
+ } else {
+ unbatchedCount++;
+ if (job.getNumFailures() > 0) {
+ backoffCount++;
+ }
+ if (job.hasIdleConstraint()) {
+ idleCount++;
+ }
+ if (job.hasConnectivityConstraint()) {
+ connectivityCount++;
+ }
+ if (job.hasChargingConstraint()) {
+ chargingCount++;
+ }
+ if (job.hasBatteryNotLowConstraint()) {
+ batteryNotLowCount++;
+ }
+ if (job.hasStorageNotLowConstraint()) {
+ storageNotLowCount++;
+ }
+ if (job.hasContentTriggerConstraint()) {
+ contentCount++;
+ }
+ }
+ runnableJobs.add(job);
+ } else {
+ evaluateControllerStatesLocked(job);
+ }
+ }
+
+ public void postProcess() {
+ if (backoffCount > 0 ||
+ idleCount >= mConstants.MIN_IDLE_COUNT ||
+ connectivityCount >= mConstants.MIN_CONNECTIVITY_COUNT ||
+ chargingCount >= mConstants.MIN_CHARGING_COUNT ||
+ batteryNotLowCount >= mConstants.MIN_BATTERY_NOT_LOW_COUNT ||
+ storageNotLowCount >= mConstants.MIN_STORAGE_NOT_LOW_COUNT ||
+ contentCount >= mConstants.MIN_CONTENT_COUNT ||
+ forceBatchedCount >= mConstants.MIN_READY_NON_ACTIVE_JOBS_COUNT ||
+ (unbatchedCount > 0 && (unbatchedCount + forceBatchedCount)
+ >= mConstants.MIN_READY_JOBS_COUNT)) {
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Running jobs.");
+ }
+ noteJobsPending(runnableJobs);
+ mPendingJobs.addAll(runnableJobs);
+ if (mPendingJobs.size() > 1) {
+ mPendingJobs.sort(mEnqueueTimeComparator);
+ }
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "maybeQueueReadyJobsForExecutionLocked: Not running anything.");
+ }
+ }
+
+ // Be ready for next time
+ reset();
+ }
+
+ @VisibleForTesting
+ void reset() {
+ chargingCount = 0;
+ idleCount = 0;
+ backoffCount = 0;
+ connectivityCount = 0;
+ batteryNotLowCount = 0;
+ storageNotLowCount = 0;
+ contentCount = 0;
+ forceBatchedCount = 0;
+ unbatchedCount = 0;
+ runnableJobs.clear();
+ }
+ }
+ private final MaybeReadyJobQueueFunctor mMaybeQueueFunctor = new MaybeReadyJobQueueFunctor();
+
+ private void maybeQueueReadyJobsForExecutionLocked() {
+ if (DEBUG) Slog.d(TAG, "Maybe queuing ready jobs...");
+
+ noteJobsNonpending(mPendingJobs);
+ mPendingJobs.clear();
+ stopNonReadyActiveJobsLocked();
+ mJobs.forEachJob(mMaybeQueueFunctor);
+ mMaybeQueueFunctor.postProcess();
+ }
+
+ /**
+ * Heartbeat tracking. The heartbeat alarm is intentionally non-wakeup.
+ */
+ class HeartbeatAlarmListener implements AlarmManager.OnAlarmListener {
+
+ @Override
+ public void onAlarm() {
+ synchronized (mLock) {
+ final long sinceLast = sElapsedRealtimeClock.millis() - mLastHeartbeatTime;
+ final long beatsElapsed = sinceLast / mConstants.STANDBY_HEARTBEAT_TIME;
+ if (beatsElapsed > 0) {
+ mLastHeartbeatTime += beatsElapsed * mConstants.STANDBY_HEARTBEAT_TIME;
+ advanceHeartbeatLocked(beatsElapsed);
+ }
+ }
+ setNextHeartbeatAlarm();
+ }
+ }
+
+ // Intentionally does not touch the alarm timing
+ void advanceHeartbeatLocked(long beatsElapsed) {
+ if (!mConstants.USE_HEARTBEATS) {
+ return;
+ }
+ mHeartbeat += beatsElapsed;
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, "Advancing standby heartbeat by " + beatsElapsed
+ + " to " + mHeartbeat);
+ }
+ // Don't update ACTIVE or NEVER bucket milestones. Note that mHeartbeat
+ // will be equal to mNextBucketHeartbeat[bucket] for one beat, during which
+ // new jobs scheduled by apps in that bucket will be permitted to run
+ // immediately.
+ boolean didAdvanceBucket = false;
+ for (int i = 1; i < mNextBucketHeartbeat.length - 1; i++) {
+ // Did we reach or cross a bucket boundary?
+ if (mHeartbeat >= mNextBucketHeartbeat[i]) {
+ didAdvanceBucket = true;
+ }
+ while (mHeartbeat > mNextBucketHeartbeat[i]) {
+ mNextBucketHeartbeat[i] += mConstants.STANDBY_BEATS[i];
+ }
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, " Bucket " + i + " next heartbeat "
+ + mNextBucketHeartbeat[i]);
+ }
+ }
+
+ if (didAdvanceBucket) {
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, "Hit bucket boundary; reevaluating job runnability");
+ }
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+ }
+
+ void setNextHeartbeatAlarm() {
+ final long heartbeatLength;
+ synchronized (mLock) {
+ if (!mConstants.USE_HEARTBEATS) {
+ return;
+ }
+ heartbeatLength = mConstants.STANDBY_HEARTBEAT_TIME;
+ }
+ final long now = sElapsedRealtimeClock.millis();
+ final long nextBeatOrdinal = (now + heartbeatLength) / heartbeatLength;
+ final long nextHeartbeat = nextBeatOrdinal * heartbeatLength;
+ if (DEBUG_STANDBY) {
+ Slog.i(TAG, "Setting heartbeat alarm for " + nextHeartbeat
+ + " = " + TimeUtils.formatDuration(nextHeartbeat - now));
+ }
+ AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
+ am.setExact(AlarmManager.ELAPSED_REALTIME, nextHeartbeat,
+ HEARTBEAT_TAG, mHeartbeatAlarm, mHandler);
+ }
+
+ /** Returns true if both the calling and source users for the job are started. */
+ private boolean areUsersStartedLocked(final JobStatus job) {
+ boolean sourceStarted = ArrayUtils.contains(mStartedUsers, job.getSourceUserId());
+ if (job.getUserId() == job.getSourceUserId()) {
+ return sourceStarted;
+ }
+ return sourceStarted && ArrayUtils.contains(mStartedUsers, job.getUserId());
+ }
+
+ /**
+ * Criteria for moving a job into the pending queue:
+ * - It's ready.
+ * - It's not pending.
+ * - It's not already running on a JSC.
+ * - The user that requested the job is running.
+ * - The job's standby bucket has come due to be runnable.
+ * - The component is enabled and runnable.
+ */
+ @VisibleForTesting
+ boolean isReadyToBeExecutedLocked(JobStatus job) {
+ final boolean jobReady = job.isReady();
+
+ if (DEBUG) {
+ Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
+ + " ready=" + jobReady);
+ }
+
+ // This is a condition that is very likely to be false (most jobs that are
+ // scheduled are sitting there, not ready yet) and very cheap to check (just
+ // a few conditions on data in JobStatus).
+ if (!jobReady) {
+ if (job.getSourcePackageName().equals("android.jobscheduler.cts.jobtestapp")) {
+ Slog.v(TAG, " NOT READY: " + job);
+ }
+ return false;
+ }
+
+ final boolean jobExists = mJobs.containsJob(job);
+
+ final boolean userStarted = areUsersStartedLocked(job);
+
+ if (DEBUG) {
+ Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
+ + " exists=" + jobExists + " userStarted=" + userStarted);
+ }
+
+ // These are also fairly cheap to check, though they typically will not
+ // be conditions we fail.
+ if (!jobExists || !userStarted) {
+ return false;
+ }
+
+ if (isJobThermalConstrainedLocked(job)) {
+ return false;
+ }
+
+ final boolean jobPending = mPendingJobs.contains(job);
+ final boolean jobActive = isCurrentlyActiveLocked(job);
+
+ if (DEBUG) {
+ Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
+ + " pending=" + jobPending + " active=" + jobActive);
+ }
+
+ // These can be a little more expensive (especially jobActive, since we need to
+ // go through the array of all potentially active jobs), so we are doing them
+ // later... but still before checking with the package manager!
+ if (jobPending || jobActive) {
+ return false;
+ }
+
+ if (mConstants.USE_HEARTBEATS) {
+ // If the app is in a non-active standby bucket, make sure we've waited
+ // an appropriate amount of time since the last invocation. During device-
+ // wide parole, standby bucketing is ignored.
+ //
+ // Jobs in 'active' apps are not subject to standby, nor are jobs that are
+ // specifically marked as exempt.
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
+ + " parole=" + mInParole + " active=" + job.uidActive
+ + " exempt=" + job.getJob().isExemptedFromAppStandby());
+ }
+ if (!mInParole
+ && !job.uidActive
+ && !job.getJob().isExemptedFromAppStandby()) {
+ final int bucket = job.getStandbyBucket();
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, " bucket=" + bucket + " heartbeat=" + mHeartbeat
+ + " next=" + mNextBucketHeartbeat[bucket]);
+ }
+ if (mHeartbeat < mNextBucketHeartbeat[bucket]) {
+ // Only skip this job if the app is still waiting for the end of its nominal
+ // bucket interval. Once it's waited that long, we let it go ahead and clear.
+ // The final (NEVER) bucket is special; we never age those apps' jobs into
+ // runnability.
+ final long appLastRan = heartbeatWhenJobsLastRun(job);
+ if (bucket >= mConstants.STANDBY_BEATS.length
+ || (mHeartbeat > appLastRan
+ && mHeartbeat < appLastRan + mConstants.STANDBY_BEATS[bucket])) {
+ if (job.getWhenStandbyDeferred() == 0) {
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, "Bucket deferral: " + mHeartbeat + " < "
+ + (appLastRan + mConstants.STANDBY_BEATS[bucket])
+ + " for " + job);
+ }
+ job.setWhenStandbyDeferred(sElapsedRealtimeClock.millis());
+ }
+ return false;
+ } else {
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, "Bucket deferred job aged into runnability at "
+ + mHeartbeat + " : " + job);
+ }
+ }
+ }
+ }
+ }
+
+ // The expensive check: validate that the defined package+service is
+ // still present & viable.
+ return isComponentUsable(job);
+ }
+
+ private boolean isComponentUsable(@NonNull JobStatus job) {
+ final ServiceInfo service;
+ try {
+ // TODO: cache result until we're notified that something in the package changed.
+ service = AppGlobals.getPackageManager().getServiceInfo(
+ job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+ job.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+
+ if (service == null) {
+ if (DEBUG) {
+ Slog.v(TAG, "isComponentUsable: " + job.toShortString()
+ + " component not present");
+ }
+ return false;
+ }
+
+ // Everything else checked out so far, so this is the final yes/no check
+ final boolean appIsBad = mActivityManagerInternal.isAppBad(service.applicationInfo);
+ if (DEBUG && appIsBad) {
+ Slog.i(TAG, "App is bad for " + job.toShortString() + " so not runnable");
+ }
+ return !appIsBad;
+ }
+
+ @VisibleForTesting
+ void evaluateControllerStatesLocked(final JobStatus job) {
+ for (int c = mControllers.size() - 1; c >= 0; --c) {
+ final StateController sc = mControllers.get(c);
+ sc.evaluateStateLocked(job);
+ }
+ }
+
+ /**
+ * Returns true if non-job constraint components are in place -- if job.isReady() returns true
+ * and this method returns true, then the job is ready to be executed.
+ */
+ public boolean areComponentsInPlaceLocked(JobStatus job) {
+ // This code is very similar to the code in isReadyToBeExecutedLocked --- it uses the same
+ // conditions.
+
+ final boolean jobExists = mJobs.containsJob(job);
+ final boolean userStarted = areUsersStartedLocked(job);
+
+ if (DEBUG) {
+ Slog.v(TAG, "areComponentsInPlaceLocked: " + job.toShortString()
+ + " exists=" + jobExists + " userStarted=" + userStarted);
+ }
+
+ // These are also fairly cheap to check, though they typically will not
+ // be conditions we fail.
+ if (!jobExists || !userStarted) {
+ return false;
+ }
+
+ if (isJobThermalConstrainedLocked(job)) {
+ return false;
+ }
+
+ // Job pending/active doesn't affect the readiness of a job.
+
+ // Skipping the heartbeat check as this will only come into play when using the rolling
+ // window quota management system.
+
+ // The expensive check: validate that the defined package+service is
+ // still present & viable.
+ return isComponentUsable(job);
+ }
+
+ /** Returns the maximum amount of time this job could run for. */
+ public long getMaxJobExecutionTimeMs(JobStatus job) {
+ synchronized (mLock) {
+ if (mConstants.USE_HEARTBEATS) {
+ return JobServiceContext.EXECUTING_TIMESLICE_MILLIS;
+ }
+ return Math.min(mQuotaController.getMaxJobExecutionTimeMsLocked(job),
+ JobServiceContext.EXECUTING_TIMESLICE_MILLIS);
+ }
+ }
+
+ /**
+ * Reconcile jobs in the pending queue against available execution contexts.
+ * A controller can force a job into the pending queue even if it's already running, but
+ * here is where we decide whether to actually execute it.
+ */
+ void maybeRunPendingJobsLocked() {
+ if (DEBUG) {
+ Slog.d(TAG, "pending queue: " + mPendingJobs.size() + " jobs.");
+ }
+ mConcurrencyManager.assignJobsToContextsLocked();
+ reportActiveLocked();
+ }
+
+ private int adjustJobPriority(int curPriority, JobStatus job) {
+ if (curPriority < JobInfo.PRIORITY_TOP_APP) {
+ float factor = mJobPackageTracker.getLoadFactor(job);
+ if (factor >= mConstants.HEAVY_USE_FACTOR) {
+ curPriority += JobInfo.PRIORITY_ADJ_ALWAYS_RUNNING;
+ } else if (factor >= mConstants.MODERATE_USE_FACTOR) {
+ curPriority += JobInfo.PRIORITY_ADJ_OFTEN_RUNNING;
+ }
+ }
+ return curPriority;
+ }
+
+ int evaluateJobPriorityLocked(JobStatus job) {
+ int priority = job.getPriority();
+ if (priority >= JobInfo.PRIORITY_BOUND_FOREGROUND_SERVICE) {
+ return adjustJobPriority(priority, job);
+ }
+ int override = mUidPriorityOverride.get(job.getSourceUid(), 0);
+ if (override != 0) {
+ return adjustJobPriority(override, job);
+ }
+ return adjustJobPriority(priority, job);
+ }
+
+ final class LocalService implements JobSchedulerInternal {
+
+ /**
+ * The current bucket heartbeat ordinal
+ */
+ public long currentHeartbeat() {
+ return getCurrentHeartbeat();
+ }
+
+ /**
+ * Heartbeat ordinal at which the given standby bucket's jobs next become runnable
+ */
+ public long nextHeartbeatForBucket(int bucket) {
+ synchronized (mLock) {
+ return mNextBucketHeartbeat[bucket];
+ }
+ }
+
+ /**
+ * Heartbeat ordinal for the given app. This is typically the heartbeat at which
+ * the app last ran jobs, so that a newly-scheduled job in an app that hasn't run
+ * jobs in a long time is immediately runnable even if the app is bucketed into
+ * an infrequent time allocation.
+ */
+ public long baseHeartbeatForApp(String packageName, @UserIdInt int userId,
+ final int appStandbyBucket) {
+ if (appStandbyBucket == 0 ||
+ appStandbyBucket >= mConstants.STANDBY_BEATS.length) {
+ // ACTIVE => everything can be run right away
+ // NEVER => we won't run them anyway, so let them go in the future
+ // as soon as the app enters normal use
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, "Base heartbeat forced ZERO for new job in "
+ + packageName + "/" + userId);
+ }
+ return 0;
+ }
+
+ final long baseHeartbeat = heartbeatWhenJobsLastRun(packageName, userId);
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, "Base heartbeat " + baseHeartbeat + " for new job in "
+ + packageName + "/" + userId);
+ }
+ return baseHeartbeat;
+ }
+
+ public void noteJobStart(String packageName, int userId) {
+ synchronized (mLock) {
+ setLastJobHeartbeatLocked(packageName, userId, mHeartbeat);
+ }
+ }
+
+ /**
+ * Returns a list of all pending jobs. A running job is not considered pending. Periodic
+ * jobs are always considered pending.
+ */
+ @Override
+ public List<JobInfo> getSystemScheduledPendingJobs() {
+ synchronized (mLock) {
+ final List<JobInfo> pendingJobs = new ArrayList<JobInfo>();
+ mJobs.forEachJob(Process.SYSTEM_UID, (job) -> {
+ if (job.getJob().isPeriodic() || !isCurrentlyActiveLocked(job)) {
+ pendingJobs.add(job.getJob());
+ }
+ });
+ return pendingJobs;
+ }
+ }
+
+ @Override
+ public void cancelJobsForUid(int uid, String reason) {
+ JobSchedulerService.this.cancelJobsForUid(uid, reason);
+ }
+
+ @Override
+ public void addBackingUpUid(int uid) {
+ synchronized (mLock) {
+ // No need to actually do anything here, since for a full backup the
+ // activity manager will kill the process which will kill the job (and
+ // cause it to restart, but now it can't run).
+ mBackingUpUids.put(uid, uid);
+ }
+ }
+
+ @Override
+ public void removeBackingUpUid(int uid) {
+ synchronized (mLock) {
+ mBackingUpUids.delete(uid);
+ // If there are any jobs for this uid, we need to rebuild the pending list
+ // in case they are now ready to run.
+ if (mJobs.countJobsForUid(uid) > 0) {
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+ }
+ }
+
+ @Override
+ public void clearAllBackingUpUids() {
+ synchronized (mLock) {
+ if (mBackingUpUids.size() > 0) {
+ mBackingUpUids.clear();
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
+ }
+ }
+
+ @Override
+ public void reportAppUsage(String packageName, int userId) {
+ JobSchedulerService.this.reportAppUsage(packageName, userId);
+ }
+
+ @Override
+ public JobStorePersistStats getPersistStats() {
+ synchronized (mLock) {
+ return new JobStorePersistStats(mJobs.getPersistStats());
+ }
+ }
+ }
+
+ /**
+ * Tracking of app assignments to standby buckets
+ */
+ final class StandbyTracker extends AppIdleStateChangeListener {
+
+ // AppIdleStateChangeListener interface for live updates
+
+ @Override
+ public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId,
+ boolean idle, int bucket, int reason) {
+ // QuotaController handles this now.
+ }
+
+ @Override
+ public void onParoleStateChanged(boolean isParoleOn) {
+ if (DEBUG_STANDBY) {
+ Slog.i(TAG, "Global parole state now " + (isParoleOn ? "ON" : "OFF"));
+ }
+ mInParole = isParoleOn;
+ }
+
+ @Override
+ public void onUserInteractionStarted(String packageName, int userId) {
+ final int uid = mLocalPM.getPackageUid(packageName,
+ PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
+ if (uid < 0) {
+ // Quietly ignore; the case is already logged elsewhere
+ return;
+ }
+
+ long sinceLast = mUsageStats.getTimeSinceLastJobRun(packageName, userId);
+ if (sinceLast > 2 * DateUtils.DAY_IN_MILLIS) {
+ // Too long ago, not worth logging
+ sinceLast = 0L;
+ }
+ final DeferredJobCounter counter = new DeferredJobCounter();
+ synchronized (mLock) {
+ mJobs.forEachJobForSourceUid(uid, counter);
+ }
+ if (counter.numDeferred() > 0 || sinceLast > 0) {
+ BatteryStatsInternal mBatteryStatsInternal = LocalServices.getService
+ (BatteryStatsInternal.class);
+ mBatteryStatsInternal.noteJobsDeferred(uid, counter.numDeferred(), sinceLast);
+ StatsLog.write_non_chained(StatsLog.DEFERRED_JOB_STATS_REPORTED, uid, null,
+ counter.numDeferred(), sinceLast);
+ }
+ }
+ }
+
+ static class DeferredJobCounter implements Consumer<JobStatus> {
+ private int mDeferred = 0;
+
+ public int numDeferred() {
+ return mDeferred;
+ }
+
+ @Override
+ public void accept(JobStatus job) {
+ if (job.getWhenStandbyDeferred() > 0) {
+ mDeferred++;
+ }
+ }
+ }
+
+ public static int standbyBucketToBucketIndex(int bucket) {
+ // Normalize AppStandby constants to indices into our bookkeeping
+ if (bucket == UsageStatsManager.STANDBY_BUCKET_NEVER) return NEVER_INDEX;
+ else if (bucket > UsageStatsManager.STANDBY_BUCKET_FREQUENT) return RARE_INDEX;
+ else if (bucket > UsageStatsManager.STANDBY_BUCKET_WORKING_SET) return FREQUENT_INDEX;
+ else if (bucket > UsageStatsManager.STANDBY_BUCKET_ACTIVE) return WORKING_INDEX;
+ else return ACTIVE_INDEX;
+ }
+
+ // Static to support external callers
+ public static int standbyBucketForPackage(String packageName, int userId, long elapsedNow) {
+ UsageStatsManagerInternal usageStats = LocalServices.getService(
+ UsageStatsManagerInternal.class);
+ int bucket = usageStats != null
+ ? usageStats.getAppStandbyBucket(packageName, userId, elapsedNow)
+ : 0;
+
+ bucket = standbyBucketToBucketIndex(bucket);
+
+ if (DEBUG_STANDBY) {
+ Slog.v(TAG, packageName + "/" + userId + " standby bucket index: " + bucket);
+ }
+ return bucket;
+ }
+
+ /**
+ * Binder stub trampoline implementation
+ */
+ final class JobSchedulerStub extends IJobScheduler.Stub {
+ /** Cache determination of whether a given app can persist jobs
+ * key is uid of the calling app; value is undetermined/true/false
+ */
+ private final SparseArray<Boolean> mPersistCache = new SparseArray<Boolean>();
+
+ // Enforce that only the app itself (or shared uid participant) can schedule a
+ // job that runs one of the app's services, as well as verifying that the
+ // named service properly requires the BIND_JOB_SERVICE permission
+ private void enforceValidJobRequest(int uid, JobInfo job) {
+ final IPackageManager pm = AppGlobals.getPackageManager();
+ final ComponentName service = job.getService();
+ try {
+ ServiceInfo si = pm.getServiceInfo(service,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+ UserHandle.getUserId(uid));
+ if (si == null) {
+ throw new IllegalArgumentException("No such service " + service);
+ }
+ if (si.applicationInfo.uid != uid) {
+ throw new IllegalArgumentException("uid " + uid +
+ " cannot schedule job in " + service.getPackageName());
+ }
+ if (!JobService.PERMISSION_BIND.equals(si.permission)) {
+ throw new IllegalArgumentException("Scheduled service " + service
+ + " does not require android.permission.BIND_JOB_SERVICE permission");
+ }
+ } catch (RemoteException e) {
+ // Can't happen; the Package Manager is in this same process
+ }
+ }
+
+ private boolean canPersistJobs(int pid, int uid) {
+ // If we get this far we're good to go; all we need to do now is check
+ // whether the app is allowed to persist its scheduled work.
+ final boolean canPersist;
+ synchronized (mPersistCache) {
+ Boolean cached = mPersistCache.get(uid);
+ if (cached != null) {
+ canPersist = cached.booleanValue();
+ } else {
+ // Persisting jobs is tantamount to running at boot, so we permit
+ // it when the app has declared that it uses the RECEIVE_BOOT_COMPLETED
+ // permission
+ int result = getContext().checkPermission(
+ android.Manifest.permission.RECEIVE_BOOT_COMPLETED, pid, uid);
+ canPersist = (result == PackageManager.PERMISSION_GRANTED);
+ mPersistCache.put(uid, canPersist);
+ }
+ }
+ return canPersist;
+ }
+
+ private void validateJobFlags(JobInfo job, int callingUid) {
+ if ((job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.CONNECTIVITY_INTERNAL, TAG);
+ }
+ if ((job.getFlags() & JobInfo.FLAG_EXEMPT_FROM_APP_STANDBY) != 0) {
+ if (callingUid != Process.SYSTEM_UID) {
+ throw new SecurityException("Job has invalid flags");
+ }
+ if (job.isPeriodic()) {
+ Slog.wtf(TAG, "Periodic jobs mustn't have"
+ + " FLAG_EXEMPT_FROM_APP_STANDBY. Job=" + job);
+ }
+ }
+ }
+
+ // IJobScheduler implementation
+ @Override
+ public int schedule(JobInfo job) throws RemoteException {
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduling job: " + job.toString());
+ }
+ final int pid = Binder.getCallingPid();
+ final int uid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(uid);
+
+ enforceValidJobRequest(uid, job);
+ if (job.isPersisted()) {
+ if (!canPersistJobs(pid, uid)) {
+ throw new IllegalArgumentException("Error: requested job be persisted without"
+ + " holding RECEIVE_BOOT_COMPLETED permission.");
+ }
+ }
+
+ validateJobFlags(job, uid);
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.scheduleAsPackage(job, null, uid, null, userId,
+ null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ // IJobScheduler implementation
+ @Override
+ public int enqueue(JobInfo job, JobWorkItem work) throws RemoteException {
+ if (DEBUG) {
+ Slog.d(TAG, "Enqueueing job: " + job.toString() + " work: " + work);
+ }
+ final int uid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(uid);
+
+ enforceValidJobRequest(uid, job);
+ if (job.isPersisted()) {
+ throw new IllegalArgumentException("Can't enqueue work for persisted jobs");
+ }
+ if (work == null) {
+ throw new NullPointerException("work is null");
+ }
+
+ validateJobFlags(job, uid);
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.scheduleAsPackage(job, work, uid, null, userId,
+ null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public int scheduleAsPackage(JobInfo job, String packageName, int userId, String tag)
+ throws RemoteException {
+ final int callerUid = Binder.getCallingUid();
+ if (DEBUG) {
+ Slog.d(TAG, "Caller uid " + callerUid + " scheduling job: " + job.toString()
+ + " on behalf of " + packageName + "/");
+ }
+
+ if (packageName == null) {
+ throw new NullPointerException("Must specify a package for scheduleAsPackage()");
+ }
+
+ int mayScheduleForOthers = getContext().checkCallingOrSelfPermission(
+ android.Manifest.permission.UPDATE_DEVICE_STATS);
+ if (mayScheduleForOthers != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Caller uid " + callerUid
+ + " not permitted to schedule jobs for other apps");
+ }
+
+ validateJobFlags(job, callerUid);
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.scheduleAsPackage(job, null, callerUid,
+ packageName, userId, tag);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public ParceledListSlice<JobInfo> getAllPendingJobs() throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return new ParceledListSlice<>(JobSchedulerService.this.getPendingJobs(uid));
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public JobInfo getPendingJob(int jobId) throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return JobSchedulerService.this.getPendingJob(uid, jobId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void cancelAll() throws RemoteException {
+ final int uid = Binder.getCallingUid();
+ long ident = Binder.clearCallingIdentity();
+ try {
+ JobSchedulerService.this.cancelJobsForUid(uid,
+ "cancelAll() called by app, callingUid=" + uid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void cancel(int jobId) throws RemoteException {
+ final int uid = Binder.getCallingUid();
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ JobSchedulerService.this.cancelJob(uid, jobId, uid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * "dumpsys" infrastructure
+ */
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return;
+
+ int filterUid = -1;
+ boolean proto = false;
+ if (!ArrayUtils.isEmpty(args)) {
+ int opti = 0;
+ while (opti < args.length) {
+ String arg = args[opti];
+ if ("-h".equals(arg)) {
+ dumpHelp(pw);
+ return;
+ } else if ("-a".equals(arg)) {
+ // Ignore, we always dump all.
+ } else if ("--proto".equals(arg)) {
+ proto = true;
+ } else if (arg.length() > 0 && arg.charAt(0) == '-') {
+ pw.println("Unknown option: " + arg);
+ return;
+ } else {
+ break;
+ }
+ opti++;
+ }
+ if (opti < args.length) {
+ String pkg = args[opti];
+ try {
+ filterUid = getContext().getPackageManager().getPackageUid(pkg,
+ PackageManager.MATCH_ANY_USER);
+ } catch (NameNotFoundException ignored) {
+ pw.println("Invalid package: " + pkg);
+ return;
+ }
+ }
+ }
+
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ if (proto) {
+ JobSchedulerService.this.dumpInternalProto(fd, filterUid);
+ } else {
+ JobSchedulerService.this.dumpInternal(new IndentingPrintWriter(pw, " "),
+ filterUid);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+
+ @Override
+ public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
+ String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
+ (new JobSchedulerShellCommand(JobSchedulerService.this)).exec(
+ this, in, out, err, args, callback, resultReceiver);
+ }
+
+ /**
+ * <b>For internal system user only!</b>
+ * Returns a list of all currently-executing jobs.
+ */
+ @Override
+ public List<JobInfo> getStartedJobs() {
+ final int uid = Binder.getCallingUid();
+ if (uid != Process.SYSTEM_UID) {
+ throw new SecurityException(
+ "getStartedJobs() is system internal use only.");
+ }
+
+ final ArrayList<JobInfo> runningJobs;
+
+ synchronized (mLock) {
+ runningJobs = new ArrayList<>(mActiveServices.size());
+ for (JobServiceContext jsc : mActiveServices) {
+ final JobStatus job = jsc.getRunningJobLocked();
+ if (job != null) {
+ runningJobs.add(job.getJob());
+ }
+ }
+ }
+
+ return runningJobs;
+ }
+
+ /**
+ * <b>For internal system user only!</b>
+ * Returns a snapshot of the state of all jobs known to the system.
+ *
+ * <p class="note">This is a slow operation, so it should be called sparingly.
+ */
+ @Override
+ public ParceledListSlice<JobSnapshot> getAllJobSnapshots() {
+ final int uid = Binder.getCallingUid();
+ if (uid != Process.SYSTEM_UID) {
+ throw new SecurityException(
+ "getAllJobSnapshots() is system internal use only.");
+ }
+ synchronized (mLock) {
+ final ArrayList<JobSnapshot> snapshots = new ArrayList<>(mJobs.size());
+ mJobs.forEachJob((job) -> snapshots.add(
+ new JobSnapshot(job.getJob(), job.getSatisfiedConstraintFlags(),
+ isReadyToBeExecutedLocked(job))));
+ return new ParceledListSlice<>(snapshots);
+ }
+ }
+ };
+
+ // Shell command infrastructure: run the given job immediately
+ int executeRunCommand(String pkgName, int userId, int jobId, boolean force) {
+ if (DEBUG) {
+ Slog.v(TAG, "executeRunCommand(): " + pkgName + "/" + userId
+ + " " + jobId + " f=" + force);
+ }
+
+ try {
+ final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
+ userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
+ if (uid < 0) {
+ return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
+ }
+
+ synchronized (mLock) {
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+ if (js == null) {
+ return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
+ }
+
+ js.overrideState = (force) ? JobStatus.OVERRIDE_FULL : JobStatus.OVERRIDE_SOFT;
+ if (!js.isConstraintsSatisfied()) {
+ js.overrideState = 0;
+ return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS;
+ }
+
+ queueReadyJobsForExecutionLocked();
+ maybeRunPendingJobsLocked();
+ }
+ } catch (RemoteException e) {
+ // can't happen
+ }
+ return 0;
+ }
+
+ // Shell command infrastructure: immediately timeout currently executing jobs
+ int executeTimeoutCommand(PrintWriter pw, String pkgName, int userId,
+ boolean hasJobId, int jobId) {
+ if (DEBUG) {
+ Slog.v(TAG, "executeTimeoutCommand(): " + pkgName + "/" + userId + " " + jobId);
+ }
+
+ synchronized (mLock) {
+ boolean foundSome = false;
+ for (int i=0; i<mActiveServices.size(); i++) {
+ final JobServiceContext jc = mActiveServices.get(i);
+ final JobStatus js = jc.getRunningJobLocked();
+ if (jc.timeoutIfExecutingLocked(pkgName, userId, hasJobId, jobId, "shell")) {
+ foundSome = true;
+ pw.print("Timing out: ");
+ js.printUniqueId(pw);
+ pw.print(" ");
+ pw.println(js.getServiceComponent().flattenToShortString());
+ }
+ }
+ if (!foundSome) {
+ pw.println("No matching executing jobs found.");
+ }
+ }
+ return 0;
+ }
+
+ // Shell command infrastructure: cancel a scheduled job
+ int executeCancelCommand(PrintWriter pw, String pkgName, int userId,
+ boolean hasJobId, int jobId) {
+ if (DEBUG) {
+ Slog.v(TAG, "executeCancelCommand(): " + pkgName + "/" + userId + " " + jobId);
+ }
+
+ int pkgUid = -1;
+ try {
+ IPackageManager pm = AppGlobals.getPackageManager();
+ pkgUid = pm.getPackageUid(pkgName, 0, userId);
+ } catch (RemoteException e) { /* can't happen */ }
+
+ if (pkgUid < 0) {
+ pw.println("Package " + pkgName + " not found.");
+ return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
+ }
+
+ if (!hasJobId) {
+ pw.println("Canceling all jobs for " + pkgName + " in user " + userId);
+ if (!cancelJobsForUid(pkgUid, "cancel shell command for package")) {
+ pw.println("No matching jobs found.");
+ }
+ } else {
+ pw.println("Canceling job " + pkgName + "/#" + jobId + " in user " + userId);
+ if (!cancelJob(pkgUid, jobId, Process.SHELL_UID)) {
+ pw.println("No matching job found.");
+ }
+ }
+
+ return 0;
+ }
+
+ void setMonitorBattery(boolean enabled) {
+ synchronized (mLock) {
+ if (mBatteryController != null) {
+ mBatteryController.getTracker().setMonitorBatteryLocked(enabled);
+ }
+ }
+ }
+
+ int getBatterySeq() {
+ synchronized (mLock) {
+ return mBatteryController != null ? mBatteryController.getTracker().getSeq() : -1;
+ }
+ }
+
+ boolean getBatteryCharging() {
+ synchronized (mLock) {
+ return mBatteryController != null
+ ? mBatteryController.getTracker().isOnStablePower() : false;
+ }
+ }
+
+ boolean getBatteryNotLow() {
+ synchronized (mLock) {
+ return mBatteryController != null
+ ? mBatteryController.getTracker().isBatteryNotLow() : false;
+ }
+ }
+
+ int getStorageSeq() {
+ synchronized (mLock) {
+ return mStorageController != null ? mStorageController.getTracker().getSeq() : -1;
+ }
+ }
+
+ boolean getStorageNotLow() {
+ synchronized (mLock) {
+ return mStorageController != null
+ ? mStorageController.getTracker().isStorageNotLow() : false;
+ }
+ }
+
+ long getCurrentHeartbeat() {
+ synchronized (mLock) {
+ return mHeartbeat;
+ }
+ }
+
+ // Shell command infrastructure
+ int getJobState(PrintWriter pw, String pkgName, int userId, int jobId) {
+ try {
+ final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
+ userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
+ if (uid < 0) {
+ pw.print("unknown("); pw.print(pkgName); pw.println(")");
+ return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE;
+ }
+
+ synchronized (mLock) {
+ final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId);
+ if (DEBUG) Slog.d(TAG, "get-job-state " + uid + "/" + jobId + ": " + js);
+ if (js == null) {
+ pw.print("unknown("); UserHandle.formatUid(pw, uid);
+ pw.print("/jid"); pw.print(jobId); pw.println(")");
+ return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
+ }
+
+ boolean printed = false;
+ if (mPendingJobs.contains(js)) {
+ pw.print("pending");
+ printed = true;
+ }
+ if (isCurrentlyActiveLocked(js)) {
+ if (printed) {
+ pw.print(" ");
+ }
+ printed = true;
+ pw.println("active");
+ }
+ if (!ArrayUtils.contains(mStartedUsers, js.getUserId())) {
+ if (printed) {
+ pw.print(" ");
+ }
+ printed = true;
+ pw.println("user-stopped");
+ }
+ if (!ArrayUtils.contains(mStartedUsers, js.getSourceUserId())) {
+ if (printed) {
+ pw.print(" ");
+ }
+ printed = true;
+ pw.println("source-user-stopped");
+ }
+ if (mBackingUpUids.indexOfKey(js.getSourceUid()) >= 0) {
+ if (printed) {
+ pw.print(" ");
+ }
+ printed = true;
+ pw.println("backing-up");
+ }
+ boolean componentPresent = false;
+ try {
+ componentPresent = (AppGlobals.getPackageManager().getServiceInfo(
+ js.getServiceComponent(),
+ PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+ js.getUserId()) != null);
+ } catch (RemoteException e) {
+ }
+ if (!componentPresent) {
+ if (printed) {
+ pw.print(" ");
+ }
+ printed = true;
+ pw.println("no-component");
+ }
+ if (js.isReady()) {
+ if (printed) {
+ pw.print(" ");
+ }
+ printed = true;
+ pw.println("ready");
+ }
+ if (!printed) {
+ pw.print("waiting");
+ }
+ pw.println();
+ }
+ } catch (RemoteException e) {
+ // can't happen
+ }
+ return 0;
+ }
+
+ // Shell command infrastructure
+ int executeHeartbeatCommand(PrintWriter pw, int numBeats) {
+ if (numBeats < 1) {
+ pw.println(getCurrentHeartbeat());
+ return 0;
+ }
+
+ pw.print("Advancing standby heartbeat by ");
+ pw.println(numBeats);
+ synchronized (mLock) {
+ advanceHeartbeatLocked(numBeats);
+ }
+ return 0;
+ }
+
+ void triggerDockState(boolean idleState) {
+ final Intent dockIntent;
+ if (idleState) {
+ dockIntent = new Intent(Intent.ACTION_DOCK_IDLE);
+ } else {
+ dockIntent = new Intent(Intent.ACTION_DOCK_ACTIVE);
+ }
+ dockIntent.setPackage("android");
+ dockIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND);
+ getContext().sendBroadcastAsUser(dockIntent, UserHandle.ALL);
+ }
+
+ static void dumpHelp(PrintWriter pw) {
+ pw.println("Job Scheduler (jobscheduler) dump options:");
+ pw.println(" [-h] [package] ...");
+ pw.println(" -h: print this help");
+ pw.println(" [package] is an optional package name to limit the output to.");
+ }
+
+ /** Sort jobs by caller UID, then by Job ID. */
+ private static void sortJobs(List<JobStatus> jobs) {
+ Collections.sort(jobs, new Comparator<JobStatus>() {
+ @Override
+ public int compare(JobStatus o1, JobStatus o2) {
+ int uid1 = o1.getUid();
+ int uid2 = o2.getUid();
+ int id1 = o1.getJobId();
+ int id2 = o2.getJobId();
+ if (uid1 != uid2) {
+ return uid1 < uid2 ? -1 : 1;
+ }
+ return id1 < id2 ? -1 : (id1 > id2 ? 1 : 0);
+ }
+ });
+ }
+
+ void dumpInternal(final IndentingPrintWriter pw, int filterUid) {
+ final int filterUidFinal = UserHandle.getAppId(filterUid);
+ final long now = sSystemClock.millis();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final long nowUptime = sUptimeMillisClock.millis();
+
+ final Predicate<JobStatus> predicate = (js) -> {
+ return filterUidFinal == -1 || UserHandle.getAppId(js.getUid()) == filterUidFinal
+ || UserHandle.getAppId(js.getSourceUid()) == filterUidFinal;
+ };
+ synchronized (mLock) {
+ mConstants.dump(pw);
+ for (StateController controller : mControllers) {
+ pw.increaseIndent();
+ controller.dumpConstants(pw);
+ pw.decreaseIndent();
+ }
+ pw.println();
+
+ pw.println(" Heartbeat:");
+ pw.print(" Current: "); pw.println(mHeartbeat);
+ pw.println(" Next");
+ pw.print(" ACTIVE: "); pw.println(mNextBucketHeartbeat[0]);
+ pw.print(" WORKING: "); pw.println(mNextBucketHeartbeat[1]);
+ pw.print(" FREQUENT: "); pw.println(mNextBucketHeartbeat[2]);
+ pw.print(" RARE: "); pw.println(mNextBucketHeartbeat[3]);
+ pw.print(" Last heartbeat: ");
+ TimeUtils.formatDuration(mLastHeartbeatTime, nowElapsed, pw);
+ pw.println();
+ pw.print(" Next heartbeat: ");
+ TimeUtils.formatDuration(mLastHeartbeatTime + mConstants.STANDBY_HEARTBEAT_TIME,
+ nowElapsed, pw);
+ pw.println();
+ pw.print(" In parole?: ");
+ pw.print(mInParole);
+ pw.println();
+ pw.print(" In thermal throttling?: ");
+ pw.print(mThermalConstraint);
+ pw.println();
+ pw.println();
+
+ pw.println("Started users: " + Arrays.toString(mStartedUsers));
+ pw.print("Registered ");
+ pw.print(mJobs.size());
+ pw.println(" jobs:");
+ if (mJobs.size() > 0) {
+ final List<JobStatus> jobs = mJobs.mJobSet.getAllJobs();
+ sortJobs(jobs);
+ for (JobStatus job : jobs) {
+ pw.print(" JOB #"); job.printUniqueId(pw); pw.print(": ");
+ pw.println(job.toShortStringExceptUniqueId());
+
+ // Skip printing details if the caller requested a filter
+ if (!predicate.test(job)) {
+ continue;
+ }
+
+ job.dump(pw, " ", true, nowElapsed);
+ pw.print(" Last run heartbeat: ");
+ pw.print(heartbeatWhenJobsLastRun(job));
+ pw.println();
+
+ pw.print(" Ready: ");
+ pw.print(isReadyToBeExecutedLocked(job));
+ pw.print(" (job=");
+ pw.print(job.isReady());
+ pw.print(" user=");
+ pw.print(areUsersStartedLocked(job));
+ pw.print(" !pending=");
+ pw.print(!mPendingJobs.contains(job));
+ pw.print(" !active=");
+ pw.print(!isCurrentlyActiveLocked(job));
+ pw.print(" !backingup=");
+ pw.print(!(mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0));
+ pw.print(" comp=");
+ boolean componentPresent = false;
+ try {
+ componentPresent = (AppGlobals.getPackageManager().getServiceInfo(
+ job.getServiceComponent(),
+ PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+ job.getUserId()) != null);
+ } catch (RemoteException e) {
+ }
+ pw.print(componentPresent);
+ pw.println(")");
+ }
+ } else {
+ pw.println(" None.");
+ }
+ for (int i=0; i<mControllers.size(); i++) {
+ pw.println();
+ pw.println(mControllers.get(i).getClass().getSimpleName() + ":");
+ pw.increaseIndent();
+ mControllers.get(i).dumpControllerStateLocked(pw, predicate);
+ pw.decreaseIndent();
+ }
+ pw.println();
+ pw.println("Uid priority overrides:");
+ for (int i=0; i< mUidPriorityOverride.size(); i++) {
+ int uid = mUidPriorityOverride.keyAt(i);
+ if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) {
+ pw.print(" "); pw.print(UserHandle.formatUid(uid));
+ pw.print(": "); pw.println(mUidPriorityOverride.valueAt(i));
+ }
+ }
+ if (mBackingUpUids.size() > 0) {
+ pw.println();
+ pw.println("Backing up uids:");
+ boolean first = true;
+ for (int i = 0; i < mBackingUpUids.size(); i++) {
+ int uid = mBackingUpUids.keyAt(i);
+ if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) {
+ if (first) {
+ pw.print(" ");
+ first = false;
+ } else {
+ pw.print(", ");
+ }
+ pw.print(UserHandle.formatUid(uid));
+ }
+ }
+ pw.println();
+ }
+ pw.println();
+ mJobPackageTracker.dump(pw, "", filterUidFinal);
+ pw.println();
+ if (mJobPackageTracker.dumpHistory(pw, "", filterUidFinal)) {
+ pw.println();
+ }
+ pw.println("Pending queue:");
+ for (int i=0; i<mPendingJobs.size(); i++) {
+ JobStatus job = mPendingJobs.get(i);
+ pw.print(" Pending #"); pw.print(i); pw.print(": ");
+ pw.println(job.toShortString());
+ job.dump(pw, " ", false, nowElapsed);
+ int priority = evaluateJobPriorityLocked(job);
+ pw.print(" Evaluated priority: ");
+ pw.println(JobInfo.getPriorityString(priority));
+
+ pw.print(" Tag: "); pw.println(job.getTag());
+ pw.print(" Enq: ");
+ TimeUtils.formatDuration(job.madePending - nowUptime, pw);
+ pw.println();
+ }
+ pw.println();
+ pw.println("Active jobs:");
+ for (int i=0; i<mActiveServices.size(); i++) {
+ JobServiceContext jsc = mActiveServices.get(i);
+ pw.print(" Slot #"); pw.print(i); pw.print(": ");
+ final JobStatus job = jsc.getRunningJobLocked();
+ if (job == null) {
+ if (jsc.mStoppedReason != null) {
+ pw.print("inactive since ");
+ TimeUtils.formatDuration(jsc.mStoppedTime, nowElapsed, pw);
+ pw.print(", stopped because: ");
+ pw.println(jsc.mStoppedReason);
+ } else {
+ pw.println("inactive");
+ }
+ continue;
+ } else {
+ pw.println(job.toShortString());
+ pw.print(" Running for: ");
+ TimeUtils.formatDuration(nowElapsed - jsc.getExecutionStartTimeElapsed(), pw);
+ pw.print(", timeout at: ");
+ TimeUtils.formatDuration(jsc.getTimeoutElapsed() - nowElapsed, pw);
+ pw.println();
+ job.dump(pw, " ", false, nowElapsed);
+ int priority = evaluateJobPriorityLocked(jsc.getRunningJobLocked());
+ pw.print(" Evaluated priority: ");
+ pw.println(JobInfo.getPriorityString(priority));
+
+ pw.print(" Active at ");
+ TimeUtils.formatDuration(job.madeActive - nowUptime, pw);
+ pw.print(", pending for ");
+ TimeUtils.formatDuration(job.madeActive - job.madePending, pw);
+ pw.println();
+ }
+ }
+ if (filterUid == -1) {
+ pw.println();
+ pw.print("mReadyToRock="); pw.println(mReadyToRock);
+ pw.print("mReportedActive="); pw.println(mReportedActive);
+ }
+ pw.println();
+
+ mConcurrencyManager.dumpLocked(pw, now, nowElapsed);
+
+ pw.println();
+ pw.print("PersistStats: ");
+ pw.println(mJobs.getPersistStats());
+ }
+ pw.println();
+ }
+
+ void dumpInternalProto(final FileDescriptor fd, int filterUid) {
+ ProtoOutputStream proto = new ProtoOutputStream(fd);
+ final int filterUidFinal = UserHandle.getAppId(filterUid);
+ final long now = sSystemClock.millis();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final long nowUptime = sUptimeMillisClock.millis();
+ final Predicate<JobStatus> predicate = (js) -> {
+ return filterUidFinal == -1 || UserHandle.getAppId(js.getUid()) == filterUidFinal
+ || UserHandle.getAppId(js.getSourceUid()) == filterUidFinal;
+ };
+
+ synchronized (mLock) {
+ final long settingsToken = proto.start(JobSchedulerServiceDumpProto.SETTINGS);
+ mConstants.dump(proto);
+ for (StateController controller : mControllers) {
+ controller.dumpConstants(proto);
+ }
+ proto.end(settingsToken);
+
+ proto.write(JobSchedulerServiceDumpProto.CURRENT_HEARTBEAT, mHeartbeat);
+ proto.write(JobSchedulerServiceDumpProto.NEXT_HEARTBEAT, mNextBucketHeartbeat[0]);
+ proto.write(JobSchedulerServiceDumpProto.NEXT_HEARTBEAT, mNextBucketHeartbeat[1]);
+ proto.write(JobSchedulerServiceDumpProto.NEXT_HEARTBEAT, mNextBucketHeartbeat[2]);
+ proto.write(JobSchedulerServiceDumpProto.NEXT_HEARTBEAT, mNextBucketHeartbeat[3]);
+ proto.write(JobSchedulerServiceDumpProto.LAST_HEARTBEAT_TIME_MILLIS,
+ mLastHeartbeatTime - nowUptime);
+ proto.write(JobSchedulerServiceDumpProto.NEXT_HEARTBEAT_TIME_MILLIS,
+ mLastHeartbeatTime + mConstants.STANDBY_HEARTBEAT_TIME - nowUptime);
+ proto.write(JobSchedulerServiceDumpProto.IN_PAROLE, mInParole);
+ proto.write(JobSchedulerServiceDumpProto.IN_THERMAL, mThermalConstraint);
+
+ for (int u : mStartedUsers) {
+ proto.write(JobSchedulerServiceDumpProto.STARTED_USERS, u);
+ }
+ if (mJobs.size() > 0) {
+ final List<JobStatus> jobs = mJobs.mJobSet.getAllJobs();
+ sortJobs(jobs);
+ for (JobStatus job : jobs) {
+ final long rjToken = proto.start(JobSchedulerServiceDumpProto.REGISTERED_JOBS);
+ job.writeToShortProto(proto, JobSchedulerServiceDumpProto.RegisteredJob.INFO);
+
+ // Skip printing details if the caller requested a filter
+ if (!predicate.test(job)) {
+ continue;
+ }
+
+ job.dump(proto, JobSchedulerServiceDumpProto.RegisteredJob.DUMP, true, nowElapsed);
+
+ // isReadyToBeExecuted
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_READY,
+ job.isReady());
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_USER_STARTED,
+ areUsersStartedLocked(job));
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_PENDING,
+ mPendingJobs.contains(job));
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_CURRENTLY_ACTIVE,
+ isCurrentlyActiveLocked(job));
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_UID_BACKING_UP,
+ mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0);
+ boolean componentPresent = false;
+ try {
+ componentPresent = (AppGlobals.getPackageManager().getServiceInfo(
+ job.getServiceComponent(),
+ PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+ job.getUserId()) != null);
+ } catch (RemoteException e) {
+ }
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_COMPONENT_PRESENT,
+ componentPresent);
+ proto.write(RegisteredJob.LAST_RUN_HEARTBEAT, heartbeatWhenJobsLastRun(job));
+
+ proto.end(rjToken);
+ }
+ }
+ for (StateController controller : mControllers) {
+ controller.dumpControllerStateLocked(
+ proto, JobSchedulerServiceDumpProto.CONTROLLERS, predicate);
+ }
+ for (int i=0; i< mUidPriorityOverride.size(); i++) {
+ int uid = mUidPriorityOverride.keyAt(i);
+ if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) {
+ long pToken = proto.start(JobSchedulerServiceDumpProto.PRIORITY_OVERRIDES);
+ proto.write(JobSchedulerServiceDumpProto.PriorityOverride.UID, uid);
+ proto.write(JobSchedulerServiceDumpProto.PriorityOverride.OVERRIDE_VALUE,
+ mUidPriorityOverride.valueAt(i));
+ proto.end(pToken);
+ }
+ }
+ for (int i = 0; i < mBackingUpUids.size(); i++) {
+ int uid = mBackingUpUids.keyAt(i);
+ if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) {
+ proto.write(JobSchedulerServiceDumpProto.BACKING_UP_UIDS, uid);
+ }
+ }
+
+ mJobPackageTracker.dump(proto, JobSchedulerServiceDumpProto.PACKAGE_TRACKER,
+ filterUidFinal);
+ mJobPackageTracker.dumpHistory(proto, JobSchedulerServiceDumpProto.HISTORY,
+ filterUidFinal);
+
+ for (JobStatus job : mPendingJobs) {
+ final long pjToken = proto.start(JobSchedulerServiceDumpProto.PENDING_JOBS);
+
+ job.writeToShortProto(proto, PendingJob.INFO);
+ job.dump(proto, PendingJob.DUMP, false, nowElapsed);
+ proto.write(PendingJob.EVALUATED_PRIORITY, evaluateJobPriorityLocked(job));
+ proto.write(PendingJob.ENQUEUED_DURATION_MS, nowUptime - job.madePending);
+
+ proto.end(pjToken);
+ }
+ for (JobServiceContext jsc : mActiveServices) {
+ final long ajToken = proto.start(JobSchedulerServiceDumpProto.ACTIVE_JOBS);
+ final JobStatus job = jsc.getRunningJobLocked();
+
+ if (job == null) {
+ final long ijToken = proto.start(ActiveJob.INACTIVE);
+
+ proto.write(ActiveJob.InactiveJob.TIME_SINCE_STOPPED_MS,
+ nowElapsed - jsc.mStoppedTime);
+ if (jsc.mStoppedReason != null) {
+ proto.write(ActiveJob.InactiveJob.STOPPED_REASON,
+ jsc.mStoppedReason);
+ }
+
+ proto.end(ijToken);
+ } else {
+ final long rjToken = proto.start(ActiveJob.RUNNING);
+
+ job.writeToShortProto(proto, ActiveJob.RunningJob.INFO);
+
+ proto.write(ActiveJob.RunningJob.RUNNING_DURATION_MS,
+ nowElapsed - jsc.getExecutionStartTimeElapsed());
+ proto.write(ActiveJob.RunningJob.TIME_UNTIL_TIMEOUT_MS,
+ jsc.getTimeoutElapsed() - nowElapsed);
+
+ job.dump(proto, ActiveJob.RunningJob.DUMP, false, nowElapsed);
+
+ proto.write(ActiveJob.RunningJob.EVALUATED_PRIORITY,
+ evaluateJobPriorityLocked(jsc.getRunningJobLocked()));
+
+ proto.write(ActiveJob.RunningJob.TIME_SINCE_MADE_ACTIVE_MS,
+ nowUptime - job.madeActive);
+ proto.write(ActiveJob.RunningJob.PENDING_DURATION_MS,
+ job.madeActive - job.madePending);
+
+ proto.end(rjToken);
+ }
+ proto.end(ajToken);
+ }
+ if (filterUid == -1) {
+ proto.write(JobSchedulerServiceDumpProto.IS_READY_TO_ROCK, mReadyToRock);
+ proto.write(JobSchedulerServiceDumpProto.REPORTED_ACTIVE, mReportedActive);
+ }
+ mConcurrencyManager.dumpProtoLocked(proto,
+ JobSchedulerServiceDumpProto.CONCURRENCY_MANAGER, now, nowElapsed);
+ }
+
+ proto.flush();
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
new file mode 100644
index 0000000..e361441
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.ShellCommand;
+import android.os.UserHandle;
+
+import java.io.PrintWriter;
+
+public final class JobSchedulerShellCommand extends ShellCommand {
+ public static final int CMD_ERR_NO_PACKAGE = -1000;
+ public static final int CMD_ERR_NO_JOB = -1001;
+ public static final int CMD_ERR_CONSTRAINTS = -1002;
+
+ JobSchedulerService mInternal;
+ IPackageManager mPM;
+
+ JobSchedulerShellCommand(JobSchedulerService service) {
+ mInternal = service;
+ mPM = AppGlobals.getPackageManager();
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ switch (cmd != null ? cmd : "") {
+ case "run":
+ return runJob(pw);
+ case "timeout":
+ return timeout(pw);
+ case "cancel":
+ return cancelJob(pw);
+ case "monitor-battery":
+ return monitorBattery(pw);
+ case "get-battery-seq":
+ return getBatterySeq(pw);
+ case "get-battery-charging":
+ return getBatteryCharging(pw);
+ case "get-battery-not-low":
+ return getBatteryNotLow(pw);
+ case "get-storage-seq":
+ return getStorageSeq(pw);
+ case "get-storage-not-low":
+ return getStorageNotLow(pw);
+ case "get-job-state":
+ return getJobState(pw);
+ case "heartbeat":
+ return doHeartbeat(pw);
+ case "trigger-dock-state":
+ return triggerDockState(pw);
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ } catch (Exception e) {
+ pw.println("Exception: " + e);
+ }
+ return -1;
+ }
+
+ private void checkPermission(String operation) throws Exception {
+ final int uid = Binder.getCallingUid();
+ if (uid == 0) {
+ // Root can do anything.
+ return;
+ }
+ final int perm = mPM.checkUidPermission(
+ "android.permission.CHANGE_APP_IDLE_STATE", uid);
+ if (perm != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Uid " + uid
+ + " not permitted to " + operation);
+ }
+ }
+
+ private boolean printError(int errCode, String pkgName, int userId, int jobId) {
+ PrintWriter pw;
+ switch (errCode) {
+ case CMD_ERR_NO_PACKAGE:
+ pw = getErrPrintWriter();
+ pw.print("Package not found: ");
+ pw.print(pkgName);
+ pw.print(" / user ");
+ pw.println(userId);
+ return true;
+
+ case CMD_ERR_NO_JOB:
+ pw = getErrPrintWriter();
+ pw.print("Could not find job ");
+ pw.print(jobId);
+ pw.print(" in package ");
+ pw.print(pkgName);
+ pw.print(" / user ");
+ pw.println(userId);
+ return true;
+
+ case CMD_ERR_CONSTRAINTS:
+ pw = getErrPrintWriter();
+ pw.print("Job ");
+ pw.print(jobId);
+ pw.print(" in package ");
+ pw.print(pkgName);
+ pw.print(" / user ");
+ pw.print(userId);
+ pw.println(" has functional constraints but --force not specified");
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ private int runJob(PrintWriter pw) throws Exception {
+ checkPermission("force scheduled jobs");
+
+ boolean force = false;
+ int userId = UserHandle.USER_SYSTEM;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-f":
+ case "--force":
+ force = true;
+ break;
+
+ case "-u":
+ case "--user":
+ userId = Integer.parseInt(getNextArgRequired());
+ break;
+
+ default:
+ pw.println("Error: unknown option '" + opt + "'");
+ return -1;
+ }
+ }
+
+ final String pkgName = getNextArgRequired();
+ final int jobId = Integer.parseInt(getNextArgRequired());
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ int ret = mInternal.executeRunCommand(pkgName, userId, jobId, force);
+ if (printError(ret, pkgName, userId, jobId)) {
+ return ret;
+ }
+
+ // success!
+ pw.print("Running job");
+ if (force) {
+ pw.print(" [FORCED]");
+ }
+ pw.println();
+
+ return ret;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private int timeout(PrintWriter pw) throws Exception {
+ checkPermission("force timeout jobs");
+
+ int userId = UserHandle.USER_ALL;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-u":
+ case "--user":
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ break;
+
+ default:
+ pw.println("Error: unknown option '" + opt + "'");
+ return -1;
+ }
+ }
+
+ if (userId == UserHandle.USER_CURRENT) {
+ userId = ActivityManager.getCurrentUser();
+ }
+
+ final String pkgName = getNextArg();
+ final String jobIdStr = getNextArg();
+ final int jobId = jobIdStr != null ? Integer.parseInt(jobIdStr) : -1;
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return mInternal.executeTimeoutCommand(pw, pkgName, userId, jobIdStr != null, jobId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private int cancelJob(PrintWriter pw) throws Exception {
+ checkPermission("cancel jobs");
+
+ int userId = UserHandle.USER_SYSTEM;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-u":
+ case "--user":
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ break;
+
+ default:
+ pw.println("Error: unknown option '" + opt + "'");
+ return -1;
+ }
+ }
+
+ if (userId < 0) {
+ pw.println("Error: must specify a concrete user ID");
+ return -1;
+ }
+
+ final String pkgName = getNextArg();
+ final String jobIdStr = getNextArg();
+ final int jobId = jobIdStr != null ? Integer.parseInt(jobIdStr) : -1;
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return mInternal.executeCancelCommand(pw, pkgName, userId, jobIdStr != null, jobId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private int monitorBattery(PrintWriter pw) throws Exception {
+ checkPermission("change battery monitoring");
+ String opt = getNextArgRequired();
+ boolean enabled;
+ if ("on".equals(opt)) {
+ enabled = true;
+ } else if ("off".equals(opt)) {
+ enabled = false;
+ } else {
+ getErrPrintWriter().println("Error: unknown option " + opt);
+ return 1;
+ }
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mInternal.setMonitorBattery(enabled);
+ if (enabled) pw.println("Battery monitoring enabled");
+ else pw.println("Battery monitoring disabled");
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return 0;
+ }
+
+ private int getBatterySeq(PrintWriter pw) {
+ int seq = mInternal.getBatterySeq();
+ pw.println(seq);
+ return 0;
+ }
+
+ private int getBatteryCharging(PrintWriter pw) {
+ boolean val = mInternal.getBatteryCharging();
+ pw.println(val);
+ return 0;
+ }
+
+ private int getBatteryNotLow(PrintWriter pw) {
+ boolean val = mInternal.getBatteryNotLow();
+ pw.println(val);
+ return 0;
+ }
+
+ private int getStorageSeq(PrintWriter pw) {
+ int seq = mInternal.getStorageSeq();
+ pw.println(seq);
+ return 0;
+ }
+
+ private int getStorageNotLow(PrintWriter pw) {
+ boolean val = mInternal.getStorageNotLow();
+ pw.println(val);
+ return 0;
+ }
+
+ private int getJobState(PrintWriter pw) throws Exception {
+ checkPermission("force timeout jobs");
+
+ int userId = UserHandle.USER_SYSTEM;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-u":
+ case "--user":
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ break;
+
+ default:
+ pw.println("Error: unknown option '" + opt + "'");
+ return -1;
+ }
+ }
+
+ if (userId == UserHandle.USER_CURRENT) {
+ userId = ActivityManager.getCurrentUser();
+ }
+
+ final String pkgName = getNextArgRequired();
+ final String jobIdStr = getNextArgRequired();
+ final int jobId = Integer.parseInt(jobIdStr);
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ int ret = mInternal.getJobState(pw, pkgName, userId, jobId);
+ printError(ret, pkgName, userId, jobId);
+ return ret;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private int doHeartbeat(PrintWriter pw) throws Exception {
+ checkPermission("manipulate scheduler heartbeat");
+
+ final String arg = getNextArg();
+ final int numBeats = (arg != null) ? Integer.parseInt(arg) : 0;
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return mInternal.executeHeartbeatCommand(pw, numBeats);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private int triggerDockState(PrintWriter pw) throws Exception {
+ checkPermission("trigger wireless charging dock state");
+
+ final String opt = getNextArgRequired();
+ boolean idleState;
+ if ("idle".equals(opt)) {
+ idleState = true;
+ } else if ("active".equals(opt)) {
+ idleState = false;
+ } else {
+ getErrPrintWriter().println("Error: unknown option " + opt);
+ return 1;
+ }
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mInternal.triggerDockState(idleState);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return 0;
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutPrintWriter();
+
+ pw.println("Job scheduler (jobscheduler) commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println(" run [-f | --force] [-u | --user USER_ID] PACKAGE JOB_ID");
+ pw.println(" Trigger immediate execution of a specific scheduled job.");
+ pw.println(" Options:");
+ pw.println(" -f or --force: run the job even if technical constraints such as");
+ pw.println(" connectivity are not currently met");
+ pw.println(" -u or --user: specify which user's job is to be run; the default is");
+ pw.println(" the primary or system user");
+ pw.println(" timeout [-u | --user USER_ID] [PACKAGE] [JOB_ID]");
+ pw.println(" Trigger immediate timeout of currently executing jobs, as if their.");
+ pw.println(" execution timeout had expired.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's job is to be run; the default is");
+ pw.println(" all users");
+ pw.println(" cancel [-u | --user USER_ID] PACKAGE [JOB_ID]");
+ pw.println(" Cancel a scheduled job. If a job ID is not supplied, all jobs scheduled");
+ pw.println(" by that package will be canceled. USE WITH CAUTION.");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's job is to be run; the default is");
+ pw.println(" the primary or system user");
+ pw.println(" heartbeat [num]");
+ pw.println(" With no argument, prints the current standby heartbeat. With a positive");
+ pw.println(" argument, advances the standby heartbeat by that number.");
+ pw.println(" monitor-battery [on|off]");
+ pw.println(" Control monitoring of all battery changes. Off by default. Turning");
+ pw.println(" on makes get-battery-seq useful.");
+ pw.println(" get-battery-seq");
+ pw.println(" Return the last battery update sequence number that was received.");
+ pw.println(" get-battery-charging");
+ pw.println(" Return whether the battery is currently considered to be charging.");
+ pw.println(" get-battery-not-low");
+ pw.println(" Return whether the battery is currently considered to not be low.");
+ pw.println(" get-storage-seq");
+ pw.println(" Return the last storage update sequence number that was received.");
+ pw.println(" get-storage-not-low");
+ pw.println(" Return whether storage is currently considered to not be low.");
+ pw.println(" get-job-state [-u | --user USER_ID] PACKAGE JOB_ID");
+ pw.println(" Return the current state of a job, may be any combination of:");
+ pw.println(" pending: currently on the pending list, waiting to be active");
+ pw.println(" active: job is actively running");
+ pw.println(" user-stopped: job can't run because its user is stopped");
+ pw.println(" backing-up: job can't run because app is currently backing up its data");
+ pw.println(" no-component: job can't run because its component is not available");
+ pw.println(" ready: job is ready to run (all constraints satisfied or bypassed)");
+ pw.println(" waiting: if nothing else above is printed, job not ready to run");
+ pw.println(" Options:");
+ pw.println(" -u or --user: specify which user's job is to be run; the default is");
+ pw.println(" the primary or system user");
+ pw.println(" trigger-dock-state [idle|active]");
+ pw.println(" Trigger wireless charging dock state. Active by default.");
+ pw.println();
+ }
+
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
new file mode 100644
index 0000000..7da128f
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -0,0 +1,856 @@
+/*
+ * Copyright (C) 2014 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;
+
+import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+
+import android.app.ActivityManager;
+import android.app.job.IJobCallback;
+import android.app.job.IJobService;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobWorkItem;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.util.EventLog;
+import android.util.Slog;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IBatteryStats;
+import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
+import com.android.server.job.controllers.JobStatus;
+
+/**
+ * Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this
+ * class.
+ *
+ * There are two important interactions into this class from the
+ * {@link com.android.server.job.JobSchedulerService}. To execute a job and to cancel a job.
+ * - Execution of a new job is handled by the {@link #mAvailable}. This bit is flipped once when a
+ * job lands, and again when it is complete.
+ * - Cancelling is trickier, because there are also interactions from the client. It's possible
+ * the {@link com.android.server.job.JobServiceContext.JobServiceHandler} tries to process a
+ * {@link #doCancelLocked} after the client has already finished. This is handled by having
+ * {@link com.android.server.job.JobServiceContext.JobServiceHandler#handleCancelLocked} check whether
+ * the context is still valid.
+ * To mitigate this, we avoid sending duplicate onStopJob()
+ * calls to the client after they've specified jobFinished().
+ */
+public final class JobServiceContext implements ServiceConnection {
+ private static final boolean DEBUG = JobSchedulerService.DEBUG;
+ private static final boolean DEBUG_STANDBY = JobSchedulerService.DEBUG_STANDBY;
+
+ private static final String TAG = "JobServiceContext";
+ /** Amount of time a job is allowed to execute for before being considered timed-out. */
+ public static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000; // 10mins.
+ /** Amount of time the JobScheduler waits for the initial service launch+bind. */
+ private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000;
+ /** Amount of time the JobScheduler will wait for a response from an app for a message. */
+ private static final long OP_TIMEOUT_MILLIS = 8 * 1000;
+
+ private static final String[] VERB_STRINGS = {
+ "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED"
+ };
+
+ // States that a job occupies while interacting with the client.
+ static final int VERB_BINDING = 0;
+ static final int VERB_STARTING = 1;
+ static final int VERB_EXECUTING = 2;
+ static final int VERB_STOPPING = 3;
+ static final int VERB_FINISHED = 4;
+
+ // Messages that result from interactions with the client service.
+ /** System timed out waiting for a response. */
+ private static final int MSG_TIMEOUT = 0;
+
+ public static final int NO_PREFERRED_UID = -1;
+
+ private final Handler mCallbackHandler;
+ /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */
+ private final JobCompletedListener mCompletedListener;
+ /** Used for service binding, etc. */
+ private final Context mContext;
+ private final Object mLock;
+ private final IBatteryStats mBatteryStats;
+ private final JobPackageTracker mJobPackageTracker;
+ private PowerManager.WakeLock mWakeLock;
+
+ // Execution state.
+ private JobParameters mParams;
+ @VisibleForTesting
+ int mVerb;
+ private boolean mCancelled;
+
+ /**
+ * All the information maintained about the job currently being executed.
+ *
+ * Any reads (dereferences) not done from the handler thread must be synchronized on
+ * {@link #mLock}.
+ * Writes can only be done from the handler thread, or {@link #executeRunnableJob(JobStatus)}.
+ */
+ private JobStatus mRunningJob;
+ private JobCallback mRunningCallback;
+ /** Used to store next job to run when current job is to be preempted. */
+ private int mPreferredUid;
+ IJobService service;
+
+ /**
+ * Whether this context is free. This is set to false at the start of execution, and reset to
+ * true when execution is complete.
+ */
+ @GuardedBy("mLock")
+ private boolean mAvailable;
+ /** Track start time. */
+ private long mExecutionStartTimeElapsed;
+ /** Track when job will timeout. */
+ private long mTimeoutElapsed;
+
+ // Debugging: reason this job was last stopped.
+ public String mStoppedReason;
+
+ // Debugging: time this job was last stopped.
+ public long mStoppedTime;
+
+ final class JobCallback extends IJobCallback.Stub {
+ public String mStoppedReason;
+ public long mStoppedTime;
+
+ @Override
+ public void acknowledgeStartMessage(int jobId, boolean ongoing) {
+ doAcknowledgeStartMessage(this, jobId, ongoing);
+ }
+
+ @Override
+ public void acknowledgeStopMessage(int jobId, boolean reschedule) {
+ doAcknowledgeStopMessage(this, jobId, reschedule);
+ }
+
+ @Override
+ public JobWorkItem dequeueWork(int jobId) {
+ return doDequeueWork(this, jobId);
+ }
+
+ @Override
+ public boolean completeWork(int jobId, int workId) {
+ return doCompleteWork(this, jobId, workId);
+ }
+
+ @Override
+ public void jobFinished(int jobId, boolean reschedule) {
+ doJobFinished(this, jobId, reschedule);
+ }
+ }
+
+ JobServiceContext(JobSchedulerService service, IBatteryStats batteryStats,
+ JobPackageTracker tracker, Looper looper) {
+ this(service.getContext(), service.getLock(), batteryStats, tracker, service, looper);
+ }
+
+ @VisibleForTesting
+ JobServiceContext(Context context, Object lock, IBatteryStats batteryStats,
+ JobPackageTracker tracker, JobCompletedListener completedListener, Looper looper) {
+ mContext = context;
+ mLock = lock;
+ mBatteryStats = batteryStats;
+ mJobPackageTracker = tracker;
+ mCallbackHandler = new JobServiceHandler(looper);
+ mCompletedListener = completedListener;
+ mAvailable = true;
+ mVerb = VERB_FINISHED;
+ mPreferredUid = NO_PREFERRED_UID;
+ }
+
+ /**
+ * Give a job to this context for execution. Callers must first check {@link #getRunningJobLocked()}
+ * and ensure it is null to make sure this is a valid context.
+ * @param job The status of the job that we are going to run.
+ * @return True if the job is valid and is running. False if the job cannot be executed.
+ */
+ boolean executeRunnableJob(JobStatus job) {
+ synchronized (mLock) {
+ if (!mAvailable) {
+ Slog.e(TAG, "Starting new runnable but context is unavailable > Error.");
+ return false;
+ }
+
+ mPreferredUid = NO_PREFERRED_UID;
+
+ mRunningJob = job;
+ mRunningCallback = new JobCallback();
+ final boolean isDeadlineExpired =
+ job.hasDeadlineConstraint() &&
+ (job.getLatestRunTimeElapsed() < sElapsedRealtimeClock.millis());
+ Uri[] triggeredUris = null;
+ if (job.changedUris != null) {
+ triggeredUris = new Uri[job.changedUris.size()];
+ job.changedUris.toArray(triggeredUris);
+ }
+ String[] triggeredAuthorities = null;
+ if (job.changedAuthorities != null) {
+ triggeredAuthorities = new String[job.changedAuthorities.size()];
+ job.changedAuthorities.toArray(triggeredAuthorities);
+ }
+ final JobInfo ji = job.getJob();
+ mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(),
+ ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
+ isDeadlineExpired, triggeredUris, triggeredAuthorities, job.network);
+ mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis();
+
+ final long whenDeferred = job.getWhenStandbyDeferred();
+ if (whenDeferred > 0) {
+ final long deferral = mExecutionStartTimeElapsed - whenDeferred;
+ EventLog.writeEvent(EventLogTags.JOB_DEFERRED_EXECUTION, deferral);
+ if (DEBUG_STANDBY) {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("Starting job deferred for standby by ");
+ TimeUtils.formatDuration(deferral, sb);
+ sb.append(" ms : ");
+ sb.append(job.toShortString());
+ Slog.v(TAG, sb.toString());
+ }
+ }
+
+ // Once we'e begun executing a job, we by definition no longer care whether
+ // it was inflated from disk with not-yet-coherent delay/deadline bounds.
+ job.clearPersistedUtcTimes();
+
+ mVerb = VERB_BINDING;
+ scheduleOpTimeOutLocked();
+ final Intent intent = new Intent().setComponent(job.getServiceComponent());
+ boolean binding = false;
+ try {
+ binding = mContext.bindServiceAsUser(intent, this,
+ Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND
+ | Context.BIND_NOT_PERCEPTIBLE,
+ new UserHandle(job.getUserId()));
+ } catch (SecurityException e) {
+ // Some permission policy, for example INTERACT_ACROSS_USERS and
+ // android:singleUser, can result in a SecurityException being thrown from
+ // bindServiceAsUser(). If this happens, catch it and fail gracefully.
+ Slog.w(TAG, "Job service " + job.getServiceComponent().getShortClassName()
+ + " cannot be executed: " + e.getMessage());
+ binding = false;
+ }
+ if (!binding) {
+ if (DEBUG) {
+ Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable.");
+ }
+ mRunningJob = null;
+ mRunningCallback = null;
+ mParams = null;
+ mExecutionStartTimeElapsed = 0L;
+ mVerb = VERB_FINISHED;
+ removeOpTimeOutLocked();
+ return false;
+ }
+ mJobPackageTracker.noteActive(job);
+ try {
+ mBatteryStats.noteJobStart(job.getBatteryName(), job.getSourceUid(),
+ job.getStandbyBucket(), job.getJobId());
+ } catch (RemoteException e) {
+ // Whatever.
+ }
+ final String jobPackage = job.getSourcePackageName();
+ final int jobUserId = job.getSourceUserId();
+ UsageStatsManagerInternal usageStats =
+ LocalServices.getService(UsageStatsManagerInternal.class);
+ usageStats.setLastJobRunTime(jobPackage, jobUserId, mExecutionStartTimeElapsed);
+ JobSchedulerInternal jobScheduler =
+ LocalServices.getService(JobSchedulerInternal.class);
+ jobScheduler.noteJobStart(jobPackage, jobUserId);
+ mAvailable = false;
+ mStoppedReason = null;
+ mStoppedTime = 0;
+ return true;
+ }
+ }
+
+ /**
+ * Used externally to query the running job. Will return null if there is no job running.
+ */
+ JobStatus getRunningJobLocked() {
+ return mRunningJob;
+ }
+
+ /**
+ * Used only for debugging. Will return <code>"<null>"</code> if there is no job running.
+ */
+ private String getRunningJobNameLocked() {
+ return mRunningJob != null ? mRunningJob.toShortString() : "<null>";
+ }
+
+ /** Called externally when a job that was scheduled for execution should be cancelled. */
+ @GuardedBy("mLock")
+ void cancelExecutingJobLocked(int reason, String debugReason) {
+ doCancelLocked(reason, debugReason);
+ }
+
+ @GuardedBy("mLock")
+ void preemptExecutingJobLocked() {
+ doCancelLocked(JobParameters.REASON_PREEMPT, "cancelled due to preemption");
+ }
+
+ int getPreferredUid() {
+ return mPreferredUid;
+ }
+
+ void clearPreferredUid() {
+ mPreferredUid = NO_PREFERRED_UID;
+ }
+
+ long getExecutionStartTimeElapsed() {
+ return mExecutionStartTimeElapsed;
+ }
+
+ long getTimeoutElapsed() {
+ return mTimeoutElapsed;
+ }
+
+ @GuardedBy("mLock")
+ boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId,
+ String reason) {
+ final JobStatus executing = getRunningJobLocked();
+ if (executing != null && (userId == UserHandle.USER_ALL || userId == executing.getUserId())
+ && (pkgName == null || pkgName.equals(executing.getSourcePackageName()))
+ && (!matchJobId || jobId == executing.getJobId())) {
+ if (mVerb == VERB_EXECUTING) {
+ mParams.setStopReason(JobParameters.REASON_TIMEOUT, reason);
+ sendStopMessageLocked("force timeout from shell");
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void doJobFinished(JobCallback cb, int jobId, boolean reschedule) {
+ doCallback(cb, reschedule, "app called jobFinished");
+ }
+
+ void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) {
+ doCallback(cb, reschedule, null);
+ }
+
+ void doAcknowledgeStartMessage(JobCallback cb, int jobId, boolean ongoing) {
+ doCallback(cb, ongoing, "finished start");
+ }
+
+ JobWorkItem doDequeueWork(JobCallback cb, int jobId) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ assertCallerLocked(cb);
+ if (mVerb == VERB_STOPPING || mVerb == VERB_FINISHED) {
+ // This job is either all done, or on its way out. Either way, it
+ // should not dispatch any more work. We will pick up any remaining
+ // work the next time we start the job again.
+ return null;
+ }
+ final JobWorkItem work = mRunningJob.dequeueWorkLocked();
+ if (work == null && !mRunningJob.hasExecutingWorkLocked()) {
+ // This will finish the job.
+ doCallbackLocked(false, "last work dequeued");
+ }
+ return work;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ boolean doCompleteWork(JobCallback cb, int jobId, int workId) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ assertCallerLocked(cb);
+ return mRunningJob.completeWorkLocked(ActivityManager.getService(), workId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ /**
+ * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work
+ * we intend to send to the client - we stop sending work when the service is unbound so until
+ * then we keep the wakelock.
+ * @param name The concrete component name of the service that has been connected.
+ * @param service The IBinder of the Service's communication channel,
+ */
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ JobStatus runningJob;
+ synchronized (mLock) {
+ // This isn't strictly necessary b/c the JobServiceHandler is running on the main
+ // looper and at this point we can't get any binder callbacks from the client. Better
+ // safe than sorry.
+ runningJob = mRunningJob;
+
+ if (runningJob == null || !name.equals(runningJob.getServiceComponent())) {
+ closeAndCleanupJobLocked(true /* needsReschedule */,
+ "connected for different component");
+ return;
+ }
+ this.service = IJobService.Stub.asInterface(service);
+ final PowerManager pm =
+ (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ runningJob.getTag());
+ wl.setWorkSource(deriveWorkSource(runningJob));
+ wl.setReferenceCounted(false);
+ wl.acquire();
+
+ // We use a new wakelock instance per job. In rare cases there is a race between
+ // teardown following job completion/cancellation and new job service spin-up
+ // such that if we simply assign mWakeLock to be the new instance, we orphan
+ // the currently-live lock instead of cleanly replacing it. Watch for this and
+ // explicitly fast-forward the release if we're in that situation.
+ if (mWakeLock != null) {
+ Slog.w(TAG, "Bound new job " + runningJob + " but live wakelock " + mWakeLock
+ + " tag=" + mWakeLock.getTag());
+ mWakeLock.release();
+ }
+ mWakeLock = wl;
+ doServiceBoundLocked();
+ }
+ }
+
+ private WorkSource deriveWorkSource(JobStatus runningJob) {
+ final int jobUid = runningJob.getSourceUid();
+ if (WorkSource.isChainedBatteryAttributionEnabled(mContext)) {
+ WorkSource workSource = new WorkSource();
+ workSource.createWorkChain()
+ .addNode(jobUid, null)
+ .addNode(android.os.Process.SYSTEM_UID, "JobScheduler");
+ return workSource;
+ } else {
+ return new WorkSource(jobUid);
+ }
+ }
+
+ /** If the client service crashes we reschedule this job and clean up. */
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ synchronized (mLock) {
+ closeAndCleanupJobLocked(true /* needsReschedule */, "unexpectedly disconnected");
+ }
+ }
+
+ /**
+ * This class is reused across different clients, and passes itself in as a callback. Check
+ * whether the client exercising the callback is the client we expect.
+ * @return True if the binder calling is coming from the client we expect.
+ */
+ private boolean verifyCallerLocked(JobCallback cb) {
+ if (mRunningCallback != cb) {
+ if (DEBUG) {
+ Slog.d(TAG, "Stale callback received, ignoring.");
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private void assertCallerLocked(JobCallback cb) {
+ if (!verifyCallerLocked(cb)) {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("Caller no longer running");
+ if (cb.mStoppedReason != null) {
+ sb.append(", last stopped ");
+ TimeUtils.formatDuration(sElapsedRealtimeClock.millis() - cb.mStoppedTime, sb);
+ sb.append(" because: ");
+ sb.append(cb.mStoppedReason);
+ }
+ throw new SecurityException(sb.toString());
+ }
+ }
+
+ /**
+ * Scheduling of async messages (basically timeouts at this point).
+ */
+ private class JobServiceHandler extends Handler {
+ JobServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_TIMEOUT:
+ synchronized (mLock) {
+ if (message.obj == mRunningCallback) {
+ handleOpTimeoutLocked();
+ } else {
+ JobCallback jc = (JobCallback)message.obj;
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("Ignoring timeout of no longer active job");
+ if (jc.mStoppedReason != null) {
+ sb.append(", stopped ");
+ TimeUtils.formatDuration(sElapsedRealtimeClock.millis()
+ - jc.mStoppedTime, sb);
+ sb.append(" because: ");
+ sb.append(jc.mStoppedReason);
+ }
+ Slog.w(TAG, sb.toString());
+ }
+ }
+ break;
+ default:
+ Slog.e(TAG, "Unrecognised message: " + message);
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ void doServiceBoundLocked() {
+ removeOpTimeOutLocked();
+ handleServiceBoundLocked();
+ }
+
+ void doCallback(JobCallback cb, boolean reschedule, String reason) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ if (!verifyCallerLocked(cb)) {
+ return;
+ }
+ doCallbackLocked(reschedule, reason);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @GuardedBy("mLock")
+ void doCallbackLocked(boolean reschedule, String reason) {
+ if (DEBUG) {
+ Slog.d(TAG, "doCallback of : " + mRunningJob
+ + " v:" + VERB_STRINGS[mVerb]);
+ }
+ removeOpTimeOutLocked();
+
+ if (mVerb == VERB_STARTING) {
+ handleStartedLocked(reschedule);
+ } else if (mVerb == VERB_EXECUTING ||
+ mVerb == VERB_STOPPING) {
+ handleFinishedLocked(reschedule, reason);
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Unrecognised callback: " + mRunningJob);
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ void doCancelLocked(int arg1, String debugReason) {
+ if (mVerb == VERB_FINISHED) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "Trying to process cancel for torn-down context, ignoring.");
+ }
+ return;
+ }
+ mParams.setStopReason(arg1, debugReason);
+ if (arg1 == JobParameters.REASON_PREEMPT) {
+ mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
+ NO_PREFERRED_UID;
+ }
+ handleCancelLocked(debugReason);
+ }
+
+ /** Start the job on the service. */
+ @GuardedBy("mLock")
+ private void handleServiceBoundLocked() {
+ if (DEBUG) {
+ Slog.d(TAG, "handleServiceBound for " + getRunningJobNameLocked());
+ }
+ if (mVerb != VERB_BINDING) {
+ Slog.e(TAG, "Sending onStartJob for a job that isn't pending. "
+ + VERB_STRINGS[mVerb]);
+ closeAndCleanupJobLocked(false /* reschedule */, "started job not pending");
+ return;
+ }
+ if (mCancelled) {
+ if (DEBUG) {
+ Slog.d(TAG, "Job cancelled while waiting for bind to complete. "
+ + mRunningJob);
+ }
+ closeAndCleanupJobLocked(true /* reschedule */, "cancelled while waiting for bind");
+ return;
+ }
+ try {
+ mVerb = VERB_STARTING;
+ scheduleOpTimeOutLocked();
+ service.startJob(mParams);
+ } catch (Exception e) {
+ // We catch 'Exception' because client-app malice or bugs might induce a wide
+ // range of possible exception-throw outcomes from startJob() and its handling
+ // of the client's ParcelableBundle extras.
+ Slog.e(TAG, "Error sending onStart message to '" +
+ mRunningJob.getServiceComponent().getShortClassName() + "' ", e);
+ }
+ }
+
+ /**
+ * State behaviours.
+ * VERB_STARTING -> Successful start, change job to VERB_EXECUTING and post timeout.
+ * _PENDING -> Error
+ * _EXECUTING -> Error
+ * _STOPPING -> Error
+ */
+ @GuardedBy("mLock")
+ private void handleStartedLocked(boolean workOngoing) {
+ switch (mVerb) {
+ case VERB_STARTING:
+ mVerb = VERB_EXECUTING;
+ if (!workOngoing) {
+ // Job is finished already so fast-forward to handleFinished.
+ handleFinishedLocked(false, "onStartJob returned false");
+ return;
+ }
+ if (mCancelled) {
+ if (DEBUG) {
+ Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete.");
+ }
+ // Cancelled *while* waiting for acknowledgeStartMessage from client.
+ handleCancelLocked(null);
+ return;
+ }
+ scheduleOpTimeOutLocked();
+ break;
+ default:
+ Slog.e(TAG, "Handling started job but job wasn't starting! Was "
+ + VERB_STRINGS[mVerb] + ".");
+ return;
+ }
+ }
+
+ /**
+ * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done.
+ * _STOPPING -> Successful finish, clean up and notify done.
+ * _STARTING -> Error
+ * _PENDING -> Error
+ */
+ @GuardedBy("mLock")
+ private void handleFinishedLocked(boolean reschedule, String reason) {
+ switch (mVerb) {
+ case VERB_EXECUTING:
+ case VERB_STOPPING:
+ closeAndCleanupJobLocked(reschedule, reason);
+ break;
+ default:
+ Slog.e(TAG, "Got an execution complete message for a job that wasn't being" +
+ "executed. Was " + VERB_STRINGS[mVerb] + ".");
+ }
+ }
+
+ /**
+ * A job can be in various states when a cancel request comes in:
+ * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for
+ * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)}
+ * _STARTING -> Mark as cancelled and wait for
+ * {@link JobServiceContext#doAcknowledgeStartMessage}
+ * _EXECUTING -> call {@link #sendStopMessageLocked}}, but only if there are no callbacks
+ * in the message queue.
+ * _ENDING -> No point in doing anything here, so we ignore.
+ */
+ @GuardedBy("mLock")
+ private void handleCancelLocked(String reason) {
+ if (JobSchedulerService.DEBUG) {
+ Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " "
+ + VERB_STRINGS[mVerb]);
+ }
+ switch (mVerb) {
+ case VERB_BINDING:
+ case VERB_STARTING:
+ mCancelled = true;
+ applyStoppedReasonLocked(reason);
+ break;
+ case VERB_EXECUTING:
+ sendStopMessageLocked(reason);
+ break;
+ case VERB_STOPPING:
+ // Nada.
+ break;
+ default:
+ Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb);
+ break;
+ }
+ }
+
+ /** Process MSG_TIMEOUT here. */
+ @GuardedBy("mLock")
+ private void handleOpTimeoutLocked() {
+ switch (mVerb) {
+ case VERB_BINDING:
+ Slog.w(TAG, "Time-out while trying to bind " + getRunningJobNameLocked()
+ + ", dropping.");
+ closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while binding");
+ break;
+ case VERB_STARTING:
+ // Client unresponsive - wedged or failed to respond in time. We don't really
+ // know what happened so let's log it and notify the JobScheduler
+ // FINISHED/NO-RETRY.
+ Slog.w(TAG, "No response from client for onStartJob "
+ + getRunningJobNameLocked());
+ closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while starting");
+ break;
+ case VERB_STOPPING:
+ // At least we got somewhere, so fail but ask the JobScheduler to reschedule.
+ Slog.w(TAG, "No response from client for onStopJob "
+ + getRunningJobNameLocked());
+ closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping");
+ break;
+ case VERB_EXECUTING:
+ // Not an error - client ran out of time.
+ Slog.i(TAG, "Client timed out while executing (no jobFinished received), " +
+ "sending onStop: " + getRunningJobNameLocked());
+ mParams.setStopReason(JobParameters.REASON_TIMEOUT, "client timed out");
+ sendStopMessageLocked("timeout while executing");
+ break;
+ default:
+ Slog.e(TAG, "Handling timeout for an invalid job state: "
+ + getRunningJobNameLocked() + ", dropping.");
+ closeAndCleanupJobLocked(false /* needsReschedule */, "invalid timeout");
+ }
+ }
+
+ /**
+ * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING ->
+ * VERB_STOPPING.
+ */
+ @GuardedBy("mLock")
+ private void sendStopMessageLocked(String reason) {
+ removeOpTimeOutLocked();
+ if (mVerb != VERB_EXECUTING) {
+ Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob);
+ closeAndCleanupJobLocked(false /* reschedule */, reason);
+ return;
+ }
+ try {
+ applyStoppedReasonLocked(reason);
+ mVerb = VERB_STOPPING;
+ scheduleOpTimeOutLocked();
+ service.stopJob(mParams);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error sending onStopJob to client.", e);
+ // The job's host app apparently crashed during the job, so we should reschedule.
+ closeAndCleanupJobLocked(true /* reschedule */, "host crashed when trying to stop");
+ }
+ }
+
+ /**
+ * The provided job has finished, either by calling
+ * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)}
+ * or from acknowledging the stop message we sent. Either way, we're done tracking it and
+ * we want to clean up internally.
+ */
+ @GuardedBy("mLock")
+ private void closeAndCleanupJobLocked(boolean reschedule, String reason) {
+ final JobStatus completedJob;
+ if (mVerb == VERB_FINISHED) {
+ return;
+ }
+ applyStoppedReasonLocked(reason);
+ completedJob = mRunningJob;
+ mJobPackageTracker.noteInactive(completedJob, mParams.getStopReason(), reason);
+ try {
+ mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(),
+ mRunningJob.getSourceUid(), mParams.getStopReason(),
+ mRunningJob.getStandbyBucket(), mRunningJob.getJobId());
+ } catch (RemoteException e) {
+ // Whatever.
+ }
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ }
+ mContext.unbindService(JobServiceContext.this);
+ mWakeLock = null;
+ mRunningJob = null;
+ mRunningCallback = null;
+ mParams = null;
+ mVerb = VERB_FINISHED;
+ mCancelled = false;
+ service = null;
+ mAvailable = true;
+ removeOpTimeOutLocked();
+ mCompletedListener.onJobCompletedLocked(completedJob, reschedule);
+ }
+
+ private void applyStoppedReasonLocked(String reason) {
+ if (reason != null && mStoppedReason == null) {
+ mStoppedReason = reason;
+ mStoppedTime = sElapsedRealtimeClock.millis();
+ if (mRunningCallback != null) {
+ mRunningCallback.mStoppedReason = mStoppedReason;
+ mRunningCallback.mStoppedTime = mStoppedTime;
+ }
+ }
+ }
+
+ /**
+ * Called when sending a message to the client, over whose execution we have no control. If
+ * we haven't received a response in a certain amount of time, we want to give up and carry
+ * on with life.
+ */
+ private void scheduleOpTimeOutLocked() {
+ removeOpTimeOutLocked();
+
+ final long timeoutMillis;
+ switch (mVerb) {
+ case VERB_EXECUTING:
+ timeoutMillis = EXECUTING_TIMESLICE_MILLIS;
+ break;
+
+ case VERB_BINDING:
+ timeoutMillis = OP_BIND_TIMEOUT_MILLIS;
+ break;
+
+ default:
+ timeoutMillis = OP_TIMEOUT_MILLIS;
+ break;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduling time out for '" +
+ mRunningJob.getServiceComponent().getShortClassName() + "' jId: " +
+ mParams.getJobId() + ", in " + (timeoutMillis / 1000) + " s");
+ }
+ Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT, mRunningCallback);
+ mCallbackHandler.sendMessageDelayed(m, timeoutMillis);
+ mTimeoutElapsed = sElapsedRealtimeClock.millis() + timeoutMillis;
+ }
+
+
+ private void removeOpTimeOutLocked() {
+ mCallbackHandler.removeMessages(MSG_TIMEOUT);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
new file mode 100644
index 0000000..d69faf3
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -0,0 +1,1285 @@
+/*
+ * Copyright (C) 2014 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;
+
+import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+import static com.android.server.job.JobSchedulerService.sSystemClock;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.app.job.JobInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.NetworkRequest;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.format.DateUtils;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.BitUtils;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.server.IoThread;
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerInternal.JobStorePersistStats;
+import com.android.server.job.controllers.JobStatus;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Maintains the master list of jobs that the job scheduler is tracking. These jobs are compared by
+ * reference, so none of the functions in this class should make a copy.
+ * Also handles read/write of persisted jobs.
+ *
+ * Note on locking:
+ * All callers to this class must <strong>lock on the class object they are calling</strong>.
+ * This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
+ * and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
+ * object.
+ *
+ * Test:
+ * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+ */
+public final class JobStore {
+ private static final String TAG = "JobStore";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG;
+
+ /** Threshold to adjust how often we want to write to the db. */
+ private static final long JOB_PERSIST_DELAY = 2000L;
+
+ final Object mLock;
+ final Object mWriteScheduleLock; // used solely for invariants around write scheduling
+ final JobSet mJobSet; // per-caller-uid and per-source-uid tracking
+ final Context mContext;
+
+ // Bookkeeping around incorrect boot-time system clock
+ private final long mXmlTimestamp;
+ private boolean mRtcGood;
+
+ @GuardedBy("mWriteScheduleLock")
+ private boolean mWriteScheduled;
+
+ @GuardedBy("mWriteScheduleLock")
+ private boolean mWriteInProgress;
+
+ private static final Object sSingletonLock = new Object();
+ private final AtomicFile mJobsFile;
+ /** Handler backed by IoThread for writing to disk. */
+ private final Handler mIoHandler = IoThread.getHandler();
+ private static JobStore sSingleton;
+
+ private JobStorePersistStats mPersistInfo = new JobStorePersistStats();
+
+ /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
+ static JobStore initAndGet(JobSchedulerService jobManagerService) {
+ synchronized (sSingletonLock) {
+ if (sSingleton == null) {
+ sSingleton = new JobStore(jobManagerService.getContext(),
+ jobManagerService.getLock(), Environment.getDataDirectory());
+ }
+ return sSingleton;
+ }
+ }
+
+ /**
+ * @return A freshly initialized job store object, with no loaded jobs.
+ */
+ @VisibleForTesting
+ public static JobStore initAndGetForTesting(Context context, File dataDir) {
+ JobStore jobStoreUnderTest = new JobStore(context, new Object(), dataDir);
+ jobStoreUnderTest.clear();
+ return jobStoreUnderTest;
+ }
+
+ /**
+ * Construct the instance of the job store. This results in a blocking read from disk.
+ */
+ private JobStore(Context context, Object lock, File dataDir) {
+ mLock = lock;
+ mWriteScheduleLock = new Object();
+ mContext = context;
+
+ File systemDir = new File(dataDir, "system");
+ File jobDir = new File(systemDir, "job");
+ jobDir.mkdirs();
+ mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"), "jobs");
+
+ mJobSet = new JobSet();
+
+ // If the current RTC is earlier than the timestamp on our persisted jobs file,
+ // we suspect that the RTC is uninitialized and so we cannot draw conclusions
+ // about persisted job scheduling.
+ //
+ // Note that if the persisted jobs file does not exist, we proceed with the
+ // assumption that the RTC is good. This is less work and is safe: if the
+ // clock updates to sanity then we'll be saving the persisted jobs file in that
+ // correct state, which is normal; or we'll wind up writing the jobs file with
+ // an incorrect historical timestamp. That's fine; at worst we'll reboot with
+ // a *correct* timestamp, see a bunch of overdue jobs, and run them; then
+ // settle into normal operation.
+ mXmlTimestamp = mJobsFile.getLastModifiedTime();
+ mRtcGood = (sSystemClock.millis() > mXmlTimestamp);
+
+ readJobMapFromDisk(mJobSet, mRtcGood);
+ }
+
+ public boolean jobTimesInflatedValid() {
+ return mRtcGood;
+ }
+
+ public boolean clockNowValidToInflate(long now) {
+ return now >= mXmlTimestamp;
+ }
+
+ /**
+ * Find all the jobs that were affected by RTC clock uncertainty at boot time. Returns
+ * parallel lists of the existing JobStatus objects and of new, equivalent JobStatus instances
+ * with now-corrected time bounds.
+ */
+ public void getRtcCorrectedJobsLocked(final ArrayList<JobStatus> toAdd,
+ final ArrayList<JobStatus> toRemove) {
+ final long elapsedNow = sElapsedRealtimeClock.millis();
+ final IActivityManager am = ActivityManager.getService();
+
+ // Find the jobs that need to be fixed up, collecting them for post-iteration
+ // replacement with their new versions
+ forEachJob(job -> {
+ final Pair<Long, Long> utcTimes = job.getPersistedUtcTimes();
+ if (utcTimes != null) {
+ Pair<Long, Long> elapsedRuntimes =
+ convertRtcBoundsToElapsed(utcTimes, elapsedNow);
+ JobStatus newJob = new JobStatus(job, job.getBaseHeartbeat(),
+ elapsedRuntimes.first, elapsedRuntimes.second,
+ 0, job.getLastSuccessfulRunTime(), job.getLastFailedRunTime());
+ newJob.prepareLocked(am);
+ toAdd.add(newJob);
+ toRemove.add(job);
+ }
+ });
+ }
+
+ /**
+ * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
+ * it will be replaced.
+ * @param jobStatus Job to add.
+ * @return Whether or not an equivalent JobStatus was replaced by this operation.
+ */
+ public boolean add(JobStatus jobStatus) {
+ boolean replaced = mJobSet.remove(jobStatus);
+ mJobSet.add(jobStatus);
+ if (jobStatus.isPersisted()) {
+ maybeWriteStatusToDiskAsync();
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "Added job status to store: " + jobStatus);
+ }
+ return replaced;
+ }
+
+ boolean containsJob(JobStatus jobStatus) {
+ return mJobSet.contains(jobStatus);
+ }
+
+ public int size() {
+ return mJobSet.size();
+ }
+
+ public JobStorePersistStats getPersistStats() {
+ return mPersistInfo;
+ }
+
+ public int countJobsForUid(int uid) {
+ return mJobSet.countJobsForUid(uid);
+ }
+
+ /**
+ * Remove the provided job. Will also delete the job if it was persisted.
+ * @param removeFromPersisted If true, the job will be removed from the persisted job list
+ * immediately (if it was persisted).
+ * @return Whether or not the job existed to be removed.
+ */
+ public boolean remove(JobStatus jobStatus, boolean removeFromPersisted) {
+ boolean removed = mJobSet.remove(jobStatus);
+ if (!removed) {
+ if (DEBUG) {
+ Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
+ }
+ return false;
+ }
+ if (removeFromPersisted && jobStatus.isPersisted()) {
+ maybeWriteStatusToDiskAsync();
+ }
+ return removed;
+ }
+
+ /**
+ * Remove the jobs of users not specified in the whitelist.
+ * @param whitelist Array of User IDs whose jobs are not to be removed.
+ */
+ public void removeJobsOfNonUsers(int[] whitelist) {
+ mJobSet.removeJobsOfNonUsers(whitelist);
+ }
+
+ @VisibleForTesting
+ public void clear() {
+ mJobSet.clear();
+ maybeWriteStatusToDiskAsync();
+ }
+
+ /**
+ * @param userHandle User for whom we are querying the list of jobs.
+ * @return A list of all the jobs scheduled for the provided user. Never null.
+ */
+ public List<JobStatus> getJobsByUser(int userHandle) {
+ return mJobSet.getJobsByUser(userHandle);
+ }
+
+ /**
+ * @param uid Uid of the requesting app.
+ * @return All JobStatus objects for a given uid from the master list. Never null.
+ */
+ public List<JobStatus> getJobsByUid(int uid) {
+ return mJobSet.getJobsByUid(uid);
+ }
+
+ /**
+ * @param uid Uid of the requesting app.
+ * @param jobId Job id, specified at schedule-time.
+ * @return the JobStatus that matches the provided uId and jobId, or null if none found.
+ */
+ public JobStatus getJobByUidAndJobId(int uid, int jobId) {
+ return mJobSet.get(uid, jobId);
+ }
+
+ /**
+ * Iterate over the set of all jobs, invoking the supplied functor on each. This is for
+ * customers who need to examine each job; we'd much rather not have to generate
+ * transient unified collections for them to iterate over and then discard, or creating
+ * iterators every time a client needs to perform a sweep.
+ */
+ public void forEachJob(Consumer<JobStatus> functor) {
+ mJobSet.forEachJob(null, functor);
+ }
+
+ public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate,
+ Consumer<JobStatus> functor) {
+ mJobSet.forEachJob(filterPredicate, functor);
+ }
+
+ public void forEachJob(int uid, Consumer<JobStatus> functor) {
+ mJobSet.forEachJob(uid, functor);
+ }
+
+ public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) {
+ mJobSet.forEachJobForSourceUid(sourceUid, functor);
+ }
+
+ /** Version of the db schema. */
+ private static final int JOBS_FILE_VERSION = 0;
+ /** Tag corresponds to constraints this job needs. */
+ private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
+ /** Tag corresponds to execution parameters. */
+ private static final String XML_TAG_PERIODIC = "periodic";
+ private static final String XML_TAG_ONEOFF = "one-off";
+ private static final String XML_TAG_EXTRAS = "extras";
+
+ /**
+ * Every time the state changes we write all the jobs in one swath, instead of trying to
+ * track incremental changes.
+ */
+ private void maybeWriteStatusToDiskAsync() {
+ synchronized (mWriteScheduleLock) {
+ if (!mWriteScheduled) {
+ if (DEBUG) {
+ Slog.v(TAG, "Scheduling persist of jobs to disk.");
+ }
+ mIoHandler.postDelayed(mWriteRunnable, JOB_PERSIST_DELAY);
+ mWriteScheduled = mWriteInProgress = true;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void readJobMapFromDisk(JobSet jobSet, boolean rtcGood) {
+ new ReadJobMapFromDiskRunnable(jobSet, rtcGood).run();
+ }
+
+ /** Write persisted JobStore state to disk synchronously. Should only be used for testing. */
+ @VisibleForTesting
+ public void writeStatusToDiskForTesting() {
+ synchronized (mWriteScheduleLock) {
+ if (mWriteScheduled) {
+ throw new IllegalStateException("An asynchronous write is already scheduled.");
+ }
+
+ mWriteScheduled = mWriteInProgress = true;
+ mWriteRunnable.run();
+ }
+ }
+
+ /**
+ * Wait for any pending write to the persistent store to clear
+ * @param maxWaitMillis Maximum time from present to wait
+ * @return {@code true} if I/O cleared as expected, {@code false} if the wait
+ * timed out before the pending write completed.
+ */
+ @VisibleForTesting
+ public boolean waitForWriteToCompleteForTesting(long maxWaitMillis) {
+ final long start = SystemClock.uptimeMillis();
+ final long end = start + maxWaitMillis;
+ synchronized (mWriteScheduleLock) {
+ while (mWriteInProgress) {
+ final long now = SystemClock.uptimeMillis();
+ if (now >= end) {
+ // still not done and we've hit the end; failure
+ return false;
+ }
+ try {
+ mWriteScheduleLock.wait(now - start + maxWaitMillis);
+ } catch (InterruptedException e) {
+ // Spurious; keep waiting
+ break;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Runnable that writes {@link #mJobSet} out to xml.
+ * NOTE: This Runnable locks on mLock
+ */
+ private final Runnable mWriteRunnable = new Runnable() {
+ @Override
+ public void run() {
+ final long startElapsed = sElapsedRealtimeClock.millis();
+ final List<JobStatus> storeCopy = new ArrayList<JobStatus>();
+ // Intentionally allow new scheduling of a write operation *before* we clone
+ // the job set. If we reset it to false after cloning, there's a window in
+ // which no new write will be scheduled but mLock is not held, i.e. a new
+ // job might appear and fail to be recognized as needing a persist. The
+ // potential cost is one redundant write of an identical set of jobs in the
+ // rare case of that specific race, but by doing it this way we avoid quite
+ // a bit of lock contention.
+ synchronized (mWriteScheduleLock) {
+ mWriteScheduled = false;
+ }
+ synchronized (mLock) {
+ // Clone the jobs so we can release the lock before writing.
+ mJobSet.forEachJob(null, (job) -> {
+ if (job.isPersisted()) {
+ storeCopy.add(new JobStatus(job));
+ }
+ });
+ }
+ writeJobsMapImpl(storeCopy);
+ if (DEBUG) {
+ Slog.v(TAG, "Finished writing, took " + (sElapsedRealtimeClock.millis()
+ - startElapsed) + "ms");
+ }
+ synchronized (mWriteScheduleLock) {
+ mWriteInProgress = false;
+ mWriteScheduleLock.notifyAll();
+ }
+ }
+
+ private void writeJobsMapImpl(List<JobStatus> jobList) {
+ int numJobs = 0;
+ int numSystemJobs = 0;
+ int numSyncJobs = 0;
+ try {
+ final long startTime = SystemClock.uptimeMillis();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(baos, StandardCharsets.UTF_8.name());
+ out.startDocument(null, true);
+ out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+
+ out.startTag(null, "job-info");
+ out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
+ for (int i=0; i<jobList.size(); i++) {
+ JobStatus jobStatus = jobList.get(i);
+ if (DEBUG) {
+ Slog.d(TAG, "Saving job " + jobStatus.getJobId());
+ }
+ out.startTag(null, "job");
+ addAttributesToJobTag(out, jobStatus);
+ writeConstraintsToXml(out, jobStatus);
+ writeExecutionCriteriaToXml(out, jobStatus);
+ writeBundleToXml(jobStatus.getJob().getExtras(), out);
+ out.endTag(null, "job");
+
+ numJobs++;
+ if (jobStatus.getUid() == Process.SYSTEM_UID) {
+ numSystemJobs++;
+ if (isSyncJob(jobStatus)) {
+ numSyncJobs++;
+ }
+ }
+ }
+ out.endTag(null, "job-info");
+ out.endDocument();
+
+ // Write out to disk in one fell swoop.
+ FileOutputStream fos = mJobsFile.startWrite(startTime);
+ fos.write(baos.toByteArray());
+ mJobsFile.finishWrite(fos);
+ } catch (IOException e) {
+ if (DEBUG) {
+ Slog.v(TAG, "Error writing out job data.", e);
+ }
+ } catch (XmlPullParserException e) {
+ if (DEBUG) {
+ Slog.d(TAG, "Error persisting bundle.", e);
+ }
+ } finally {
+ mPersistInfo.countAllJobsSaved = numJobs;
+ mPersistInfo.countSystemServerJobsSaved = numSystemJobs;
+ mPersistInfo.countSystemSyncManagerJobsSaved = numSyncJobs;
+ }
+ }
+
+ /** Write out a tag with data comprising the required fields and priority of this job and
+ * its client.
+ */
+ private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
+ throws IOException {
+ out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
+ out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
+ out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
+ if (jobStatus.getSourcePackageName() != null) {
+ out.attribute(null, "sourcePackageName", jobStatus.getSourcePackageName());
+ }
+ if (jobStatus.getSourceTag() != null) {
+ out.attribute(null, "sourceTag", jobStatus.getSourceTag());
+ }
+ out.attribute(null, "sourceUserId", String.valueOf(jobStatus.getSourceUserId()));
+ out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
+ out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
+ out.attribute(null, "flags", String.valueOf(jobStatus.getFlags()));
+ if (jobStatus.getInternalFlags() != 0) {
+ out.attribute(null, "internalFlags", String.valueOf(jobStatus.getInternalFlags()));
+ }
+
+ out.attribute(null, "lastSuccessfulRunTime",
+ String.valueOf(jobStatus.getLastSuccessfulRunTime()));
+ out.attribute(null, "lastFailedRunTime",
+ String.valueOf(jobStatus.getLastFailedRunTime()));
+ }
+
+ private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
+ throws IOException, XmlPullParserException {
+ out.startTag(null, XML_TAG_EXTRAS);
+ PersistableBundle extrasCopy = deepCopyBundle(extras, 10);
+ extrasCopy.saveToXml(out);
+ out.endTag(null, XML_TAG_EXTRAS);
+ }
+
+ private PersistableBundle deepCopyBundle(PersistableBundle bundle, int maxDepth) {
+ if (maxDepth <= 0) {
+ return null;
+ }
+ PersistableBundle copy = (PersistableBundle) bundle.clone();
+ Set<String> keySet = bundle.keySet();
+ for (String key: keySet) {
+ Object o = copy.get(key);
+ if (o instanceof PersistableBundle) {
+ PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1);
+ copy.putPersistableBundle(key, bCopy);
+ }
+ }
+ return copy;
+ }
+
+ /**
+ * Write out a tag with data identifying this job's constraints. If the constraint isn't here
+ * it doesn't apply.
+ */
+ private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
+ out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
+ if (jobStatus.hasConnectivityConstraint()) {
+ final NetworkRequest network = jobStatus.getJob().getRequiredNetwork();
+ out.attribute(null, "net-capabilities", Long.toString(
+ BitUtils.packBits(network.networkCapabilities.getCapabilities())));
+ out.attribute(null, "net-unwanted-capabilities", Long.toString(
+ BitUtils.packBits(network.networkCapabilities.getUnwantedCapabilities())));
+
+ out.attribute(null, "net-transport-types", Long.toString(
+ BitUtils.packBits(network.networkCapabilities.getTransportTypes())));
+ }
+ if (jobStatus.hasIdleConstraint()) {
+ out.attribute(null, "idle", Boolean.toString(true));
+ }
+ if (jobStatus.hasChargingConstraint()) {
+ out.attribute(null, "charging", Boolean.toString(true));
+ }
+ if (jobStatus.hasBatteryNotLowConstraint()) {
+ out.attribute(null, "battery-not-low", Boolean.toString(true));
+ }
+ if (jobStatus.hasStorageNotLowConstraint()) {
+ out.attribute(null, "storage-not-low", Boolean.toString(true));
+ }
+ out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
+ }
+
+ private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
+ throws IOException {
+ final JobInfo job = jobStatus.getJob();
+ if (jobStatus.getJob().isPeriodic()) {
+ out.startTag(null, XML_TAG_PERIODIC);
+ out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
+ out.attribute(null, "flex", Long.toString(job.getFlexMillis()));
+ } else {
+ out.startTag(null, XML_TAG_ONEOFF);
+ }
+
+ // If we still have the persisted times, we need to record those directly because
+ // we haven't yet been able to calculate the usual elapsed-timebase bounds
+ // correctly due to wall-clock uncertainty.
+ Pair <Long, Long> utcJobTimes = jobStatus.getPersistedUtcTimes();
+ if (DEBUG && utcJobTimes != null) {
+ Slog.i(TAG, "storing original UTC timestamps for " + jobStatus);
+ }
+
+ final long nowRTC = sSystemClock.millis();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ if (jobStatus.hasDeadlineConstraint()) {
+ // Wall clock deadline.
+ final long deadlineWallclock = (utcJobTimes == null)
+ ? nowRTC + (jobStatus.getLatestRunTimeElapsed() - nowElapsed)
+ : utcJobTimes.second;
+ out.attribute(null, "deadline", Long.toString(deadlineWallclock));
+ }
+ if (jobStatus.hasTimingDelayConstraint()) {
+ final long delayWallclock = (utcJobTimes == null)
+ ? nowRTC + (jobStatus.getEarliestRunTime() - nowElapsed)
+ : utcJobTimes.first;
+ out.attribute(null, "delay", Long.toString(delayWallclock));
+ }
+
+ // Only write out back-off policy if it differs from the default.
+ // This also helps the case where the job is idle -> these aren't allowed to specify
+ // back-off.
+ if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
+ || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
+ out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
+ out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
+ }
+ if (job.isPeriodic()) {
+ out.endTag(null, XML_TAG_PERIODIC);
+ } else {
+ out.endTag(null, XML_TAG_ONEOFF);
+ }
+ }
+ };
+
+ /**
+ * Translate the supplied RTC times to the elapsed timebase, with clamping appropriate
+ * to interpreting them as a job's delay + deadline times for alarm-setting purposes.
+ * @param rtcTimes a Pair<Long, Long> in which {@code first} is the "delay" earliest
+ * allowable runtime for the job, and {@code second} is the "deadline" time at which
+ * the job becomes overdue.
+ */
+ private static Pair<Long, Long> convertRtcBoundsToElapsed(Pair<Long, Long> rtcTimes,
+ long nowElapsed) {
+ final long nowWallclock = sSystemClock.millis();
+ final long earliest = (rtcTimes.first > JobStatus.NO_EARLIEST_RUNTIME)
+ ? nowElapsed + Math.max(rtcTimes.first - nowWallclock, 0)
+ : JobStatus.NO_EARLIEST_RUNTIME;
+ final long latest = (rtcTimes.second < JobStatus.NO_LATEST_RUNTIME)
+ ? nowElapsed + Math.max(rtcTimes.second - nowWallclock, 0)
+ : JobStatus.NO_LATEST_RUNTIME;
+ return Pair.create(earliest, latest);
+ }
+
+ private static boolean isSyncJob(JobStatus status) {
+ return com.android.server.content.SyncJobService.class.getName()
+ .equals(status.getServiceComponent().getClassName());
+ }
+
+ /**
+ * Runnable that reads list of persisted job from xml. This is run once at start up, so doesn't
+ * need to go through {@link JobStore#add(com.android.server.job.controllers.JobStatus)}.
+ */
+ private final class ReadJobMapFromDiskRunnable implements Runnable {
+ private final JobSet jobSet;
+ private final boolean rtcGood;
+
+ /**
+ * @param jobSet Reference to the (empty) set of JobStatus objects that back the JobStore,
+ * so that after disk read we can populate it directly.
+ */
+ ReadJobMapFromDiskRunnable(JobSet jobSet, boolean rtcIsGood) {
+ this.jobSet = jobSet;
+ this.rtcGood = rtcIsGood;
+ }
+
+ @Override
+ public void run() {
+ int numJobs = 0;
+ int numSystemJobs = 0;
+ int numSyncJobs = 0;
+ try {
+ List<JobStatus> jobs;
+ FileInputStream fis = mJobsFile.openRead();
+ synchronized (mLock) {
+ jobs = readJobMapImpl(fis, rtcGood);
+ if (jobs != null) {
+ long now = sElapsedRealtimeClock.millis();
+ IActivityManager am = ActivityManager.getService();
+ for (int i=0; i<jobs.size(); i++) {
+ JobStatus js = jobs.get(i);
+ js.prepareLocked(am);
+ js.enqueueTime = now;
+ this.jobSet.add(js);
+
+ numJobs++;
+ if (js.getUid() == Process.SYSTEM_UID) {
+ numSystemJobs++;
+ if (isSyncJob(js)) {
+ numSyncJobs++;
+ }
+ }
+ }
+ }
+ }
+ fis.close();
+ } catch (FileNotFoundException e) {
+ if (DEBUG) {
+ Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
+ }
+ } catch (XmlPullParserException | IOException e) {
+ Slog.wtf(TAG, "Error jobstore xml.", e);
+ } finally {
+ if (mPersistInfo.countAllJobsLoaded < 0) { // Only set them once.
+ mPersistInfo.countAllJobsLoaded = numJobs;
+ mPersistInfo.countSystemServerJobsLoaded = numSystemJobs;
+ mPersistInfo.countSystemSyncManagerJobsLoaded = numSyncJobs;
+ }
+ }
+ Slog.i(TAG, "Read " + numJobs + " jobs");
+ }
+
+ private List<JobStatus> readJobMapImpl(FileInputStream fis, boolean rtcIsGood)
+ throws XmlPullParserException, IOException {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(fis, StandardCharsets.UTF_8.name());
+
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.START_TAG &&
+ eventType != XmlPullParser.END_DOCUMENT) {
+ eventType = parser.next();
+ Slog.d(TAG, "Start tag: " + parser.getName());
+ }
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ if (DEBUG) {
+ Slog.d(TAG, "No persisted jobs.");
+ }
+ return null;
+ }
+
+ String tagName = parser.getName();
+ if ("job-info".equals(tagName)) {
+ final List<JobStatus> jobs = new ArrayList<JobStatus>();
+ // Read in version info.
+ try {
+ int version = Integer.parseInt(parser.getAttributeValue(null, "version"));
+ if (version != JOBS_FILE_VERSION) {
+ Slog.d(TAG, "Invalid version number, aborting jobs file read.");
+ return null;
+ }
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Invalid version number, aborting jobs file read.");
+ return null;
+ }
+ eventType = parser.next();
+ do {
+ // Read each <job/>
+ if (eventType == XmlPullParser.START_TAG) {
+ tagName = parser.getName();
+ // Start reading job.
+ if ("job".equals(tagName)) {
+ JobStatus persistedJob = restoreJobFromXml(rtcIsGood, parser);
+ if (persistedJob != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "Read out " + persistedJob);
+ }
+ jobs.add(persistedJob);
+ } else {
+ Slog.d(TAG, "Error reading job from file.");
+ }
+ }
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+ return jobs;
+ }
+ return null;
+ }
+
+ /**
+ * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
+ * will take the parser into the body of the job tag.
+ * @return Newly instantiated job holding all the information we just read out of the xml tag.
+ */
+ private JobStatus restoreJobFromXml(boolean rtcIsGood, XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ JobInfo.Builder jobBuilder;
+ int uid, sourceUserId;
+ long lastSuccessfulRunTime;
+ long lastFailedRunTime;
+ int internalFlags = 0;
+
+ // Read out job identifier attributes and priority.
+ try {
+ jobBuilder = buildBuilderFromXml(parser);
+ jobBuilder.setPersisted(true);
+ uid = Integer.parseInt(parser.getAttributeValue(null, "uid"));
+
+ String val = parser.getAttributeValue(null, "priority");
+ if (val != null) {
+ jobBuilder.setPriority(Integer.parseInt(val));
+ }
+ val = parser.getAttributeValue(null, "flags");
+ if (val != null) {
+ jobBuilder.setFlags(Integer.parseInt(val));
+ }
+ val = parser.getAttributeValue(null, "internalFlags");
+ if (val != null) {
+ internalFlags = Integer.parseInt(val);
+ }
+ val = parser.getAttributeValue(null, "sourceUserId");
+ sourceUserId = val == null ? -1 : Integer.parseInt(val);
+
+ val = parser.getAttributeValue(null, "lastSuccessfulRunTime");
+ lastSuccessfulRunTime = val == null ? 0 : Long.parseLong(val);
+
+ val = parser.getAttributeValue(null, "lastFailedRunTime");
+ lastFailedRunTime = val == null ? 0 : Long.parseLong(val);
+ } catch (NumberFormatException e) {
+ Slog.e(TAG, "Error parsing job's required fields, skipping");
+ return null;
+ }
+
+ String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
+ final String sourceTag = parser.getAttributeValue(null, "sourceTag");
+
+ int eventType;
+ // Read out constraints tag.
+ do {
+ eventType = parser.next();
+ } while (eventType == XmlPullParser.TEXT); // Push through to next START_TAG.
+
+ if (!(eventType == XmlPullParser.START_TAG &&
+ XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
+ // Expecting a <constraints> start tag.
+ return null;
+ }
+ try {
+ buildConstraintsFromXml(jobBuilder, parser);
+ } catch (NumberFormatException e) {
+ Slog.d(TAG, "Error reading constraints, skipping.");
+ return null;
+ }
+ parser.next(); // Consume </constraints>
+
+ // Read out execution parameters tag.
+ do {
+ eventType = parser.next();
+ } while (eventType == XmlPullParser.TEXT);
+ if (eventType != XmlPullParser.START_TAG) {
+ return null;
+ }
+
+ // Tuple of (earliest runtime, latest runtime) in UTC.
+ final Pair<Long, Long> rtcRuntimes;
+ try {
+ rtcRuntimes = buildRtcExecutionTimesFromXml(parser);
+ } catch (NumberFormatException e) {
+ if (DEBUG) {
+ Slog.d(TAG, "Error parsing execution time parameters, skipping.");
+ }
+ return null;
+ }
+
+ final long elapsedNow = sElapsedRealtimeClock.millis();
+ Pair<Long, Long> elapsedRuntimes = convertRtcBoundsToElapsed(rtcRuntimes, elapsedNow);
+
+ if (XML_TAG_PERIODIC.equals(parser.getName())) {
+ try {
+ String val = parser.getAttributeValue(null, "period");
+ final long periodMillis = Long.parseLong(val);
+ val = parser.getAttributeValue(null, "flex");
+ final long flexMillis = (val != null) ? Long.valueOf(val) : periodMillis;
+ jobBuilder.setPeriodic(periodMillis, flexMillis);
+ // As a sanity check, cap the recreated run time to be no later than flex+period
+ // from now. This is the latest the periodic could be pushed out. This could
+ // happen if the periodic ran early (at flex time before period), and then the
+ // device rebooted.
+ if (elapsedRuntimes.second > elapsedNow + periodMillis + flexMillis) {
+ final long clampedLateRuntimeElapsed = elapsedNow + flexMillis
+ + periodMillis;
+ final long clampedEarlyRuntimeElapsed = clampedLateRuntimeElapsed
+ - flexMillis;
+ Slog.w(TAG,
+ String.format("Periodic job for uid='%d' persisted run-time is" +
+ " too big [%s, %s]. Clamping to [%s,%s]",
+ uid,
+ DateUtils.formatElapsedTime(elapsedRuntimes.first / 1000),
+ DateUtils.formatElapsedTime(elapsedRuntimes.second / 1000),
+ DateUtils.formatElapsedTime(
+ clampedEarlyRuntimeElapsed / 1000),
+ DateUtils.formatElapsedTime(
+ clampedLateRuntimeElapsed / 1000))
+ );
+ elapsedRuntimes =
+ Pair.create(clampedEarlyRuntimeElapsed, clampedLateRuntimeElapsed);
+ }
+ } catch (NumberFormatException e) {
+ Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
+ return null;
+ }
+ } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
+ try {
+ if (elapsedRuntimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
+ jobBuilder.setMinimumLatency(elapsedRuntimes.first - elapsedNow);
+ }
+ if (elapsedRuntimes.second != JobStatus.NO_LATEST_RUNTIME) {
+ jobBuilder.setOverrideDeadline(
+ elapsedRuntimes.second - elapsedNow);
+ }
+ } catch (NumberFormatException e) {
+ Slog.d(TAG, "Error reading job execution criteria, skipping.");
+ return null;
+ }
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
+ }
+ // Expecting a parameters start tag.
+ return null;
+ }
+ maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
+
+ parser.nextTag(); // Consume parameters end tag.
+
+ // Read out extras Bundle.
+ do {
+ eventType = parser.next();
+ } while (eventType == XmlPullParser.TEXT);
+ if (!(eventType == XmlPullParser.START_TAG
+ && XML_TAG_EXTRAS.equals(parser.getName()))) {
+ if (DEBUG) {
+ Slog.d(TAG, "Error reading extras, skipping.");
+ }
+ return null;
+ }
+
+ PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
+ jobBuilder.setExtras(extras);
+ parser.nextTag(); // Consume </extras>
+
+ final JobInfo builtJob;
+ try {
+ builtJob = jobBuilder.build();
+ } catch (Exception e) {
+ Slog.w(TAG, "Unable to build job from XML, ignoring: "
+ + jobBuilder.summarize());
+ return null;
+ }
+
+ // Migrate sync jobs forward from earlier, incomplete representation
+ if ("android".equals(sourcePackageName)
+ && extras != null
+ && extras.getBoolean("SyncManagerJob", false)) {
+ sourcePackageName = extras.getString("owningPackage", sourcePackageName);
+ if (DEBUG) {
+ Slog.i(TAG, "Fixing up sync job source package name from 'android' to '"
+ + sourcePackageName + "'");
+ }
+ }
+
+ // And now we're done
+ JobSchedulerInternal service = LocalServices.getService(JobSchedulerInternal.class);
+ final int appBucket = JobSchedulerService.standbyBucketForPackage(sourcePackageName,
+ sourceUserId, elapsedNow);
+ long currentHeartbeat = service != null ? service.currentHeartbeat() : 0;
+ JobStatus js = new JobStatus(
+ jobBuilder.build(), uid, sourcePackageName, sourceUserId,
+ appBucket, currentHeartbeat, sourceTag,
+ elapsedRuntimes.first, elapsedRuntimes.second,
+ lastSuccessfulRunTime, lastFailedRunTime,
+ (rtcIsGood) ? null : rtcRuntimes, internalFlags);
+ return js;
+ }
+
+ private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
+ // Pull out required fields from <job> attributes.
+ int jobId = Integer.parseInt(parser.getAttributeValue(null, "jobid"));
+ String packageName = parser.getAttributeValue(null, "package");
+ String className = parser.getAttributeValue(null, "class");
+ ComponentName cname = new ComponentName(packageName, className);
+
+ return new JobInfo.Builder(jobId, cname);
+ }
+
+ private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
+ String val;
+
+ final String netCapabilities = parser.getAttributeValue(null, "net-capabilities");
+ final String netUnwantedCapabilities = parser.getAttributeValue(
+ null, "net-unwanted-capabilities");
+ final String netTransportTypes = parser.getAttributeValue(null, "net-transport-types");
+ if (netCapabilities != null && netTransportTypes != null) {
+ final NetworkRequest request = new NetworkRequest.Builder().build();
+ final long unwantedCapabilities = netUnwantedCapabilities != null
+ ? Long.parseLong(netUnwantedCapabilities)
+ : BitUtils.packBits(request.networkCapabilities.getUnwantedCapabilities());
+
+ // We're okay throwing NFE here; caught by caller
+ request.networkCapabilities.setCapabilities(
+ BitUtils.unpackBits(Long.parseLong(netCapabilities)),
+ BitUtils.unpackBits(unwantedCapabilities));
+ request.networkCapabilities.setTransportTypes(
+ BitUtils.unpackBits(Long.parseLong(netTransportTypes)));
+ jobBuilder.setRequiredNetwork(request);
+ } else {
+ // Read legacy values
+ val = parser.getAttributeValue(null, "connectivity");
+ if (val != null) {
+ jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
+ }
+ val = parser.getAttributeValue(null, "metered");
+ if (val != null) {
+ jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_METERED);
+ }
+ val = parser.getAttributeValue(null, "unmetered");
+ if (val != null) {
+ jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
+ }
+ val = parser.getAttributeValue(null, "not-roaming");
+ if (val != null) {
+ jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING);
+ }
+ }
+
+ val = parser.getAttributeValue(null, "idle");
+ if (val != null) {
+ jobBuilder.setRequiresDeviceIdle(true);
+ }
+ val = parser.getAttributeValue(null, "charging");
+ if (val != null) {
+ jobBuilder.setRequiresCharging(true);
+ }
+ val = parser.getAttributeValue(null, "battery-not-low");
+ if (val != null) {
+ jobBuilder.setRequiresBatteryNotLow(true);
+ }
+ val = parser.getAttributeValue(null, "storage-not-low");
+ if (val != null) {
+ jobBuilder.setRequiresStorageNotLow(true);
+ }
+ }
+
+ /**
+ * Builds the back-off policy out of the params tag. These attributes may not exist, depending
+ * on whether the back-off was set when the job was first scheduled.
+ */
+ private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
+ String val = parser.getAttributeValue(null, "initial-backoff");
+ if (val != null) {
+ long initialBackoff = Long.parseLong(val);
+ val = parser.getAttributeValue(null, "backoff-policy");
+ int backoffPolicy = Integer.parseInt(val); // Will throw NFE which we catch higher up.
+ jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
+ }
+ }
+
+ /**
+ * Extract a job's earliest/latest run time data from XML. These are returned in
+ * unadjusted UTC wall clock time, because we do not yet know whether the system
+ * clock is reliable for purposes of calculating deltas from 'now'.
+ *
+ * @param parser
+ * @return A Pair of timestamps in UTC wall-clock time. The first is the earliest
+ * time at which the job is to become runnable, and the second is the deadline at
+ * which it becomes overdue to execute.
+ * @throws NumberFormatException
+ */
+ private Pair<Long, Long> buildRtcExecutionTimesFromXml(XmlPullParser parser)
+ throws NumberFormatException {
+ String val;
+ // Pull out execution time data.
+ val = parser.getAttributeValue(null, "delay");
+ final long earliestRunTimeRtc = (val != null)
+ ? Long.parseLong(val)
+ : JobStatus.NO_EARLIEST_RUNTIME;
+ val = parser.getAttributeValue(null, "deadline");
+ final long latestRunTimeRtc = (val != null)
+ ? Long.parseLong(val)
+ : JobStatus.NO_LATEST_RUNTIME;
+ return Pair.create(earliestRunTimeRtc, latestRunTimeRtc);
+ }
+ }
+
+ /** Set of all tracked jobs. */
+ @VisibleForTesting
+ public static final class JobSet {
+ @VisibleForTesting // Key is the getUid() originator of the jobs in each sheaf
+ final SparseArray<ArraySet<JobStatus>> mJobs;
+
+ @VisibleForTesting // Same data but with the key as getSourceUid() of the jobs in each sheaf
+ final SparseArray<ArraySet<JobStatus>> mJobsPerSourceUid;
+
+ public JobSet() {
+ mJobs = new SparseArray<ArraySet<JobStatus>>();
+ mJobsPerSourceUid = new SparseArray<>();
+ }
+
+ public List<JobStatus> getJobsByUid(int uid) {
+ ArrayList<JobStatus> matchingJobs = new ArrayList<JobStatus>();
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ if (jobs != null) {
+ matchingJobs.addAll(jobs);
+ }
+ return matchingJobs;
+ }
+
+ // By user, not by uid, so we need to traverse by key and check
+ public List<JobStatus> getJobsByUser(int userId) {
+ final ArrayList<JobStatus> result = new ArrayList<JobStatus>();
+ for (int i = mJobsPerSourceUid.size() - 1; i >= 0; i--) {
+ if (UserHandle.getUserId(mJobsPerSourceUid.keyAt(i)) == userId) {
+ final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(i);
+ if (jobs != null) {
+ result.addAll(jobs);
+ }
+ }
+ }
+ return result;
+ }
+
+ public boolean add(JobStatus job) {
+ final int uid = job.getUid();
+ final int sourceUid = job.getSourceUid();
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ if (jobs == null) {
+ jobs = new ArraySet<JobStatus>();
+ mJobs.put(uid, jobs);
+ }
+ ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid);
+ if (jobsForSourceUid == null) {
+ jobsForSourceUid = new ArraySet<>();
+ mJobsPerSourceUid.put(sourceUid, jobsForSourceUid);
+ }
+ final boolean added = jobs.add(job);
+ final boolean addedInSource = jobsForSourceUid.add(job);
+ if (added != addedInSource) {
+ Slog.wtf(TAG, "mJobs and mJobsPerSourceUid mismatch; caller= " + added
+ + " source= " + addedInSource);
+ }
+ return added || addedInSource;
+ }
+
+ public boolean remove(JobStatus job) {
+ final int uid = job.getUid();
+ final ArraySet<JobStatus> jobs = mJobs.get(uid);
+ final int sourceUid = job.getSourceUid();
+ final ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid);
+ final boolean didRemove = jobs != null && jobs.remove(job);
+ final boolean sourceRemove = jobsForSourceUid != null && jobsForSourceUid.remove(job);
+ if (didRemove != sourceRemove) {
+ Slog.wtf(TAG, "Job presence mismatch; caller=" + didRemove
+ + " source=" + sourceRemove);
+ }
+ if (didRemove || sourceRemove) {
+ // no more jobs for this uid? let the now-empty set objects be GC'd.
+ if (jobs != null && jobs.size() == 0) {
+ mJobs.remove(uid);
+ }
+ if (jobsForSourceUid != null && jobsForSourceUid.size() == 0) {
+ mJobsPerSourceUid.remove(sourceUid);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Removes the jobs of all users not specified by the whitelist of user ids.
+ * This will remove jobs scheduled *by* non-existent users as well as jobs scheduled *for*
+ * non-existent users
+ */
+ public void removeJobsOfNonUsers(final int[] whitelist) {
+ final Predicate<JobStatus> noSourceUser =
+ job -> !ArrayUtils.contains(whitelist, job.getSourceUserId());
+ final Predicate<JobStatus> noCallingUser =
+ job -> !ArrayUtils.contains(whitelist, job.getUserId());
+ removeAll(noSourceUser.or(noCallingUser));
+ }
+
+ private void removeAll(Predicate<JobStatus> predicate) {
+ for (int jobSetIndex = mJobs.size() - 1; jobSetIndex >= 0; jobSetIndex--) {
+ final ArraySet<JobStatus> jobs = mJobs.valueAt(jobSetIndex);
+ for (int jobIndex = jobs.size() - 1; jobIndex >= 0; jobIndex--) {
+ if (predicate.test(jobs.valueAt(jobIndex))) {
+ jobs.removeAt(jobIndex);
+ }
+ }
+ if (jobs.size() == 0) {
+ mJobs.removeAt(jobSetIndex);
+ }
+ }
+ for (int jobSetIndex = mJobsPerSourceUid.size() - 1; jobSetIndex >= 0; jobSetIndex--) {
+ final ArraySet<JobStatus> jobs = mJobsPerSourceUid.valueAt(jobSetIndex);
+ for (int jobIndex = jobs.size() - 1; jobIndex >= 0; jobIndex--) {
+ if (predicate.test(jobs.valueAt(jobIndex))) {
+ jobs.removeAt(jobIndex);
+ }
+ }
+ if (jobs.size() == 0) {
+ mJobsPerSourceUid.removeAt(jobSetIndex);
+ }
+ }
+ }
+
+ public boolean contains(JobStatus job) {
+ final int uid = job.getUid();
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ return jobs != null && jobs.contains(job);
+ }
+
+ public JobStatus get(int uid, int jobId) {
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ if (jobs != null) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus job = jobs.valueAt(i);
+ if (job.getJobId() == jobId) {
+ return job;
+ }
+ }
+ }
+ return null;
+ }
+
+ // Inefficient; use only for testing
+ public List<JobStatus> getAllJobs() {
+ ArrayList<JobStatus> allJobs = new ArrayList<JobStatus>(size());
+ for (int i = mJobs.size() - 1; i >= 0; i--) {
+ ArraySet<JobStatus> jobs = mJobs.valueAt(i);
+ if (jobs != null) {
+ // Use a for loop over the ArraySet, so we don't need to make its
+ // optional collection class iterator implementation or have to go
+ // through a temporary array from toArray().
+ for (int j = jobs.size() - 1; j >= 0; j--) {
+ allJobs.add(jobs.valueAt(j));
+ }
+ }
+ }
+ return allJobs;
+ }
+
+ public void clear() {
+ mJobs.clear();
+ mJobsPerSourceUid.clear();
+ }
+
+ public int size() {
+ int total = 0;
+ for (int i = mJobs.size() - 1; i >= 0; i--) {
+ total += mJobs.valueAt(i).size();
+ }
+ return total;
+ }
+
+ // We only want to count the jobs that this uid has scheduled on its own
+ // behalf, not those that the app has scheduled on someone else's behalf.
+ public int countJobsForUid(int uid) {
+ int total = 0;
+ ArraySet<JobStatus> jobs = mJobs.get(uid);
+ if (jobs != null) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus job = jobs.valueAt(i);
+ if (job.getUid() == job.getSourceUid()) {
+ total++;
+ }
+ }
+ }
+ return total;
+ }
+
+ public void forEachJob(@Nullable Predicate<JobStatus> filterPredicate,
+ Consumer<JobStatus> functor) {
+ for (int uidIndex = mJobs.size() - 1; uidIndex >= 0; uidIndex--) {
+ ArraySet<JobStatus> jobs = mJobs.valueAt(uidIndex);
+ if (jobs != null) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ final JobStatus jobStatus = jobs.valueAt(i);
+ if ((filterPredicate == null) || filterPredicate.test(jobStatus)) {
+ functor.accept(jobStatus);
+ }
+ }
+ }
+ }
+ }
+
+ public void forEachJob(int callingUid, Consumer<JobStatus> functor) {
+ ArraySet<JobStatus> jobs = mJobs.get(callingUid);
+ if (jobs != null) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ functor.accept(jobs.valueAt(i));
+ }
+ }
+ }
+
+ public void forEachJobForSourceUid(int sourceUid, Consumer<JobStatus> functor) {
+ final ArraySet<JobStatus> jobs = mJobsPerSourceUid.get(sourceUid);
+ if (jobs != null) {
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ functor.accept(jobs.valueAt(i));
+ }
+ }
+ }
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
new file mode 100644
index 0000000..87bfc27
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 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;
+
+import com.android.server.job.controllers.JobStatus;
+
+/**
+ * Interface through which a {@link com.android.server.job.controllers.StateController} informs
+ * the {@link com.android.server.job.JobSchedulerService} that there are some tasks potentially
+ * ready to be run.
+ */
+public interface StateChangedListener {
+ /**
+ * Called by the controller to notify the JobManager that it should check on the state of a
+ * task.
+ */
+ public void onControllerStateChanged();
+
+ /**
+ * Called by the controller to notify the JobManager that regardless of the state of the task,
+ * it must be run immediately.
+ * @param jobStatus The state of the task which is to be run immediately. <strong>null
+ * indicates to the scheduler that any ready jobs should be flushed.</strong>
+ */
+ public void onRunJobNow(JobStatus jobStatus);
+
+ public void onDeviceIdleStateChanged(boolean deviceIdle);
+}
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
new file mode 100644
index 0000000..b698e5b
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2017 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 android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
+import com.android.server.AppStateTracker;
+import com.android.server.AppStateTracker.Listener;
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobStore;
+import com.android.server.job.StateControllerProto;
+import com.android.server.job.StateControllerProto.BackgroundJobsController.TrackedJob;
+
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Tracks the following pieces of JobStatus state:
+ *
+ * - the CONSTRAINT_BACKGROUND_NOT_RESTRICTED general constraint bit, which
+ * is used to selectively permit battery-saver exempted jobs to run; and
+ *
+ * - 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.
+ */
+public final class BackgroundJobsController extends StateController {
+ private static final String TAG = "JobScheduler.Background";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ // Tri-state about possible "is this uid 'active'?" knowledge
+ static final int UNKNOWN = 0;
+ static final int KNOWN_ACTIVE = 1;
+ static final int KNOWN_INACTIVE = 2;
+
+ private final AppStateTracker mAppStateTracker;
+
+ public BackgroundJobsController(JobSchedulerService service) {
+ super(service);
+
+ mAppStateTracker = Preconditions.checkNotNull(
+ LocalServices.getService(AppStateTracker.class));
+ mAppStateTracker.addListener(mForceAppStandbyListener);
+ }
+
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+ updateSingleJobRestrictionLocked(jobStatus, UNKNOWN);
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+ boolean forUpdate) {
+ }
+
+ @Override
+ public void dumpControllerStateLocked(final IndentingPrintWriter pw,
+ final Predicate<JobStatus> predicate) {
+ mAppStateTracker.dump(pw);
+ pw.println();
+
+ mService.getJobStore().forEachJob(predicate, (jobStatus) -> {
+ final int uid = jobStatus.getSourceUid();
+ final String sourcePkg = jobStatus.getSourcePackageName();
+ pw.print("#");
+ jobStatus.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, uid);
+ pw.print(mAppStateTracker.isUidActive(uid) ? " active" : " idle");
+ if (mAppStateTracker.isUidPowerSaveWhitelisted(uid) ||
+ mAppStateTracker.isUidTempPowerSaveWhitelisted(uid)) {
+ pw.print(", whitelisted");
+ }
+ pw.print(": ");
+ pw.print(sourcePkg);
+
+ pw.print(" [RUN_ANY_IN_BACKGROUND ");
+ pw.print(mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(uid, sourcePkg)
+ ? "allowed]" : "disallowed]");
+
+ if ((jobStatus.satisfiedConstraints
+ & JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
+ pw.println(" RUNNABLE");
+ } else {
+ pw.println(" WAITING");
+ }
+ });
+ }
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.BACKGROUND);
+
+ mAppStateTracker.dumpProto(proto,
+ StateControllerProto.BackgroundJobsController.FORCE_APP_STANDBY_TRACKER);
+
+ mService.getJobStore().forEachJob(predicate, (jobStatus) -> {
+ final long jsToken =
+ proto.start(StateControllerProto.BackgroundJobsController.TRACKED_JOBS);
+
+ jobStatus.writeToShortProto(proto,
+ TrackedJob.INFO);
+ final int sourceUid = jobStatus.getSourceUid();
+ proto.write(TrackedJob.SOURCE_UID, sourceUid);
+ final String sourcePkg = jobStatus.getSourcePackageName();
+ proto.write(TrackedJob.SOURCE_PACKAGE_NAME, sourcePkg);
+
+ proto.write(TrackedJob.IS_IN_FOREGROUND,
+ mAppStateTracker.isUidActive(sourceUid));
+ proto.write(TrackedJob.IS_WHITELISTED,
+ mAppStateTracker.isUidPowerSaveWhitelisted(sourceUid) ||
+ mAppStateTracker.isUidTempPowerSaveWhitelisted(sourceUid));
+
+ proto.write(
+ TrackedJob.CAN_RUN_ANY_IN_BACKGROUND,
+ mAppStateTracker.isRunAnyInBackgroundAppOpsAllowed(
+ sourceUid, sourcePkg));
+
+ proto.write(
+ TrackedJob.ARE_CONSTRAINTS_SATISFIED,
+ (jobStatus.satisfiedConstraints &
+ JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0);
+
+ proto.end(jsToken);
+ });
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+
+ private void updateAllJobRestrictionsLocked() {
+ updateJobRestrictionsLocked(/*filterUid=*/ -1, UNKNOWN);
+ }
+
+ private void updateJobRestrictionsForUidLocked(int uid, boolean isActive) {
+ updateJobRestrictionsLocked(uid, (isActive) ? KNOWN_ACTIVE : KNOWN_INACTIVE);
+ }
+
+ private void updateJobRestrictionsLocked(int filterUid, int newActiveState) {
+ final UpdateJobFunctor updateTrackedJobs = new UpdateJobFunctor(newActiveState);
+
+ final long start = DEBUG ? SystemClock.elapsedRealtimeNanos() : 0;
+
+ final JobStore store = mService.getJobStore();
+ if (filterUid > 0) {
+ store.forEachJobForSourceUid(filterUid, updateTrackedJobs);
+ } else {
+ store.forEachJob(updateTrackedJobs);
+ }
+
+ final long time = DEBUG ? (SystemClock.elapsedRealtimeNanos() - start) : 0;
+ if (DEBUG) {
+ Slog.d(TAG, String.format(
+ "Job status updated: %d/%d checked/total jobs, %d us",
+ updateTrackedJobs.mCheckedCount,
+ updateTrackedJobs.mTotalCount,
+ (time / 1000)
+ ));
+ }
+
+ if (updateTrackedJobs.mChanged) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+
+ boolean updateSingleJobRestrictionLocked(JobStatus jobStatus, int activeState) {
+
+ final int uid = jobStatus.getSourceUid();
+ final String packageName = jobStatus.getSourcePackageName();
+
+ final boolean canRun = !mAppStateTracker.areJobsRestricted(uid, packageName,
+ (jobStatus.getInternalFlags() & JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION)
+ != 0);
+
+ final boolean isActive;
+ if (activeState == UNKNOWN) {
+ isActive = mAppStateTracker.isUidActive(uid);
+ } else {
+ isActive = (activeState == KNOWN_ACTIVE);
+ }
+ boolean didChange = jobStatus.setBackgroundNotRestrictedConstraintSatisfied(canRun);
+ didChange |= jobStatus.setUidActive(isActive);
+ return didChange;
+ }
+
+ private final class UpdateJobFunctor implements Consumer<JobStatus> {
+ final int activeState;
+ boolean mChanged = false;
+ int mTotalCount = 0;
+ int mCheckedCount = 0;
+
+ public UpdateJobFunctor(int newActiveState) {
+ activeState = newActiveState;
+ }
+
+ @Override
+ public void accept(JobStatus jobStatus) {
+ mTotalCount++;
+ mCheckedCount++;
+ if (updateSingleJobRestrictionLocked(jobStatus, activeState)) {
+ mChanged = true;
+ }
+ }
+ }
+
+ private final Listener mForceAppStandbyListener = new Listener() {
+ @Override
+ public void updateAllJobs() {
+ synchronized (mLock) {
+ updateAllJobRestrictionsLocked();
+ }
+ }
+
+ @Override
+ public void updateJobsForUid(int uid, boolean isActive) {
+ synchronized (mLock) {
+ updateJobRestrictionsForUidLocked(uid, isActive);
+ }
+ }
+
+ @Override
+ public void updateJobsForUidPackage(int uid, String packageName, boolean isActive) {
+ synchronized (mLock) {
+ updateJobRestrictionsForUidLocked(uid, isActive);
+ }
+ }
+ };
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
new file mode 100644
index 0000000..46658ad
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2014 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.server.job.JobSchedulerService.sElapsedRealtimeClock;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.BatteryManagerInternal;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateControllerProto;
+
+import java.util.function.Predicate;
+
+/**
+ * Simple controller that tracks whether the phone is charging or not. The phone is considered to
+ * be charging when it's been plugged in for more than two minutes, and the system has broadcast
+ * ACTION_BATTERY_OK.
+ */
+public final class BatteryController extends StateController {
+ private static final String TAG = "JobScheduler.Battery";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>();
+ private ChargingTracker mChargeTracker;
+
+ @VisibleForTesting
+ public ChargingTracker getTracker() {
+ return mChargeTracker;
+ }
+
+ public BatteryController(JobSchedulerService service) {
+ super(service);
+ mChargeTracker = new ChargingTracker();
+ mChargeTracker.startTracking();
+ }
+
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
+ if (taskStatus.hasPowerConstraint()) {
+ mTrackedTasks.add(taskStatus);
+ taskStatus.setTrackingController(JobStatus.TRACKING_BATTERY);
+ taskStatus.setChargingConstraintSatisfied(mChargeTracker.isOnStablePower());
+ taskStatus.setBatteryNotLowConstraintSatisfied(mChargeTracker.isBatteryNotLow());
+ }
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
+ if (taskStatus.clearTrackingController(JobStatus.TRACKING_BATTERY)) {
+ mTrackedTasks.remove(taskStatus);
+ }
+ }
+
+ private void maybeReportNewChargingStateLocked() {
+ final boolean stablePower = mChargeTracker.isOnStablePower();
+ final boolean batteryNotLow = mChargeTracker.isBatteryNotLow();
+ if (DEBUG) {
+ Slog.d(TAG, "maybeReportNewChargingStateLocked: " + stablePower);
+ }
+ boolean reportChange = false;
+ for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
+ final JobStatus ts = mTrackedTasks.valueAt(i);
+ boolean previous = ts.setChargingConstraintSatisfied(stablePower);
+ if (previous != stablePower) {
+ reportChange = true;
+ }
+ previous = ts.setBatteryNotLowConstraintSatisfied(batteryNotLow);
+ if (previous != batteryNotLow) {
+ reportChange = true;
+ }
+ }
+ if (stablePower || batteryNotLow) {
+ // If one of our conditions has been satisfied, always schedule any newly ready jobs.
+ mStateChangedListener.onRunJobNow(null);
+ } else if (reportChange) {
+ // Otherwise, just let the job scheduler know the state has changed and take care of it
+ // as it thinks is best.
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+
+ public final class ChargingTracker extends BroadcastReceiver {
+ /**
+ * Track whether we're "charging", where charging means that we're ready to commit to
+ * doing work.
+ */
+ private boolean mCharging;
+ /** Keep track of whether the battery is charged enough that we want to do work. */
+ private boolean mBatteryHealthy;
+ /** Sequence number of last broadcast. */
+ private int mLastBatterySeq = -1;
+
+ private BroadcastReceiver mMonitor;
+
+ public ChargingTracker() {
+ }
+
+ public void startTracking() {
+ IntentFilter filter = new IntentFilter();
+
+ // Battery health.
+ filter.addAction(Intent.ACTION_BATTERY_LOW);
+ filter.addAction(Intent.ACTION_BATTERY_OKAY);
+ // Charging/not charging.
+ filter.addAction(BatteryManager.ACTION_CHARGING);
+ filter.addAction(BatteryManager.ACTION_DISCHARGING);
+ mContext.registerReceiver(this, filter);
+
+ // Initialise tracker state.
+ BatteryManagerInternal batteryManagerInternal =
+ LocalServices.getService(BatteryManagerInternal.class);
+ mBatteryHealthy = !batteryManagerInternal.getBatteryLevelLow();
+ mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
+ }
+
+ public void setMonitorBatteryLocked(boolean enabled) {
+ if (enabled) {
+ if (mMonitor == null) {
+ mMonitor = new BroadcastReceiver() {
+ @Override public void onReceive(Context context, Intent intent) {
+ ChargingTracker.this.onReceive(context, intent);
+ }
+ };
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ mContext.registerReceiver(mMonitor, filter);
+ }
+ } else {
+ if (mMonitor != null) {
+ mContext.unregisterReceiver(mMonitor);
+ mMonitor = null;
+ }
+ }
+ }
+
+ public boolean isOnStablePower() {
+ return mCharging && mBatteryHealthy;
+ }
+
+ public boolean isBatteryNotLow() {
+ return mBatteryHealthy;
+ }
+
+ public boolean isMonitoring() {
+ return mMonitor != null;
+ }
+
+ public int getSeq() {
+ return mLastBatterySeq;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onReceiveInternal(intent);
+ }
+
+ @VisibleForTesting
+ public void onReceiveInternal(Intent intent) {
+ synchronized (mLock) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_BATTERY_LOW.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Battery life too low to do work. @ "
+ + sElapsedRealtimeClock.millis());
+ }
+ // If we get this action, the battery is discharging => it isn't plugged in so
+ // there's no work to cancel. We track this variable for the case where it is
+ // charging, but hasn't been for long enough to be healthy.
+ mBatteryHealthy = false;
+ maybeReportNewChargingStateLocked();
+ } else if (Intent.ACTION_BATTERY_OKAY.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Battery life healthy enough to do work. @ "
+ + sElapsedRealtimeClock.millis());
+ }
+ mBatteryHealthy = true;
+ maybeReportNewChargingStateLocked();
+ } else if (BatteryManager.ACTION_CHARGING.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Received charging intent, fired @ "
+ + sElapsedRealtimeClock.millis());
+ }
+ mCharging = true;
+ maybeReportNewChargingStateLocked();
+ } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Disconnected from power.");
+ }
+ mCharging = false;
+ maybeReportNewChargingStateLocked();
+ }
+ mLastBatterySeq = intent.getIntExtra(BatteryManager.EXTRA_SEQUENCE,
+ mLastBatterySeq);
+ }
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(IndentingPrintWriter pw,
+ Predicate<JobStatus> predicate) {
+ pw.println("Stable power: " + mChargeTracker.isOnStablePower());
+ pw.println("Not low: " + mChargeTracker.isBatteryNotLow());
+
+ if (mChargeTracker.isMonitoring()) {
+ pw.print("MONITORING: seq=");
+ pw.println(mChargeTracker.getSeq());
+ }
+ pw.println();
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ final JobStatus js = mTrackedTasks.valueAt(i);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ pw.print("#");
+ js.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, js.getSourceUid());
+ pw.println();
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.BATTERY);
+
+ proto.write(StateControllerProto.BatteryController.IS_ON_STABLE_POWER,
+ mChargeTracker.isOnStablePower());
+ proto.write(StateControllerProto.BatteryController.IS_BATTERY_NOT_LOW,
+ mChargeTracker.isBatteryNotLow());
+
+ proto.write(StateControllerProto.BatteryController.IS_MONITORING,
+ mChargeTracker.isMonitoring());
+ proto.write(StateControllerProto.BatteryController.LAST_BROADCAST_SEQUENCE_NUMBER,
+ mChargeTracker.getSeq());
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ final JobStatus js = mTrackedTasks.valueAt(i);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ final long jsToken = proto.start(StateControllerProto.BatteryController.TRACKED_JOBS);
+ js.writeToShortProto(proto, StateControllerProto.BatteryController.TrackedJob.INFO);
+ proto.write(StateControllerProto.BatteryController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.end(jsToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
new file mode 100644
index 0000000..f8cf6ae
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -0,0 +1,694 @@
+/*
+ * Copyright (C) 2014 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 android.net.NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+
+import android.app.job.JobInfo;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.INetworkPolicyListener;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkPolicyManager;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.text.format.DateUtils;
+import android.util.ArraySet;
+import android.util.DataUnit;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobSchedulerService.Constants;
+import com.android.server.job.JobServiceContext;
+import com.android.server.job.StateControllerProto;
+import com.android.server.net.NetworkPolicyManagerInternal;
+
+import java.util.Objects;
+import java.util.function.Predicate;
+
+/**
+ * Handles changes in connectivity.
+ * <p>
+ * Each app can have a different default networks or different connectivity
+ * status due to user-requested network policies, so we need to check
+ * constraints on a per-UID basis.
+ *
+ * Test: atest com.android.server.job.controllers.ConnectivityControllerTest
+ */
+public final class ConnectivityController extends StateController implements
+ ConnectivityManager.OnNetworkActiveListener {
+ private static final String TAG = "JobScheduler.Connectivity";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ConnectivityManager mConnManager;
+ private final NetworkPolicyManager mNetPolicyManager;
+ private final NetworkPolicyManagerInternal mNetPolicyManagerInternal;
+
+ /** List of tracked jobs keyed by source UID. */
+ @GuardedBy("mLock")
+ private final SparseArray<ArraySet<JobStatus>> mTrackedJobs = new SparseArray<>();
+
+ /**
+ * Keep track of all the UID's jobs that the controller has requested that NetworkPolicyManager
+ * grant an exception to in the app standby chain.
+ */
+ @GuardedBy("mLock")
+ private final SparseArray<ArraySet<JobStatus>> mRequestedWhitelistJobs = new SparseArray<>();
+
+ /** List of currently available networks. */
+ @GuardedBy("mLock")
+ private final ArraySet<Network> mAvailableNetworks = new ArraySet<>();
+
+ private boolean mUseQuotaLimit;
+
+ private static final int MSG_DATA_SAVER_TOGGLED = 0;
+ private static final int MSG_UID_RULES_CHANGES = 1;
+ private static final int MSG_REEVALUATE_JOBS = 2;
+
+ private final Handler mHandler;
+
+ public ConnectivityController(JobSchedulerService service) {
+ super(service);
+ mHandler = new CcHandler(mContext.getMainLooper());
+
+ mConnManager = mContext.getSystemService(ConnectivityManager.class);
+ mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class);
+ mNetPolicyManagerInternal = LocalServices.getService(NetworkPolicyManagerInternal.class);
+
+ // We're interested in all network changes; internally we match these
+ // network changes against the active network for each UID with jobs.
+ final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build();
+ mConnManager.registerNetworkCallback(request, mNetworkCallback);
+
+ mNetPolicyManager.registerListener(mNetPolicyListener);
+
+ mUseQuotaLimit = !mConstants.USE_HEARTBEATS;
+ }
+
+ @GuardedBy("mLock")
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+ if (jobStatus.hasConnectivityConstraint()) {
+ updateConstraintsSatisfied(jobStatus);
+ ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid());
+ if (jobs == null) {
+ jobs = new ArraySet<>();
+ mTrackedJobs.put(jobStatus.getSourceUid(), jobs);
+ }
+ jobs.add(jobStatus);
+ jobStatus.setTrackingController(JobStatus.TRACKING_CONNECTIVITY);
+ }
+ }
+
+ @GuardedBy("mLock")
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+ boolean forUpdate) {
+ if (jobStatus.clearTrackingController(JobStatus.TRACKING_CONNECTIVITY)) {
+ ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUid());
+ if (jobs != null) {
+ jobs.remove(jobStatus);
+ }
+ maybeRevokeStandbyExceptionLocked(jobStatus);
+ }
+ }
+
+ @GuardedBy("mLock")
+ @Override
+ public void onConstantsUpdatedLocked() {
+ if (mConstants.USE_HEARTBEATS) {
+ // App idle exceptions are only requested for the rolling quota system.
+ if (DEBUG) Slog.i(TAG, "Revoking all standby exceptions");
+ for (int i = 0; i < mRequestedWhitelistJobs.size(); ++i) {
+ int uid = mRequestedWhitelistJobs.keyAt(i);
+ mNetPolicyManagerInternal.setAppIdleWhitelist(uid, false);
+ }
+ mRequestedWhitelistJobs.clear();
+ }
+ if (mUseQuotaLimit == mConstants.USE_HEARTBEATS) {
+ mUseQuotaLimit = !mConstants.USE_HEARTBEATS;
+ mHandler.obtainMessage(MSG_REEVALUATE_JOBS).sendToTarget();
+ }
+ }
+
+ /**
+ * Returns true if the job's requested network is available. This DOES NOT necesarilly mean
+ * that the UID has been granted access to the network.
+ */
+ public boolean isNetworkAvailable(JobStatus job) {
+ synchronized (mLock) {
+ for (int i = 0; i < mAvailableNetworks.size(); ++i) {
+ final Network network = mAvailableNetworks.valueAt(i);
+ final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(
+ network);
+ final boolean satisfied = isSatisfied(job, network, capabilities, mConstants);
+ if (DEBUG) {
+ Slog.v(TAG, "isNetworkAvailable(" + job + ") with network " + network
+ + " and capabilities " + capabilities + ". Satisfied=" + satisfied);
+ }
+ if (satisfied) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Request that NetworkPolicyManager grant an exception to the uid from its standby policy
+ * chain.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void requestStandbyExceptionLocked(JobStatus job) {
+ final int uid = job.getSourceUid();
+ // Need to call this before adding the job.
+ final boolean isExceptionRequested = isStandbyExceptionRequestedLocked(uid);
+ ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid);
+ if (jobs == null) {
+ jobs = new ArraySet<JobStatus>();
+ mRequestedWhitelistJobs.put(uid, jobs);
+ }
+ if (!jobs.add(job) || isExceptionRequested) {
+ if (DEBUG) {
+ Slog.i(TAG, "requestStandbyExceptionLocked found exception already requested.");
+ }
+ return;
+ }
+ if (DEBUG) Slog.i(TAG, "Requesting standby exception for UID: " + uid);
+ mNetPolicyManagerInternal.setAppIdleWhitelist(uid, true);
+ }
+
+ /** Returns whether a standby exception has been requested for the UID. */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ boolean isStandbyExceptionRequestedLocked(final int uid) {
+ ArraySet jobs = mRequestedWhitelistJobs.get(uid);
+ return jobs != null && jobs.size() > 0;
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ boolean wouldBeReadyWithConnectivityLocked(JobStatus jobStatus) {
+ final boolean networkAvailable = isNetworkAvailable(jobStatus);
+ if (DEBUG) {
+ Slog.v(TAG, "wouldBeReadyWithConnectivityLocked: " + jobStatus.toShortString()
+ + " networkAvailable=" + networkAvailable);
+ }
+ // If the network isn't available, then requesting an exception won't help.
+
+ return networkAvailable && wouldBeReadyWithConstraintLocked(jobStatus,
+ JobStatus.CONSTRAINT_CONNECTIVITY);
+ }
+
+ /**
+ * Tell NetworkPolicyManager not to block a UID's network connection if that's the only
+ * thing stopping a job from running.
+ */
+ @GuardedBy("mLock")
+ @Override
+ public void evaluateStateLocked(JobStatus jobStatus) {
+ if (mConstants.USE_HEARTBEATS) {
+ // This should only be used for the rolling quota system.
+ return;
+ }
+
+ if (!jobStatus.hasConnectivityConstraint()) {
+ return;
+ }
+
+ // Always check the full job readiness stat in case the component has been disabled.
+ if (wouldBeReadyWithConnectivityLocked(jobStatus)) {
+ if (DEBUG) {
+ Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would be ready.");
+ }
+ requestStandbyExceptionLocked(jobStatus);
+ } else {
+ if (DEBUG) {
+ Slog.i(TAG, "evaluateStateLocked finds job " + jobStatus + " would not be ready.");
+ }
+ maybeRevokeStandbyExceptionLocked(jobStatus);
+ }
+ }
+
+ @GuardedBy("mLock")
+ @Override
+ public void reevaluateStateLocked(final int uid) {
+ if (mConstants.USE_HEARTBEATS) {
+ return;
+ }
+ // Check if we still need a connectivity exception in case the JobService was disabled.
+ ArraySet<JobStatus> jobs = mTrackedJobs.get(uid);
+ if (jobs == null) {
+ return;
+ }
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ evaluateStateLocked(jobs.valueAt(i));
+ }
+ }
+
+ /** Cancel the requested standby exception if none of the jobs would be ready to run anyway. */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ void maybeRevokeStandbyExceptionLocked(final JobStatus job) {
+ final int uid = job.getSourceUid();
+ if (!isStandbyExceptionRequestedLocked(uid)) {
+ return;
+ }
+ ArraySet<JobStatus> jobs = mRequestedWhitelistJobs.get(uid);
+ if (jobs == null) {
+ Slog.wtf(TAG,
+ "maybeRevokeStandbyExceptionLocked found null jobs array even though a "
+ + "standby exception has been requested.");
+ return;
+ }
+ if (!jobs.remove(job) || jobs.size() > 0) {
+ if (DEBUG) {
+ Slog.i(TAG,
+ "maybeRevokeStandbyExceptionLocked not revoking because there are still "
+ + jobs.size() + " jobs left.");
+ }
+ return;
+ }
+ // No more jobs that need an exception.
+ revokeStandbyExceptionLocked(uid);
+ }
+
+ /**
+ * Tell NetworkPolicyManager to revoke any exception it granted from its standby policy chain
+ * for the uid.
+ */
+ @GuardedBy("mLock")
+ private void revokeStandbyExceptionLocked(final int uid) {
+ if (DEBUG) Slog.i(TAG, "Revoking standby exception for UID: " + uid);
+ mNetPolicyManagerInternal.setAppIdleWhitelist(uid, false);
+ mRequestedWhitelistJobs.remove(uid);
+ }
+
+ @GuardedBy("mLock")
+ @Override
+ public void onAppRemovedLocked(String pkgName, int uid) {
+ mTrackedJobs.delete(uid);
+ }
+
+ /**
+ * Test to see if running the given job on the given network is insane.
+ * <p>
+ * For example, if a job is trying to send 10MB over a 128Kbps EDGE
+ * connection, it would take 10.4 minutes, and has no chance of succeeding
+ * before the job times out, so we'd be insane to try running it.
+ */
+ private boolean isInsane(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities, Constants constants) {
+ final long maxJobExecutionTimeMs = mUseQuotaLimit
+ ? mService.getMaxJobExecutionTimeMs(jobStatus)
+ : JobServiceContext.EXECUTING_TIMESLICE_MILLIS;
+
+ final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes();
+ if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ final long bandwidth = capabilities.getLinkDownstreamBandwidthKbps();
+ // If we don't know the bandwidth, all we can do is hope the job finishes in time.
+ if (bandwidth != LINK_BANDWIDTH_UNSPECIFIED) {
+ // Divide by 8 to convert bits to bytes.
+ final long estimatedMillis = ((downloadBytes * DateUtils.SECOND_IN_MILLIS)
+ / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8));
+ if (estimatedMillis > maxJobExecutionTimeMs) {
+ // If we'd never finish before the timeout, we'd be insane!
+ Slog.w(TAG, "Estimated " + downloadBytes + " download bytes over " + bandwidth
+ + " kbps network would take " + estimatedMillis + "ms and job has "
+ + maxJobExecutionTimeMs + "ms to run; that's insane!");
+ return true;
+ }
+ }
+ }
+
+ final long uploadBytes = jobStatus.getEstimatedNetworkUploadBytes();
+ if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ final long bandwidth = capabilities.getLinkUpstreamBandwidthKbps();
+ // If we don't know the bandwidth, all we can do is hope the job finishes in time.
+ if (bandwidth != LINK_BANDWIDTH_UNSPECIFIED) {
+ // Divide by 8 to convert bits to bytes.
+ final long estimatedMillis = ((uploadBytes * DateUtils.SECOND_IN_MILLIS)
+ / (DataUnit.KIBIBYTES.toBytes(bandwidth) / 8));
+ if (estimatedMillis > maxJobExecutionTimeMs) {
+ // If we'd never finish before the timeout, we'd be insane!
+ Slog.w(TAG, "Estimated " + uploadBytes + " upload bytes over " + bandwidth
+ + " kbps network would take " + estimatedMillis + "ms and job has "
+ + maxJobExecutionTimeMs + "ms to run; that's insane!");
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean isCongestionDelayed(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities, Constants constants) {
+ // If network is congested, and job is less than 50% through the
+ // developer-requested window, then we're okay delaying the job.
+ if (!capabilities.hasCapability(NET_CAPABILITY_NOT_CONGESTED)) {
+ return jobStatus.getFractionRunTime() < constants.CONN_CONGESTION_DELAY_FRAC;
+ } else {
+ return false;
+ }
+ }
+
+ private static boolean isStrictSatisfied(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities, Constants constants) {
+ return jobStatus.getJob().getRequiredNetwork().networkCapabilities
+ .satisfiedByNetworkCapabilities(capabilities);
+ }
+
+ private static boolean isRelaxedSatisfied(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities, Constants constants) {
+ // Only consider doing this for prefetching jobs
+ if (!jobStatus.getJob().isPrefetch()) {
+ return false;
+ }
+
+ // See if we match after relaxing any unmetered request
+ final NetworkCapabilities relaxed = new NetworkCapabilities(
+ jobStatus.getJob().getRequiredNetwork().networkCapabilities)
+ .removeCapability(NET_CAPABILITY_NOT_METERED);
+ if (relaxed.satisfiedByNetworkCapabilities(capabilities)) {
+ // TODO: treat this as "maybe" response; need to check quotas
+ return jobStatus.getFractionRunTime() > constants.CONN_PREFETCH_RELAX_FRAC;
+ } else {
+ return false;
+ }
+ }
+
+ @VisibleForTesting
+ boolean isSatisfied(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities, Constants constants) {
+ // Zeroth, we gotta have a network to think about being satisfied
+ if (network == null || capabilities == null) return false;
+
+ // First, are we insane?
+ if (isInsane(jobStatus, network, capabilities, constants)) return false;
+
+ // Second, is the network congested?
+ if (isCongestionDelayed(jobStatus, network, capabilities, constants)) return false;
+
+ // Third, is the network a strict match?
+ if (isStrictSatisfied(jobStatus, network, capabilities, constants)) return true;
+
+ // Third, is the network a relaxed match?
+ if (isRelaxedSatisfied(jobStatus, network, capabilities, constants)) return true;
+
+ return false;
+ }
+
+ private boolean updateConstraintsSatisfied(JobStatus jobStatus) {
+ final Network network = mConnManager.getActiveNetworkForUid(jobStatus.getSourceUid());
+ final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network);
+ return updateConstraintsSatisfied(jobStatus, network, capabilities);
+ }
+
+ private boolean updateConstraintsSatisfied(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities) {
+ // TODO: consider matching against non-active networks
+
+ final boolean ignoreBlocked = (jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
+ final NetworkInfo info = mConnManager.getNetworkInfoForUid(network,
+ jobStatus.getSourceUid(), ignoreBlocked);
+
+ final boolean connected = (info != null) && info.isConnected();
+ final boolean satisfied = isSatisfied(jobStatus, network, capabilities, mConstants);
+
+ final boolean changed = jobStatus
+ .setConnectivityConstraintSatisfied(connected && satisfied);
+
+ // Pass along the evaluated network for job to use; prevents race
+ // conditions as default routes change over time, and opens the door to
+ // using non-default routes.
+ jobStatus.network = network;
+
+ if (DEBUG) {
+ Slog.i(TAG, "Connectivity " + (changed ? "CHANGED" : "unchanged")
+ + " for " + jobStatus + ": connected=" + connected
+ + " satisfied=" + satisfied);
+ }
+ return changed;
+ }
+
+ /**
+ * Update any jobs tracked by this controller that match given filters.
+ *
+ * @param filterUid only update jobs belonging to this UID, or {@code -1} to
+ * update all tracked jobs.
+ * @param filterNetwork only update jobs that would use this
+ * {@link Network}, or {@code null} to update all tracked jobs.
+ */
+ private void updateTrackedJobs(int filterUid, Network filterNetwork) {
+ synchronized (mLock) {
+ // Since this is a really hot codepath, temporarily cache any
+ // answers that we get from ConnectivityManager.
+ final SparseArray<NetworkCapabilities> networkToCapabilities = new SparseArray<>();
+
+ boolean changed = false;
+ if (filterUid == -1) {
+ for (int i = mTrackedJobs.size() - 1; i >= 0; i--) {
+ changed |= updateTrackedJobsLocked(mTrackedJobs.valueAt(i),
+ filterNetwork, networkToCapabilities);
+ }
+ } else {
+ changed = updateTrackedJobsLocked(mTrackedJobs.get(filterUid),
+ filterNetwork, networkToCapabilities);
+ }
+ if (changed) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ }
+
+ private boolean updateTrackedJobsLocked(ArraySet<JobStatus> jobs, Network filterNetwork,
+ SparseArray<NetworkCapabilities> networkToCapabilities) {
+ if (jobs == null || jobs.size() == 0) {
+ return false;
+ }
+
+ final Network network = mConnManager.getActiveNetworkForUid(jobs.valueAt(0).getSourceUid());
+ final int netId = network != null ? network.netId : -1;
+ NetworkCapabilities capabilities = networkToCapabilities.get(netId);
+ if (capabilities == null) {
+ capabilities = mConnManager.getNetworkCapabilities(network);
+ networkToCapabilities.put(netId, capabilities);
+ }
+ final boolean networkMatch = (filterNetwork == null
+ || Objects.equals(filterNetwork, network));
+
+ boolean changed = false;
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ final JobStatus js = jobs.valueAt(i);
+
+ // Update either when we have a network match, or when the
+ // job hasn't yet been evaluated against the currently
+ // active network; typically when we just lost a network.
+ if (networkMatch || !Objects.equals(js.network, network)) {
+ changed |= updateConstraintsSatisfied(js, network, capabilities);
+ }
+ }
+ return changed;
+ }
+
+ /**
+ * We know the network has just come up. We want to run any jobs that are ready.
+ */
+ @Override
+ public void onNetworkActive() {
+ synchronized (mLock) {
+ for (int i = mTrackedJobs.size()-1; i >= 0; i--) {
+ final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i);
+ for (int j = jobs.size() - 1; j >= 0; j--) {
+ final JobStatus js = jobs.valueAt(j);
+ if (js.isReady()) {
+ if (DEBUG) {
+ Slog.d(TAG, "Running " + js + " due to network activity.");
+ }
+ mStateChangedListener.onRunJobNow(js);
+ }
+ }
+ }
+ }
+ }
+
+ private final NetworkCallback mNetworkCallback = new NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ if (DEBUG) Slog.v(TAG, "onAvailable: " + network);
+ synchronized (mLock) {
+ mAvailableNetworks.add(network);
+ }
+ }
+
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) {
+ if (DEBUG) {
+ Slog.v(TAG, "onCapabilitiesChanged: " + network);
+ }
+ updateTrackedJobs(-1, network);
+ }
+
+ @Override
+ public void onLost(Network network) {
+ if (DEBUG) {
+ Slog.v(TAG, "onLost: " + network);
+ }
+ synchronized (mLock) {
+ mAvailableNetworks.remove(network);
+ }
+ updateTrackedJobs(-1, network);
+ }
+ };
+
+ private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() {
+ @Override
+ public void onRestrictBackgroundChanged(boolean restrictBackground) {
+ if (DEBUG) {
+ Slog.v(TAG, "onRestrictBackgroundChanged: " + restrictBackground);
+ }
+ mHandler.obtainMessage(MSG_DATA_SAVER_TOGGLED).sendToTarget();
+ }
+
+ @Override
+ public void onUidRulesChanged(int uid, int uidRules) {
+ if (DEBUG) {
+ Slog.v(TAG, "onUidRulesChanged: " + uid);
+ }
+ mHandler.obtainMessage(MSG_UID_RULES_CHANGES, uid, 0).sendToTarget();
+ }
+ };
+
+ private class CcHandler extends Handler {
+ CcHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ synchronized (mLock) {
+ switch (msg.what) {
+ case MSG_DATA_SAVER_TOGGLED:
+ updateTrackedJobs(-1, null);
+ break;
+ case MSG_UID_RULES_CHANGES:
+ updateTrackedJobs(msg.arg1, null);
+ break;
+ case MSG_REEVALUATE_JOBS:
+ updateTrackedJobs(-1, null);
+ break;
+ }
+ }
+ }
+ };
+
+ @GuardedBy("mLock")
+ @Override
+ public void dumpControllerStateLocked(IndentingPrintWriter pw,
+ Predicate<JobStatus> predicate) {
+ pw.print("mUseQuotaLimit="); pw.println(mUseQuotaLimit);
+
+ if (mRequestedWhitelistJobs.size() > 0) {
+ pw.print("Requested standby exceptions:");
+ for (int i = 0; i < mRequestedWhitelistJobs.size(); i++) {
+ pw.print(" ");
+ pw.print(mRequestedWhitelistJobs.keyAt(i));
+ pw.print(" (");
+ pw.print(mRequestedWhitelistJobs.valueAt(i).size());
+ pw.print(" jobs)");
+ }
+ pw.println();
+ }
+ if (mAvailableNetworks.size() > 0) {
+ pw.println("Available networks:");
+ pw.increaseIndent();
+ for (int i = 0; i < mAvailableNetworks.size(); i++) {
+ pw.println(mAvailableNetworks.valueAt(i));
+ }
+ pw.decreaseIndent();
+ } else {
+ pw.println("No available networks");
+ }
+ for (int i = 0; i < mTrackedJobs.size(); i++) {
+ final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i);
+ for (int j = 0; j < jobs.size(); j++) {
+ final JobStatus js = jobs.valueAt(j);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ pw.print("#");
+ js.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, js.getSourceUid());
+ pw.print(": ");
+ pw.print(js.getJob().getRequiredNetwork());
+ pw.println();
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.CONNECTIVITY);
+
+ for (int i = 0; i < mTrackedJobs.size(); i++) {
+ final ArraySet<JobStatus> jobs = mTrackedJobs.valueAt(i);
+ for (int j = 0; j < jobs.size(); j++) {
+ final JobStatus js = jobs.valueAt(j);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ final long jsToken = proto.start(
+ StateControllerProto.ConnectivityController.TRACKED_JOBS);
+ js.writeToShortProto(proto,
+ StateControllerProto.ConnectivityController.TrackedJob.INFO);
+ proto.write(StateControllerProto.ConnectivityController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ NetworkRequest rn = js.getJob().getRequiredNetwork();
+ if (rn != null) {
+ rn.writeToProto(proto,
+ StateControllerProto.ConnectivityController.TrackedJob
+ .REQUIRED_NETWORK);
+ }
+ proto.end(jsToken);
+ }
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
new file mode 100644
index 0000000..a775cf5
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2016 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 android.annotation.UserIdInt;
+import android.app.job.JobInfo;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateControllerProto;
+import com.android.server.job.StateControllerProto.ContentObserverController.Observer.TriggerContentData;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+
+/**
+ * Controller for monitoring changes to content URIs through a ContentObserver.
+ */
+public final class ContentObserverController extends StateController {
+ private static final String TAG = "JobScheduler.ContentObserver";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ /**
+ * Maximum number of changing URIs we will batch together to report.
+ * XXX Should be smarter about this, restricting it by the maximum number
+ * of characters we will retain.
+ */
+ private static final int MAX_URIS_REPORTED = 50;
+
+ /**
+ * At this point we consider it urgent to schedule the job ASAP.
+ */
+ private static final int URIS_URGENT_THRESHOLD = 40;
+
+ final private ArraySet<JobStatus> mTrackedTasks = new ArraySet<>();
+ /**
+ * Per-userid {@link JobInfo.TriggerContentUri} keyed ContentObserver cache.
+ */
+ final SparseArray<ArrayMap<JobInfo.TriggerContentUri, ObserverInstance>> mObservers =
+ new SparseArray<>();
+ final Handler mHandler;
+
+ public ContentObserverController(JobSchedulerService service) {
+ super(service);
+ mHandler = new Handler(mContext.getMainLooper());
+ }
+
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
+ if (taskStatus.hasContentTriggerConstraint()) {
+ if (taskStatus.contentObserverJobInstance == null) {
+ taskStatus.contentObserverJobInstance = new JobInstance(taskStatus);
+ }
+ if (DEBUG) {
+ Slog.i(TAG, "Tracking content-trigger job " + taskStatus);
+ }
+ mTrackedTasks.add(taskStatus);
+ taskStatus.setTrackingController(JobStatus.TRACKING_CONTENT);
+ boolean havePendingUris = false;
+ // If there is a previous job associated with the new job, propagate over
+ // any pending content URI trigger reports.
+ if (taskStatus.contentObserverJobInstance.mChangedAuthorities != null) {
+ havePendingUris = true;
+ }
+ // If we have previously reported changed authorities/uris, then we failed
+ // to complete the job with them so will re-record them to report again.
+ if (taskStatus.changedAuthorities != null) {
+ havePendingUris = true;
+ if (taskStatus.contentObserverJobInstance.mChangedAuthorities == null) {
+ taskStatus.contentObserverJobInstance.mChangedAuthorities
+ = new ArraySet<>();
+ }
+ for (String auth : taskStatus.changedAuthorities) {
+ taskStatus.contentObserverJobInstance.mChangedAuthorities.add(auth);
+ }
+ if (taskStatus.changedUris != null) {
+ if (taskStatus.contentObserverJobInstance.mChangedUris == null) {
+ taskStatus.contentObserverJobInstance.mChangedUris = new ArraySet<>();
+ }
+ for (Uri uri : taskStatus.changedUris) {
+ taskStatus.contentObserverJobInstance.mChangedUris.add(uri);
+ }
+ }
+ taskStatus.changedAuthorities = null;
+ taskStatus.changedUris = null;
+ }
+ taskStatus.changedAuthorities = null;
+ taskStatus.changedUris = null;
+ taskStatus.setContentTriggerConstraintSatisfied(havePendingUris);
+ }
+ if (lastJob != null && lastJob.contentObserverJobInstance != null) {
+ // And now we can detach the instance state from the last job.
+ lastJob.contentObserverJobInstance.detachLocked();
+ lastJob.contentObserverJobInstance = null;
+ }
+ }
+
+ @Override
+ public void prepareForExecutionLocked(JobStatus taskStatus) {
+ if (taskStatus.hasContentTriggerConstraint()) {
+ if (taskStatus.contentObserverJobInstance != null) {
+ taskStatus.changedUris = taskStatus.contentObserverJobInstance.mChangedUris;
+ taskStatus.changedAuthorities
+ = taskStatus.contentObserverJobInstance.mChangedAuthorities;
+ taskStatus.contentObserverJobInstance.mChangedUris = null;
+ taskStatus.contentObserverJobInstance.mChangedAuthorities = null;
+ }
+ }
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
+ boolean forUpdate) {
+ if (taskStatus.clearTrackingController(JobStatus.TRACKING_CONTENT)) {
+ mTrackedTasks.remove(taskStatus);
+ if (taskStatus.contentObserverJobInstance != null) {
+ taskStatus.contentObserverJobInstance.unscheduleLocked();
+ if (incomingJob != null) {
+ if (taskStatus.contentObserverJobInstance != null
+ && taskStatus.contentObserverJobInstance.mChangedAuthorities != null) {
+ // We are stopping this job, but it is going to be replaced by this given
+ // incoming job. We want to propagate our state over to it, so we don't
+ // lose any content changes that had happened since the last one started.
+ // If there is a previous job associated with the new job, propagate over
+ // any pending content URI trigger reports.
+ if (incomingJob.contentObserverJobInstance == null) {
+ incomingJob.contentObserverJobInstance = new JobInstance(incomingJob);
+ }
+ incomingJob.contentObserverJobInstance.mChangedAuthorities
+ = taskStatus.contentObserverJobInstance.mChangedAuthorities;
+ incomingJob.contentObserverJobInstance.mChangedUris
+ = taskStatus.contentObserverJobInstance.mChangedUris;
+ taskStatus.contentObserverJobInstance.mChangedAuthorities = null;
+ taskStatus.contentObserverJobInstance.mChangedUris = null;
+ }
+ // We won't detach the content observers here, because we want to
+ // allow them to continue monitoring so we don't miss anything... and
+ // since we are giving an incomingJob here, we know this will be
+ // immediately followed by a start tracking of that job.
+ } else {
+ // But here there is no incomingJob, so nothing coming up, so time to detach.
+ taskStatus.contentObserverJobInstance.detachLocked();
+ taskStatus.contentObserverJobInstance = null;
+ }
+ }
+ if (DEBUG) {
+ Slog.i(TAG, "No longer tracking job " + taskStatus);
+ }
+ }
+ }
+
+ @Override
+ public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) {
+ if (failureToReschedule.hasContentTriggerConstraint()
+ && newJob.hasContentTriggerConstraint()) {
+ // Our job has failed, and we are scheduling a new job for it.
+ // Copy the last reported content changes in to the new job, so when
+ // we schedule the new one we will pick them up and report them again.
+ newJob.changedAuthorities = failureToReschedule.changedAuthorities;
+ newJob.changedUris = failureToReschedule.changedUris;
+ }
+ }
+
+ final class ObserverInstance extends ContentObserver {
+ final JobInfo.TriggerContentUri mUri;
+ final @UserIdInt int mUserId;
+ final ArraySet<JobInstance> mJobs = new ArraySet<>();
+
+ public ObserverInstance(Handler handler, JobInfo.TriggerContentUri uri,
+ @UserIdInt int userId) {
+ super(handler);
+ mUri = uri;
+ mUserId = userId;
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (DEBUG) {
+ Slog.i(TAG, "onChange(self=" + selfChange + ") for " + uri
+ + " when mUri=" + mUri + " mUserId=" + mUserId);
+ }
+ synchronized (mLock) {
+ final int N = mJobs.size();
+ for (int i=0; i<N; i++) {
+ JobInstance inst = mJobs.valueAt(i);
+ if (inst.mChangedUris == null) {
+ inst.mChangedUris = new ArraySet<>();
+ }
+ if (inst.mChangedUris.size() < MAX_URIS_REPORTED) {
+ inst.mChangedUris.add(uri);
+ }
+ if (inst.mChangedAuthorities == null) {
+ inst.mChangedAuthorities = new ArraySet<>();
+ }
+ inst.mChangedAuthorities.add(uri.getAuthority());
+ inst.scheduleLocked();
+ }
+ }
+ }
+ }
+
+ static final class TriggerRunnable implements Runnable {
+ final JobInstance mInstance;
+
+ TriggerRunnable(JobInstance instance) {
+ mInstance = instance;
+ }
+
+ @Override public void run() {
+ mInstance.trigger();
+ }
+ }
+
+ final class JobInstance {
+ final ArrayList<ObserverInstance> mMyObservers = new ArrayList<>();
+ final JobStatus mJobStatus;
+ final Runnable mExecuteRunner;
+ final Runnable mTimeoutRunner;
+ ArraySet<Uri> mChangedUris;
+ ArraySet<String> mChangedAuthorities;
+
+ boolean mTriggerPending;
+
+ // This constructor must be called with the master job scheduler lock held.
+ JobInstance(JobStatus jobStatus) {
+ mJobStatus = jobStatus;
+ mExecuteRunner = new TriggerRunnable(this);
+ mTimeoutRunner = new TriggerRunnable(this);
+ final JobInfo.TriggerContentUri[] uris = jobStatus.getJob().getTriggerContentUris();
+ final int sourceUserId = jobStatus.getSourceUserId();
+ ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser =
+ mObservers.get(sourceUserId);
+ if (observersOfUser == null) {
+ observersOfUser = new ArrayMap<>();
+ mObservers.put(sourceUserId, observersOfUser);
+ }
+ if (uris != null) {
+ for (JobInfo.TriggerContentUri uri : uris) {
+ ObserverInstance obs = observersOfUser.get(uri);
+ if (obs == null) {
+ obs = new ObserverInstance(mHandler, uri, jobStatus.getSourceUserId());
+ observersOfUser.put(uri, obs);
+ final boolean andDescendants = (uri.getFlags() &
+ JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0;
+ if (DEBUG) {
+ Slog.v(TAG, "New observer " + obs + " for " + uri.getUri()
+ + " andDescendants=" + andDescendants
+ + " sourceUserId=" + sourceUserId);
+ }
+ mContext.getContentResolver().registerContentObserver(
+ uri.getUri(),
+ andDescendants,
+ obs,
+ sourceUserId
+ );
+ } else {
+ if (DEBUG) {
+ final boolean andDescendants = (uri.getFlags() &
+ JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS) != 0;
+ Slog.v(TAG, "Reusing existing observer " + obs + " for " + uri.getUri()
+ + " andDescendants=" + andDescendants);
+ }
+ }
+ obs.mJobs.add(this);
+ mMyObservers.add(obs);
+ }
+ }
+ }
+
+ void trigger() {
+ boolean reportChange = false;
+ synchronized (mLock) {
+ if (mTriggerPending) {
+ if (mJobStatus.setContentTriggerConstraintSatisfied(true)) {
+ reportChange = true;
+ }
+ unscheduleLocked();
+ }
+ }
+ // Let the scheduler know that state has changed. This may or may not result in an
+ // execution.
+ if (reportChange) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+
+ void scheduleLocked() {
+ if (!mTriggerPending) {
+ mTriggerPending = true;
+ mHandler.postDelayed(mTimeoutRunner, mJobStatus.getTriggerContentMaxDelay());
+ }
+ mHandler.removeCallbacks(mExecuteRunner);
+ if (mChangedUris.size() >= URIS_URGENT_THRESHOLD) {
+ // If we start getting near the limit, GO NOW!
+ mHandler.post(mExecuteRunner);
+ } else {
+ mHandler.postDelayed(mExecuteRunner, mJobStatus.getTriggerContentUpdateDelay());
+ }
+ }
+
+ void unscheduleLocked() {
+ if (mTriggerPending) {
+ mHandler.removeCallbacks(mExecuteRunner);
+ mHandler.removeCallbacks(mTimeoutRunner);
+ mTriggerPending = false;
+ }
+ }
+
+ void detachLocked() {
+ final int N = mMyObservers.size();
+ for (int i=0; i<N; i++) {
+ final ObserverInstance obs = mMyObservers.get(i);
+ obs.mJobs.remove(this);
+ if (obs.mJobs.size() == 0) {
+ if (DEBUG) {
+ Slog.i(TAG, "Unregistering observer " + obs + " for " + obs.mUri.getUri());
+ }
+ mContext.getContentResolver().unregisterContentObserver(obs);
+ ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observerOfUser =
+ mObservers.get(obs.mUserId);
+ if (observerOfUser != null) {
+ observerOfUser.remove(obs.mUri);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(IndentingPrintWriter pw,
+ Predicate<JobStatus> predicate) {
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ JobStatus js = mTrackedTasks.valueAt(i);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ pw.print("#");
+ js.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, js.getSourceUid());
+ pw.println();
+ }
+ pw.println();
+
+ int N = mObservers.size();
+ if (N > 0) {
+ pw.println("Observers:");
+ pw.increaseIndent();
+ for (int userIdx = 0; userIdx < N; userIdx++) {
+ final int userId = mObservers.keyAt(userIdx);
+ ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser =
+ mObservers.get(userId);
+ int numbOfObserversPerUser = observersOfUser.size();
+ for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) {
+ ObserverInstance obs = observersOfUser.valueAt(observerIdx);
+ int M = obs.mJobs.size();
+ boolean shouldDump = false;
+ for (int j = 0; j < M; j++) {
+ JobInstance inst = obs.mJobs.valueAt(j);
+ if (predicate.test(inst.mJobStatus)) {
+ shouldDump = true;
+ break;
+ }
+ }
+ if (!shouldDump) {
+ continue;
+ }
+ JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx);
+ pw.print(trigger.getUri());
+ pw.print(" 0x");
+ pw.print(Integer.toHexString(trigger.getFlags()));
+ pw.print(" (");
+ pw.print(System.identityHashCode(obs));
+ pw.println("):");
+ pw.increaseIndent();
+ pw.println("Jobs:");
+ pw.increaseIndent();
+ for (int j = 0; j < M; j++) {
+ JobInstance inst = obs.mJobs.valueAt(j);
+ pw.print("#");
+ inst.mJobStatus.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, inst.mJobStatus.getSourceUid());
+ if (inst.mChangedAuthorities != null) {
+ pw.println(":");
+ pw.increaseIndent();
+ if (inst.mTriggerPending) {
+ pw.print("Trigger pending: update=");
+ TimeUtils.formatDuration(
+ inst.mJobStatus.getTriggerContentUpdateDelay(), pw);
+ pw.print(", max=");
+ TimeUtils.formatDuration(
+ inst.mJobStatus.getTriggerContentMaxDelay(), pw);
+ pw.println();
+ }
+ pw.println("Changed Authorities:");
+ for (int k = 0; k < inst.mChangedAuthorities.size(); k++) {
+ pw.println(inst.mChangedAuthorities.valueAt(k));
+ }
+ if (inst.mChangedUris != null) {
+ pw.println(" Changed URIs:");
+ for (int k = 0; k < inst.mChangedUris.size(); k++) {
+ pw.println(inst.mChangedUris.valueAt(k));
+ }
+ }
+ pw.decreaseIndent();
+ } else {
+ pw.println();
+ }
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ }
+ }
+ pw.decreaseIndent();
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.CONTENT_OBSERVER);
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ JobStatus js = mTrackedTasks.valueAt(i);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ final long jsToken =
+ proto.start(StateControllerProto.ContentObserverController.TRACKED_JOBS);
+ js.writeToShortProto(proto,
+ StateControllerProto.ContentObserverController.TrackedJob.INFO);
+ proto.write(StateControllerProto.ContentObserverController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.end(jsToken);
+ }
+
+ final int n = mObservers.size();
+ for (int userIdx = 0; userIdx < n; userIdx++) {
+ final long oToken =
+ proto.start(StateControllerProto.ContentObserverController.OBSERVERS);
+ final int userId = mObservers.keyAt(userIdx);
+
+ proto.write(StateControllerProto.ContentObserverController.Observer.USER_ID, userId);
+
+ ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser =
+ mObservers.get(userId);
+ int numbOfObserversPerUser = observersOfUser.size();
+ for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) {
+ ObserverInstance obs = observersOfUser.valueAt(observerIdx);
+ int m = obs.mJobs.size();
+ boolean shouldDump = false;
+ for (int j = 0; j < m; j++) {
+ JobInstance inst = obs.mJobs.valueAt(j);
+ if (predicate.test(inst.mJobStatus)) {
+ shouldDump = true;
+ break;
+ }
+ }
+ if (!shouldDump) {
+ continue;
+ }
+ final long tToken = proto.start(
+ StateControllerProto.ContentObserverController.Observer.TRIGGERS);
+
+ JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx);
+ Uri u = trigger.getUri();
+ if (u != null) {
+ proto.write(TriggerContentData.URI, u.toString());
+ }
+ proto.write(TriggerContentData.FLAGS, trigger.getFlags());
+
+ for (int j = 0; j < m; j++) {
+ final long jToken = proto.start(TriggerContentData.JOBS);
+ JobInstance inst = obs.mJobs.valueAt(j);
+
+ inst.mJobStatus.writeToShortProto(proto, TriggerContentData.JobInstance.INFO);
+ proto.write(TriggerContentData.JobInstance.SOURCE_UID,
+ inst.mJobStatus.getSourceUid());
+
+ if (inst.mChangedAuthorities == null) {
+ proto.end(jToken);
+ continue;
+ }
+ if (inst.mTriggerPending) {
+ proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_UPDATE_DELAY_MS,
+ inst.mJobStatus.getTriggerContentUpdateDelay());
+ proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_MAX_DELAY_MS,
+ inst.mJobStatus.getTriggerContentMaxDelay());
+ }
+ for (int k = 0; k < inst.mChangedAuthorities.size(); k++) {
+ proto.write(TriggerContentData.JobInstance.CHANGED_AUTHORITIES,
+ inst.mChangedAuthorities.valueAt(k));
+ }
+ if (inst.mChangedUris != null) {
+ for (int k = 0; k < inst.mChangedUris.size(); k++) {
+ u = inst.mChangedUris.valueAt(k);
+ if (u != null) {
+ proto.write(TriggerContentData.JobInstance.CHANGED_URIS,
+ u.toString());
+ }
+ }
+ }
+
+ proto.end(jToken);
+ }
+
+ proto.end(tToken);
+ }
+
+ proto.end(oToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
new file mode 100644
index 0000000..127a5c8
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2016 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 android.app.job.JobInfo;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseBooleanArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.DeviceIdleController;
+import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateControllerProto;
+import com.android.server.job.StateControllerProto.DeviceIdleJobsController.TrackedJob;
+
+import java.util.Arrays;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * When device is dozing, set constraint for all jobs, except whitelisted apps, as not satisfied.
+ * When device is not dozing, set constraint for all jobs as satisfied.
+ */
+public final class DeviceIdleJobsController extends StateController {
+ private static final String TAG = "JobScheduler.DeviceIdle";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final long BACKGROUND_JOBS_DELAY = 3000;
+
+ static final int PROCESS_BACKGROUND_JOBS = 1;
+
+ /**
+ * These are jobs added with a special flag to indicate that they should be exempted from doze
+ * when the app is temp whitelisted or in the foreground.
+ */
+ private final ArraySet<JobStatus> mAllowInIdleJobs;
+ private final SparseBooleanArray mForegroundUids;
+ private final DeviceIdleUpdateFunctor mDeviceIdleUpdateFunctor;
+ private final DeviceIdleJobsDelayHandler mHandler;
+ private final PowerManager mPowerManager;
+ private final DeviceIdleController.LocalService mLocalDeviceIdleController;
+
+ /**
+ * True when in device idle mode, so we don't want to schedule any jobs.
+ */
+ private boolean mDeviceIdleMode;
+ private int[] mDeviceIdleWhitelistAppIds;
+ private int[] mPowerSaveTempWhitelistAppIds;
+
+ // onReceive
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED:
+ case PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED:
+ updateIdleMode(mPowerManager != null && (mPowerManager.isDeviceIdleMode()
+ || mPowerManager.isLightDeviceIdleMode()));
+ break;
+ case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED:
+ synchronized (mLock) {
+ mDeviceIdleWhitelistAppIds =
+ mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds();
+ if (DEBUG) {
+ Slog.d(TAG, "Got whitelist "
+ + Arrays.toString(mDeviceIdleWhitelistAppIds));
+ }
+ }
+ break;
+ case PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED:
+ synchronized (mLock) {
+ mPowerSaveTempWhitelistAppIds =
+ mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds();
+ if (DEBUG) {
+ Slog.d(TAG, "Got temp whitelist "
+ + Arrays.toString(mPowerSaveTempWhitelistAppIds));
+ }
+ boolean changed = false;
+ for (int i = 0; i < mAllowInIdleJobs.size(); i++) {
+ changed |= updateTaskStateLocked(mAllowInIdleJobs.valueAt(i));
+ }
+ if (changed) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ break;
+ }
+ }
+ };
+
+ public DeviceIdleJobsController(JobSchedulerService service) {
+ super(service);
+
+ mHandler = new DeviceIdleJobsDelayHandler(mContext.getMainLooper());
+ // Register for device idle mode changes
+ mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ mLocalDeviceIdleController =
+ LocalServices.getService(DeviceIdleController.LocalService.class);
+ mDeviceIdleWhitelistAppIds = mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds();
+ mPowerSaveTempWhitelistAppIds =
+ mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds();
+ mDeviceIdleUpdateFunctor = new DeviceIdleUpdateFunctor();
+ mAllowInIdleJobs = new ArraySet<>();
+ mForegroundUids = new SparseBooleanArray();
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
+ filter.addAction(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED);
+ filter.addAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
+ filter.addAction(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED);
+ mContext.registerReceiverAsUser(
+ mBroadcastReceiver, UserHandle.ALL, filter, null, null);
+ }
+
+ void updateIdleMode(boolean enabled) {
+ boolean changed = false;
+ synchronized (mLock) {
+ if (mDeviceIdleMode != enabled) {
+ changed = true;
+ }
+ mDeviceIdleMode = enabled;
+ if (DEBUG) Slog.d(TAG, "mDeviceIdleMode=" + mDeviceIdleMode);
+ if (enabled) {
+ mHandler.removeMessages(PROCESS_BACKGROUND_JOBS);
+ mService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor);
+ } else {
+ // When coming out of doze, process all foreground uids immediately, while others
+ // will be processed after a delay of 3 seconds.
+ for (int i = 0; i < mForegroundUids.size(); i++) {
+ if (mForegroundUids.valueAt(i)) {
+ mService.getJobStore().forEachJobForSourceUid(
+ mForegroundUids.keyAt(i), mDeviceIdleUpdateFunctor);
+ }
+ }
+ mHandler.sendEmptyMessageDelayed(PROCESS_BACKGROUND_JOBS, BACKGROUND_JOBS_DELAY);
+ }
+ }
+ // Inform the job scheduler service about idle mode changes
+ if (changed) {
+ mStateChangedListener.onDeviceIdleStateChanged(enabled);
+ }
+ }
+
+ /**
+ * Called by jobscheduler service to report uid state changes between active and idle
+ */
+ public void setUidActiveLocked(int uid, boolean active) {
+ final boolean changed = (active != mForegroundUids.get(uid));
+ if (!changed) {
+ return;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "uid " + uid + " going " + (active ? "active" : "inactive"));
+ }
+ mForegroundUids.put(uid, active);
+ mDeviceIdleUpdateFunctor.mChanged = false;
+ mService.getJobStore().forEachJobForSourceUid(uid, mDeviceIdleUpdateFunctor);
+ if (mDeviceIdleUpdateFunctor.mChanged) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+
+ /**
+ * Checks if the given job's scheduling app id exists in the device idle user whitelist.
+ */
+ boolean isWhitelistedLocked(JobStatus job) {
+ return Arrays.binarySearch(mDeviceIdleWhitelistAppIds,
+ UserHandle.getAppId(job.getSourceUid())) >= 0;
+ }
+
+ /**
+ * Checks if the given job's scheduling app id exists in the device idle temp whitelist.
+ */
+ boolean isTempWhitelistedLocked(JobStatus job) {
+ return ArrayUtils.contains(mPowerSaveTempWhitelistAppIds,
+ UserHandle.getAppId(job.getSourceUid()));
+ }
+
+ private boolean updateTaskStateLocked(JobStatus task) {
+ final boolean allowInIdle = ((task.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0)
+ && (mForegroundUids.get(task.getSourceUid()) || isTempWhitelistedLocked(task));
+ final boolean whitelisted = isWhitelistedLocked(task);
+ final boolean enableTask = !mDeviceIdleMode || whitelisted || allowInIdle;
+ return task.setDeviceNotDozingConstraintSatisfied(enableTask, whitelisted);
+ }
+
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+ if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
+ mAllowInIdleJobs.add(jobStatus);
+ }
+ updateTaskStateLocked(jobStatus);
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+ boolean forUpdate) {
+ if ((jobStatus.getFlags()&JobInfo.FLAG_IMPORTANT_WHILE_FOREGROUND) != 0) {
+ mAllowInIdleJobs.remove(jobStatus);
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(final IndentingPrintWriter pw,
+ final Predicate<JobStatus> predicate) {
+ pw.println("Idle mode: " + mDeviceIdleMode);
+ pw.println();
+
+ mService.getJobStore().forEachJob(predicate, (jobStatus) -> {
+ pw.print("#");
+ jobStatus.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, jobStatus.getSourceUid());
+ pw.print(": ");
+ pw.print(jobStatus.getSourcePackageName());
+ pw.print((jobStatus.satisfiedConstraints
+ & JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0
+ ? " RUNNABLE" : " WAITING");
+ if (jobStatus.dozeWhitelisted) {
+ pw.print(" WHITELISTED");
+ }
+ if (mAllowInIdleJobs.contains(jobStatus)) {
+ pw.print(" ALLOWED_IN_DOZE");
+ }
+ pw.println();
+ });
+ }
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.DEVICE_IDLE);
+
+ proto.write(StateControllerProto.DeviceIdleJobsController.IS_DEVICE_IDLE_MODE,
+ mDeviceIdleMode);
+ mService.getJobStore().forEachJob(predicate, (jobStatus) -> {
+ final long jsToken =
+ proto.start(StateControllerProto.DeviceIdleJobsController.TRACKED_JOBS);
+
+ jobStatus.writeToShortProto(proto, TrackedJob.INFO);
+ proto.write(TrackedJob.SOURCE_UID, jobStatus.getSourceUid());
+ proto.write(TrackedJob.SOURCE_PACKAGE_NAME, jobStatus.getSourcePackageName());
+ proto.write(TrackedJob.ARE_CONSTRAINTS_SATISFIED,
+ (jobStatus.satisfiedConstraints &
+ JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0);
+ proto.write(TrackedJob.IS_DOZE_WHITELISTED, jobStatus.dozeWhitelisted);
+ proto.write(TrackedJob.IS_ALLOWED_IN_DOZE, mAllowInIdleJobs.contains(jobStatus));
+
+ proto.end(jsToken);
+ });
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+
+ final class DeviceIdleUpdateFunctor implements Consumer<JobStatus> {
+ boolean mChanged;
+
+ @Override
+ public void accept(JobStatus jobStatus) {
+ mChanged |= updateTaskStateLocked(jobStatus);
+ }
+ }
+
+ final class DeviceIdleJobsDelayHandler extends Handler {
+ public DeviceIdleJobsDelayHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case PROCESS_BACKGROUND_JOBS:
+ // Just process all the jobs, the ones in foreground should already be running.
+ synchronized (mLock) {
+ mDeviceIdleUpdateFunctor.mChanged = false;
+ mService.getJobStore().forEachJob(mDeviceIdleUpdateFunctor);
+ if (mDeviceIdleUpdateFunctor.mChanged) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
new file mode 100644
index 0000000..e3c311f
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2014 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 android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateControllerProto;
+import com.android.server.job.controllers.idle.CarIdlenessTracker;
+import com.android.server.job.controllers.idle.DeviceIdlenessTracker;
+import com.android.server.job.controllers.idle.IdlenessListener;
+import com.android.server.job.controllers.idle.IdlenessTracker;
+
+import java.util.function.Predicate;
+
+public final class IdleController extends StateController implements IdlenessListener {
+ private static final String TAG = "JobScheduler.IdleController";
+ // Policy: we decide that we're "idle" if the device has been unused /
+ // screen off or dreaming or wireless charging dock idle for at least this long
+ final ArraySet<JobStatus> mTrackedTasks = new ArraySet<>();
+ IdlenessTracker mIdleTracker;
+
+ public IdleController(JobSchedulerService service) {
+ super(service);
+ initIdleStateTracking(mContext);
+ }
+
+ /**
+ * StateController interface
+ */
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
+ if (taskStatus.hasIdleConstraint()) {
+ mTrackedTasks.add(taskStatus);
+ taskStatus.setTrackingController(JobStatus.TRACKING_IDLE);
+ taskStatus.setIdleConstraintSatisfied(mIdleTracker.isIdle());
+ }
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
+ boolean forUpdate) {
+ if (taskStatus.clearTrackingController(JobStatus.TRACKING_IDLE)) {
+ mTrackedTasks.remove(taskStatus);
+ }
+ }
+
+ /**
+ * State-change notifications from the idleness tracker
+ */
+ @Override
+ public void reportNewIdleState(boolean isIdle) {
+ synchronized (mLock) {
+ for (int i = mTrackedTasks.size()-1; i >= 0; i--) {
+ mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(isIdle);
+ }
+ }
+ mStateChangedListener.onControllerStateChanged();
+ }
+
+ /**
+ * Idle state tracking, and messaging with the task manager when
+ * significant state changes occur
+ */
+ private void initIdleStateTracking(Context ctx) {
+ final boolean isCar = mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_AUTOMOTIVE);
+ if (isCar) {
+ mIdleTracker = new CarIdlenessTracker();
+ } else {
+ mIdleTracker = new DeviceIdlenessTracker();
+ }
+ mIdleTracker.startTracking(ctx, this);
+ }
+
+ @Override
+ public void dumpControllerStateLocked(IndentingPrintWriter pw,
+ Predicate<JobStatus> predicate) {
+ pw.println("Currently idle: " + mIdleTracker.isIdle());
+ pw.println("Idleness tracker:"); mIdleTracker.dump(pw);
+ pw.println();
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ final JobStatus js = mTrackedTasks.valueAt(i);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ pw.print("#");
+ js.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, js.getSourceUid());
+ pw.println();
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.IDLE);
+
+ proto.write(StateControllerProto.IdleController.IS_IDLE, mIdleTracker.isIdle());
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ final JobStatus js = mTrackedTasks.valueAt(i);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ final long jsToken = proto.start(StateControllerProto.IdleController.TRACKED_JOBS);
+ js.writeToShortProto(proto, StateControllerProto.IdleController.TrackedJob.INFO);
+ proto.write(StateControllerProto.IdleController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.end(jsToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+}
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
new file mode 100644
index 0000000..6f2b334
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -0,0 +1,1861 @@
+/*
+ * Copyright (C) 2014 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.server.job.JobSchedulerService.sElapsedRealtimeClock;
+
+import android.app.AppGlobals;
+import android.app.IActivityManager;
+import android.app.job.JobInfo;
+import android.app.job.JobWorkItem;
+import android.content.ClipData;
+import android.content.ComponentName;
+import android.net.Network;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.text.format.TimeMigrationUtils;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.StatsLog;
+import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.server.LocalServices;
+import com.android.server.job.GrantedUriPermissions;
+import com.android.server.job.JobSchedulerInternal;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobServerProtoEnums;
+import com.android.server.job.JobStatusDumpProto;
+import com.android.server.job.JobStatusShortInfoProto;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.function.Predicate;
+
+/**
+ * Uniquely identifies a job internally.
+ * Created from the public {@link android.app.job.JobInfo} object when it lands on the scheduler.
+ * Contains current state of the requirements of the job, as well as a function to evaluate
+ * whether it's ready to run.
+ * This object is shared among the various controllers - hence why the different fields are atomic.
+ * This isn't strictly necessary because each controller is only interested in a specific field,
+ * and the receivers that are listening for global state change will all run on the main looper,
+ * but we don't enforce that so this is safer.
+ *
+ * Test: atest com.android.server.job.controllers.JobStatusTest
+ * @hide
+ */
+public final class JobStatus {
+ static final String TAG = "JobSchedulerService";
+ static final boolean DEBUG = JobSchedulerService.DEBUG;
+
+ public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
+ public static final long NO_EARLIEST_RUNTIME = 0L;
+
+ static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0
+ static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE; // 1 << 2
+ static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
+ static final int CONSTRAINT_STORAGE_NOT_LOW = JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3
+ static final int CONSTRAINT_TIMING_DELAY = 1<<31;
+ static final int CONSTRAINT_DEADLINE = 1<<30;
+ static final int CONSTRAINT_CONNECTIVITY = 1<<28;
+ static final int CONSTRAINT_CONTENT_TRIGGER = 1<<26;
+ static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit constraint
+ static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24; // Implicit constraint
+ static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit constraint
+
+ /**
+ * The constraints that we want to log to statsd.
+ *
+ * Constraints that can be inferred from other atoms have been excluded to avoid logging too
+ * much information and to reduce redundancy:
+ *
+ * * CONSTRAINT_CHARGING can be inferred with PluggedStateChanged (Atom #32)
+ * * CONSTRAINT_BATTERY_NOT_LOW can be inferred with BatteryLevelChanged (Atom #30)
+ * * CONSTRAINT_CONNECTIVITY can be partially inferred with ConnectivityStateChanged
+ * (Atom #98) and BatterySaverModeStateChanged (Atom #20).
+ * * CONSTRAINT_DEVICE_NOT_DOZING can be mostly inferred with DeviceIdleModeStateChanged
+ * (Atom #21)
+ * * CONSTRAINT_BACKGROUND_NOT_RESTRICTED can be inferred with BatterySaverModeStateChanged
+ * (Atom #20)
+ */
+ private static final int STATSD_CONSTRAINTS_TO_LOG = CONSTRAINT_CONTENT_TRIGGER
+ | CONSTRAINT_DEADLINE
+ | CONSTRAINT_IDLE
+ | CONSTRAINT_STORAGE_NOT_LOW
+ | CONSTRAINT_TIMING_DELAY
+ | CONSTRAINT_WITHIN_QUOTA;
+
+ // TODO(b/129954980)
+ private static final boolean STATS_LOG_ENABLED = false;
+
+ // Soft override: ignore constraints like time that don't affect API availability
+ public static final int OVERRIDE_SOFT = 1;
+ // Full override: ignore all constraints including API-affecting like connectivity
+ public static final int OVERRIDE_FULL = 2;
+
+ /** If not specified, trigger update delay is 10 seconds. */
+ public static final long DEFAULT_TRIGGER_UPDATE_DELAY = 10*1000;
+
+ /** The minimum possible update delay is 1/2 second. */
+ public static final long MIN_TRIGGER_UPDATE_DELAY = 500;
+
+ /** If not specified, trigger maxumum delay is 2 minutes. */
+ public static final long DEFAULT_TRIGGER_MAX_DELAY = 2*60*1000;
+
+ /** The minimum possible update delay is 1 second. */
+ public static final long MIN_TRIGGER_MAX_DELAY = 1000;
+
+ final JobInfo job;
+ /**
+ * Uid of the package requesting this job. This can differ from the "source"
+ * uid when the job was scheduled on the app's behalf, such as with the jobs
+ * that underly Sync Manager operation.
+ */
+ final int callingUid;
+ final String batteryName;
+
+ /**
+ * Identity of the app in which the job is hosted.
+ */
+ final String sourcePackageName;
+ final int sourceUserId;
+ final int sourceUid;
+ final String sourceTag;
+
+ final String tag;
+
+ private GrantedUriPermissions uriPerms;
+ private boolean prepared;
+
+ static final boolean DEBUG_PREPARE = true;
+ private Throwable unpreparedPoint = null;
+
+ /**
+ * Earliest point in the future at which this job will be eligible to run. A value of 0
+ * indicates there is no delay constraint. See {@link #hasTimingDelayConstraint()}.
+ */
+ private final long earliestRunTimeElapsedMillis;
+ /**
+ * Latest point in the future at which this job must be run. A value of {@link Long#MAX_VALUE}
+ * indicates there is no deadline constraint. See {@link #hasDeadlineConstraint()}.
+ */
+ private final long latestRunTimeElapsedMillis;
+
+ /**
+ * Valid only for periodic jobs. The original latest point in the future at which this
+ * job was expected to run.
+ */
+ private long mOriginalLatestRunTimeElapsedMillis;
+
+ /** How many times this job has failed, used to compute back-off. */
+ private final int numFailures;
+
+ /**
+ * Current standby heartbeat when this job was scheduled or last ran. Used to
+ * pin the runnability check regardless of the job's app moving between buckets.
+ */
+ private final long baseHeartbeat;
+
+ /**
+ * Which app standby bucket this job's app is in. Updated when the app is moved to a
+ * different bucket.
+ */
+ private int standbyBucket;
+
+ /**
+ * Debugging: timestamp if we ever defer this job based on standby bucketing, this
+ * is when we did so.
+ */
+ private long whenStandbyDeferred;
+
+ /** The first time this job was force batched. */
+ private long mFirstForceBatchedTimeElapsed;
+
+ // Constraints.
+ final int requiredConstraints;
+ private final int mRequiredConstraintsOfInterest;
+ int satisfiedConstraints = 0;
+ private int mSatisfiedConstraintsOfInterest = 0;
+
+ // Set to true if doze constraint was satisfied due to app being whitelisted.
+ public boolean dozeWhitelisted;
+
+ // Set to true when the app is "active" per AppStateTracker
+ public boolean uidActive;
+
+ /**
+ * Flag for {@link #trackingControllers}: the battery controller is currently tracking this job.
+ */
+ public static final int TRACKING_BATTERY = 1<<0;
+ /**
+ * Flag for {@link #trackingControllers}: the network connectivity controller is currently
+ * tracking this job.
+ */
+ public static final int TRACKING_CONNECTIVITY = 1<<1;
+ /**
+ * Flag for {@link #trackingControllers}: the content observer controller is currently
+ * tracking this job.
+ */
+ public static final int TRACKING_CONTENT = 1<<2;
+ /**
+ * Flag for {@link #trackingControllers}: the idle controller is currently tracking this job.
+ */
+ public static final int TRACKING_IDLE = 1<<3;
+ /**
+ * Flag for {@link #trackingControllers}: the storage controller is currently tracking this job.
+ */
+ public static final int TRACKING_STORAGE = 1<<4;
+ /**
+ * Flag for {@link #trackingControllers}: the time controller is currently tracking this job.
+ */
+ public static final int TRACKING_TIME = 1<<5;
+ /**
+ * Flag for {@link #trackingControllers}: the quota controller is currently tracking this job.
+ */
+ public static final int TRACKING_QUOTA = 1 << 6;
+
+ /**
+ * Bit mask of controllers that are currently tracking the job.
+ */
+ private int trackingControllers;
+
+ /**
+ * Flag for {@link #mInternalFlags}: this job was scheduled when the app that owns the job
+ * service (not necessarily the caller) was in the foreground and the job has no time
+ * constraints, which makes it exempted from the battery saver job restriction.
+ *
+ * @hide
+ */
+ public static final int INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION = 1 << 0;
+
+ /**
+ * Versatile, persistable flags for a job that's updated within the system server,
+ * as opposed to {@link JobInfo#flags} that's set by callers.
+ */
+ private int mInternalFlags;
+
+ // These are filled in by controllers when preparing for execution.
+ public ArraySet<Uri> changedUris;
+ public ArraySet<String> changedAuthorities;
+ public Network network;
+
+ public int lastEvaluatedPriority;
+
+ // If non-null, this is work that has been enqueued for the job.
+ public ArrayList<JobWorkItem> pendingWork;
+
+ // If non-null, this is work that is currently being executed.
+ public ArrayList<JobWorkItem> executingWork;
+
+ public int nextPendingWorkId = 1;
+
+ // Used by shell commands
+ public int overrideState = 0;
+
+ // When this job was enqueued, for ordering. (in elapsedRealtimeMillis)
+ public long enqueueTime;
+
+ // Metrics about queue latency. (in uptimeMillis)
+ public long madePending;
+ public long madeActive;
+
+ /**
+ * Last time a job finished successfully for a periodic job, in the currentTimeMillis time,
+ * for dumpsys.
+ */
+ private long mLastSuccessfulRunTime;
+
+ /**
+ * Last time a job finished unsuccessfully, in the currentTimeMillis time, for dumpsys.
+ */
+ private long mLastFailedRunTime;
+
+ /**
+ * Transient: when a job is inflated from disk before we have a reliable RTC clock time,
+ * we retain the canonical (delay, deadline) scheduling tuple read out of the persistent
+ * store in UTC so that we can fix up the job's scheduling criteria once we get a good
+ * wall-clock time. If we have to persist the job again before the clock has been updated,
+ * we record these times again rather than calculating based on the earliest/latest elapsed
+ * time base figures.
+ *
+ * 'first' is the earliest/delay time, and 'second' is the latest/deadline time.
+ */
+ private Pair<Long, Long> mPersistedUtcTimes;
+
+ /**
+ * For use only by ContentObserverController: state it is maintaining about content URIs
+ * being observed.
+ */
+ ContentObserverController.JobInstance contentObserverJobInstance;
+
+ private long mTotalNetworkDownloadBytes = JobInfo.NETWORK_BYTES_UNKNOWN;
+ private long mTotalNetworkUploadBytes = JobInfo.NETWORK_BYTES_UNKNOWN;
+
+ /////// Booleans that track if a job is ready to run. They should be updated whenever dependent
+ /////// states change.
+
+ /**
+ * The deadline for the job has passed. This is only good for non-periodic jobs. A periodic job
+ * should only run if its constraints are satisfied.
+ * Computed as: NOT periodic AND has deadline constraint AND deadline constraint satisfied.
+ */
+ private boolean mReadyDeadlineSatisfied;
+
+ /**
+ * The device isn't Dozing or this job will be in the foreground. This implicit constraint must
+ * be satisfied.
+ */
+ private boolean mReadyNotDozing;
+
+ /**
+ * The job is not restricted from running in the background (due to Battery Saver). This
+ * implicit constraint must be satisfied.
+ */
+ private boolean mReadyNotRestrictedInBg;
+
+ /** The job is within its quota based on its standby bucket. */
+ private boolean mReadyWithinQuota;
+
+ /** Provide a handle to the service that this job will be run on. */
+ public int getServiceToken() {
+ return callingUid;
+ }
+
+ /**
+ * Core constructor for JobStatus instances. All other ctors funnel down to this one.
+ *
+ * @param job The actual requested parameters for the job
+ * @param callingUid Identity of the app that is scheduling the job. This may not be the
+ * app in which the job is implemented; such as with sync jobs.
+ * @param sourcePackageName The package name of the app in which the job will run.
+ * @param sourceUserId The user in which the job will run
+ * @param standbyBucket The standby bucket that the source package is currently assigned to,
+ * cached here for speed of handling during runnability evaluations (and updated when bucket
+ * assignments are changed)
+ * @param heartbeat Timestamp of when the job was created, in the standby-related
+ * timebase.
+ * @param tag A string associated with the job for debugging/logging purposes.
+ * @param numFailures Count of how many times this job has requested a reschedule because
+ * its work was not yet finished.
+ * @param earliestRunTimeElapsedMillis Milestone: earliest point in time at which the job
+ * is to be considered runnable
+ * @param latestRunTimeElapsedMillis Milestone: point in time at which the job will be
+ * considered overdue
+ * @param lastSuccessfulRunTime When did we last run this job to completion?
+ * @param lastFailedRunTime When did we last run this job only to have it stop incomplete?
+ * @param internalFlags Non-API property flags about this job
+ */
+ private JobStatus(JobInfo job, int callingUid, String sourcePackageName,
+ int sourceUserId, int standbyBucket, long heartbeat, String tag, int numFailures,
+ long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
+ long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags) {
+ this.job = job;
+ this.callingUid = callingUid;
+ this.standbyBucket = standbyBucket;
+ this.baseHeartbeat = heartbeat;
+
+ int tempSourceUid = -1;
+ if (sourceUserId != -1 && sourcePackageName != null) {
+ try {
+ tempSourceUid = AppGlobals.getPackageManager().getPackageUid(sourcePackageName, 0,
+ sourceUserId);
+ } catch (RemoteException ex) {
+ // Can't happen, PackageManager runs in the same process.
+ }
+ }
+ if (tempSourceUid == -1) {
+ this.sourceUid = callingUid;
+ this.sourceUserId = UserHandle.getUserId(callingUid);
+ this.sourcePackageName = job.getService().getPackageName();
+ this.sourceTag = null;
+ } else {
+ this.sourceUid = tempSourceUid;
+ this.sourceUserId = sourceUserId;
+ this.sourcePackageName = sourcePackageName;
+ this.sourceTag = tag;
+ }
+
+ this.batteryName = this.sourceTag != null
+ ? this.sourceTag + ":" + job.getService().getPackageName()
+ : job.getService().flattenToShortString();
+ this.tag = "*job*/" + this.batteryName;
+
+ this.earliestRunTimeElapsedMillis = earliestRunTimeElapsedMillis;
+ this.latestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
+ this.mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsedMillis;
+ this.numFailures = numFailures;
+
+ int requiredConstraints = job.getConstraintFlags();
+ if (job.getRequiredNetwork() != null) {
+ requiredConstraints |= CONSTRAINT_CONNECTIVITY;
+ }
+ if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME) {
+ requiredConstraints |= CONSTRAINT_TIMING_DELAY;
+ }
+ if (latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) {
+ requiredConstraints |= CONSTRAINT_DEADLINE;
+ }
+ if (job.getTriggerContentUris() != null) {
+ requiredConstraints |= CONSTRAINT_CONTENT_TRIGGER;
+ }
+ this.requiredConstraints = requiredConstraints;
+ mRequiredConstraintsOfInterest = requiredConstraints & CONSTRAINTS_OF_INTEREST;
+ mReadyNotDozing = (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
+
+ mLastSuccessfulRunTime = lastSuccessfulRunTime;
+ mLastFailedRunTime = lastFailedRunTime;
+
+ mInternalFlags = internalFlags;
+
+ updateEstimatedNetworkBytesLocked();
+
+ if (job.getRequiredNetwork() != null) {
+ // Later, when we check if a given network satisfies the required
+ // network, we need to know the UID that is requesting it, so push
+ // our source UID into place.
+ job.getRequiredNetwork().networkCapabilities.setSingleUid(this.sourceUid);
+ }
+ }
+
+ /** Copy constructor: used specifically when cloning JobStatus objects for persistence,
+ * so we preserve RTC window bounds if the source object has them. */
+ public JobStatus(JobStatus jobStatus) {
+ this(jobStatus.getJob(), jobStatus.getUid(),
+ jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(),
+ jobStatus.getStandbyBucket(), jobStatus.getBaseHeartbeat(),
+ jobStatus.getSourceTag(), jobStatus.getNumFailures(),
+ jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(),
+ jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(),
+ jobStatus.getInternalFlags());
+ mPersistedUtcTimes = jobStatus.mPersistedUtcTimes;
+ if (jobStatus.mPersistedUtcTimes != null) {
+ if (DEBUG) {
+ Slog.i(TAG, "Cloning job with persisted run times", new RuntimeException("here"));
+ }
+ }
+ }
+
+ /**
+ * Create a new JobStatus that was loaded from disk. We ignore the provided
+ * {@link android.app.job.JobInfo} time criteria because we can load a persisted periodic job
+ * from the {@link com.android.server.job.JobStore} and still want to respect its
+ * wallclock runtime rather than resetting it on every boot.
+ * We consider a freshly loaded job to no longer be in back-off, and the associated
+ * standby bucket is whatever the OS thinks it should be at this moment.
+ */
+ public JobStatus(JobInfo job, int callingUid, String sourcePkgName, int sourceUserId,
+ int standbyBucket, long baseHeartbeat, String sourceTag,
+ long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
+ long lastSuccessfulRunTime, long lastFailedRunTime,
+ Pair<Long, Long> persistedExecutionTimesUTC,
+ int innerFlags) {
+ this(job, callingUid, sourcePkgName, sourceUserId,
+ standbyBucket, baseHeartbeat,
+ sourceTag, 0,
+ earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
+ lastSuccessfulRunTime, lastFailedRunTime, innerFlags);
+
+ // Only during initial inflation do we record the UTC-timebase execution bounds
+ // read from the persistent store. If we ever have to recreate the JobStatus on
+ // the fly, it means we're rescheduling the job; and this means that the calculated
+ // elapsed timebase bounds intrinsically become correct.
+ this.mPersistedUtcTimes = persistedExecutionTimesUTC;
+ if (persistedExecutionTimesUTC != null) {
+ if (DEBUG) {
+ Slog.i(TAG, "+ restored job with RTC times because of bad boot clock");
+ }
+ }
+ }
+
+ /** Create a new job to be rescheduled with the provided parameters. */
+ public JobStatus(JobStatus rescheduling, long newBaseHeartbeat,
+ long newEarliestRuntimeElapsedMillis,
+ long newLatestRuntimeElapsedMillis, int backoffAttempt,
+ long lastSuccessfulRunTime, long lastFailedRunTime) {
+ this(rescheduling.job, rescheduling.getUid(),
+ rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(),
+ rescheduling.getStandbyBucket(), newBaseHeartbeat,
+ rescheduling.getSourceTag(), backoffAttempt, newEarliestRuntimeElapsedMillis,
+ newLatestRuntimeElapsedMillis,
+ lastSuccessfulRunTime, lastFailedRunTime, rescheduling.getInternalFlags());
+ }
+
+ /**
+ * Create a newly scheduled job.
+ * @param callingUid Uid of the package that scheduled this job.
+ * @param sourcePkg Package name of the app that will actually run the job. Null indicates
+ * that the calling package is the source.
+ * @param sourceUserId User id for whom this job is scheduled. -1 indicates this is same as the
+ * caller.
+ */
+ public static JobStatus createFromJobInfo(JobInfo job, int callingUid, String sourcePkg,
+ int sourceUserId, String tag) {
+ final long elapsedNow = sElapsedRealtimeClock.millis();
+ final long earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis;
+ if (job.isPeriodic()) {
+ // Make sure period is in the interval [min_possible_period, max_possible_period].
+ final long period = Math.max(JobInfo.getMinPeriodMillis(),
+ Math.min(JobSchedulerService.MAX_ALLOWED_PERIOD_MS, job.getIntervalMillis()));
+ latestRunTimeElapsedMillis = elapsedNow + period;
+ earliestRunTimeElapsedMillis = latestRunTimeElapsedMillis
+ // Make sure flex is in the interval [min_possible_flex, period].
+ - Math.max(JobInfo.getMinFlexMillis(), Math.min(period, job.getFlexMillis()));
+ } else {
+ earliestRunTimeElapsedMillis = job.hasEarlyConstraint() ?
+ elapsedNow + job.getMinLatencyMillis() : NO_EARLIEST_RUNTIME;
+ latestRunTimeElapsedMillis = job.hasLateConstraint() ?
+ elapsedNow + job.getMaxExecutionDelayMillis() : NO_LATEST_RUNTIME;
+ }
+ String jobPackage = (sourcePkg != null) ? sourcePkg : job.getService().getPackageName();
+
+ int standbyBucket = JobSchedulerService.standbyBucketForPackage(jobPackage,
+ sourceUserId, elapsedNow);
+ JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class);
+ long currentHeartbeat = js != null
+ ? js.baseHeartbeatForApp(jobPackage, sourceUserId, standbyBucket)
+ : 0;
+ return new JobStatus(job, callingUid, sourcePkg, sourceUserId,
+ standbyBucket, currentHeartbeat, tag, 0,
+ earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
+ 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
+ /*innerFlags=*/ 0);
+ }
+
+ public void enqueueWorkLocked(IActivityManager am, JobWorkItem work) {
+ if (pendingWork == null) {
+ pendingWork = new ArrayList<>();
+ }
+ work.setWorkId(nextPendingWorkId);
+ nextPendingWorkId++;
+ if (work.getIntent() != null
+ && GrantedUriPermissions.checkGrantFlags(work.getIntent().getFlags())) {
+ work.setGrants(GrantedUriPermissions.createFromIntent(am, work.getIntent(), sourceUid,
+ sourcePackageName, sourceUserId, toShortString()));
+ }
+ pendingWork.add(work);
+ updateEstimatedNetworkBytesLocked();
+ }
+
+ public JobWorkItem dequeueWorkLocked() {
+ if (pendingWork != null && pendingWork.size() > 0) {
+ JobWorkItem work = pendingWork.remove(0);
+ if (work != null) {
+ if (executingWork == null) {
+ executingWork = new ArrayList<>();
+ }
+ executingWork.add(work);
+ work.bumpDeliveryCount();
+ }
+ updateEstimatedNetworkBytesLocked();
+ return work;
+ }
+ return null;
+ }
+
+ public boolean hasWorkLocked() {
+ return (pendingWork != null && pendingWork.size() > 0) || hasExecutingWorkLocked();
+ }
+
+ public boolean hasExecutingWorkLocked() {
+ return executingWork != null && executingWork.size() > 0;
+ }
+
+ private static void ungrantWorkItem(IActivityManager am, JobWorkItem work) {
+ if (work.getGrants() != null) {
+ ((GrantedUriPermissions)work.getGrants()).revoke(am);
+ }
+ }
+
+ public boolean completeWorkLocked(IActivityManager am, int workId) {
+ if (executingWork != null) {
+ final int N = executingWork.size();
+ for (int i = 0; i < N; i++) {
+ JobWorkItem work = executingWork.get(i);
+ if (work.getWorkId() == workId) {
+ executingWork.remove(i);
+ ungrantWorkItem(am, work);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static void ungrantWorkList(IActivityManager am, ArrayList<JobWorkItem> list) {
+ if (list != null) {
+ final int N = list.size();
+ for (int i = 0; i < N; i++) {
+ ungrantWorkItem(am, list.get(i));
+ }
+ }
+ }
+
+ public void stopTrackingJobLocked(IActivityManager am, JobStatus incomingJob) {
+ if (incomingJob != null) {
+ // We are replacing with a new job -- transfer the work! We do any executing
+ // work first, since that was originally at the front of the pending work.
+ if (executingWork != null && executingWork.size() > 0) {
+ incomingJob.pendingWork = executingWork;
+ }
+ if (incomingJob.pendingWork == null) {
+ incomingJob.pendingWork = pendingWork;
+ } else if (pendingWork != null && pendingWork.size() > 0) {
+ incomingJob.pendingWork.addAll(pendingWork);
+ }
+ pendingWork = null;
+ executingWork = null;
+ incomingJob.nextPendingWorkId = nextPendingWorkId;
+ incomingJob.updateEstimatedNetworkBytesLocked();
+ } else {
+ // We are completely stopping the job... need to clean up work.
+ ungrantWorkList(am, pendingWork);
+ pendingWork = null;
+ ungrantWorkList(am, executingWork);
+ executingWork = null;
+ }
+ updateEstimatedNetworkBytesLocked();
+ }
+
+ public void prepareLocked(IActivityManager am) {
+ if (prepared) {
+ Slog.wtf(TAG, "Already prepared: " + this);
+ return;
+ }
+ prepared = true;
+ if (DEBUG_PREPARE) {
+ unpreparedPoint = null;
+ }
+ final ClipData clip = job.getClipData();
+ if (clip != null) {
+ uriPerms = GrantedUriPermissions.createFromClip(am, clip, sourceUid, sourcePackageName,
+ sourceUserId, job.getClipGrantFlags(), toShortString());
+ }
+ }
+
+ public void unprepareLocked(IActivityManager am) {
+ if (!prepared) {
+ Slog.wtf(TAG, "Hasn't been prepared: " + this);
+ if (DEBUG_PREPARE && unpreparedPoint != null) {
+ Slog.e(TAG, "Was already unprepared at ", unpreparedPoint);
+ }
+ return;
+ }
+ prepared = false;
+ if (DEBUG_PREPARE) {
+ unpreparedPoint = new Throwable().fillInStackTrace();
+ }
+ if (uriPerms != null) {
+ uriPerms.revoke(am);
+ uriPerms = null;
+ }
+ }
+
+ public boolean isPreparedLocked() {
+ return prepared;
+ }
+
+ public JobInfo getJob() {
+ return job;
+ }
+
+ public int getJobId() {
+ return job.getId();
+ }
+
+ public void printUniqueId(PrintWriter pw) {
+ UserHandle.formatUid(pw, callingUid);
+ pw.print("/");
+ pw.print(job.getId());
+ }
+
+ public int getNumFailures() {
+ return numFailures;
+ }
+
+ public ComponentName getServiceComponent() {
+ return job.getService();
+ }
+
+ public String getSourcePackageName() {
+ return sourcePackageName;
+ }
+
+ public int getSourceUid() {
+ return sourceUid;
+ }
+
+ public int getSourceUserId() {
+ return sourceUserId;
+ }
+
+ public int getUserId() {
+ return UserHandle.getUserId(callingUid);
+ }
+
+ public int getStandbyBucket() {
+ return standbyBucket;
+ }
+
+ public long getBaseHeartbeat() {
+ return baseHeartbeat;
+ }
+
+ public void setStandbyBucket(int newBucket) {
+ standbyBucket = newBucket;
+ }
+
+ // Called only by the standby monitoring code
+ public long getWhenStandbyDeferred() {
+ return whenStandbyDeferred;
+ }
+
+ // Called only by the standby monitoring code
+ public void setWhenStandbyDeferred(long now) {
+ whenStandbyDeferred = now;
+ }
+
+ /**
+ * Returns the first time this job was force batched, in the elapsed realtime timebase. Will be
+ * 0 if this job was never force batched.
+ */
+ public long getFirstForceBatchedTimeElapsed() {
+ return mFirstForceBatchedTimeElapsed;
+ }
+
+ public void setFirstForceBatchedTimeElapsed(long now) {
+ mFirstForceBatchedTimeElapsed = now;
+ }
+
+ public String getSourceTag() {
+ return sourceTag;
+ }
+
+ public int getUid() {
+ return callingUid;
+ }
+
+ public String getBatteryName() {
+ return batteryName;
+ }
+
+ public String getTag() {
+ return tag;
+ }
+
+ public int getPriority() {
+ return job.getPriority();
+ }
+
+ public int getFlags() {
+ return job.getFlags();
+ }
+
+ public int getInternalFlags() {
+ return mInternalFlags;
+ }
+
+ public void addInternalFlags(int flags) {
+ mInternalFlags |= flags;
+ }
+
+ public int getSatisfiedConstraintFlags() {
+ return satisfiedConstraints;
+ }
+
+ public void maybeAddForegroundExemption(Predicate<Integer> uidForegroundChecker) {
+ // Jobs with time constraints shouldn't be exempted.
+ if (job.hasEarlyConstraint() || job.hasLateConstraint()) {
+ return;
+ }
+ // Already exempted, skip the foreground check.
+ if ((mInternalFlags & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0) {
+ return;
+ }
+ if (uidForegroundChecker.test(getSourceUid())) {
+ addInternalFlags(INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION);
+ }
+ }
+
+ private void updateEstimatedNetworkBytesLocked() {
+ mTotalNetworkDownloadBytes = job.getEstimatedNetworkDownloadBytes();
+ mTotalNetworkUploadBytes = job.getEstimatedNetworkUploadBytes();
+
+ if (pendingWork != null) {
+ for (int i = 0; i < pendingWork.size(); i++) {
+ if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ // If any component of the job has unknown usage, we don't have a
+ // complete picture of what data will be used, and we have to treat the
+ // entire up/download as unknown.
+ long downloadBytes = pendingWork.get(i).getEstimatedNetworkDownloadBytes();
+ if (downloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ mTotalNetworkDownloadBytes += downloadBytes;
+ }
+ }
+ if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ // If any component of the job has unknown usage, we don't have a
+ // complete picture of what data will be used, and we have to treat the
+ // entire up/download as unknown.
+ long uploadBytes = pendingWork.get(i).getEstimatedNetworkUploadBytes();
+ if (uploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ mTotalNetworkUploadBytes += uploadBytes;
+ }
+ }
+ }
+ }
+ }
+
+ public long getEstimatedNetworkDownloadBytes() {
+ return mTotalNetworkDownloadBytes;
+ }
+
+ public long getEstimatedNetworkUploadBytes() {
+ return mTotalNetworkUploadBytes;
+ }
+
+ /** Does this job have any sort of networking constraint? */
+ public boolean hasConnectivityConstraint() {
+ return (requiredConstraints&CONSTRAINT_CONNECTIVITY) != 0;
+ }
+
+ public boolean hasChargingConstraint() {
+ return (requiredConstraints&CONSTRAINT_CHARGING) != 0;
+ }
+
+ public boolean hasBatteryNotLowConstraint() {
+ return (requiredConstraints&CONSTRAINT_BATTERY_NOT_LOW) != 0;
+ }
+
+ public boolean hasPowerConstraint() {
+ return (requiredConstraints&(CONSTRAINT_CHARGING|CONSTRAINT_BATTERY_NOT_LOW)) != 0;
+ }
+
+ public boolean hasStorageNotLowConstraint() {
+ return (requiredConstraints&CONSTRAINT_STORAGE_NOT_LOW) != 0;
+ }
+
+ public boolean hasTimingDelayConstraint() {
+ return (requiredConstraints&CONSTRAINT_TIMING_DELAY) != 0;
+ }
+
+ public boolean hasDeadlineConstraint() {
+ return (requiredConstraints&CONSTRAINT_DEADLINE) != 0;
+ }
+
+ public boolean hasIdleConstraint() {
+ return (requiredConstraints&CONSTRAINT_IDLE) != 0;
+ }
+
+ public boolean hasContentTriggerConstraint() {
+ return (requiredConstraints&CONSTRAINT_CONTENT_TRIGGER) != 0;
+ }
+
+ public long getTriggerContentUpdateDelay() {
+ long time = job.getTriggerContentUpdateDelay();
+ if (time < 0) {
+ return DEFAULT_TRIGGER_UPDATE_DELAY;
+ }
+ return Math.max(time, MIN_TRIGGER_UPDATE_DELAY);
+ }
+
+ public long getTriggerContentMaxDelay() {
+ long time = job.getTriggerContentMaxDelay();
+ if (time < 0) {
+ return DEFAULT_TRIGGER_MAX_DELAY;
+ }
+ return Math.max(time, MIN_TRIGGER_MAX_DELAY);
+ }
+
+ public boolean isPersisted() {
+ return job.isPersisted();
+ }
+
+ public long getEarliestRunTime() {
+ return earliestRunTimeElapsedMillis;
+ }
+
+ public long getLatestRunTimeElapsed() {
+ return latestRunTimeElapsedMillis;
+ }
+
+ public long getOriginalLatestRunTimeElapsed() {
+ return mOriginalLatestRunTimeElapsedMillis;
+ }
+
+ public void setOriginalLatestRunTimeElapsed(long latestRunTimeElapsed) {
+ mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsed;
+ }
+
+ /**
+ * Return the fractional position of "now" within the "run time" window of
+ * this job.
+ * <p>
+ * For example, if the earliest run time was 10 minutes ago, and the latest
+ * run time is 30 minutes from now, this would return 0.25.
+ * <p>
+ * If the job has no window defined, returns 1. When only an earliest or
+ * latest time is defined, it's treated as an infinitely small window at
+ * that time.
+ */
+ public float getFractionRunTime() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ if (earliestRunTimeElapsedMillis == 0 && latestRunTimeElapsedMillis == Long.MAX_VALUE) {
+ return 1;
+ } else if (earliestRunTimeElapsedMillis == 0) {
+ return now >= latestRunTimeElapsedMillis ? 1 : 0;
+ } else if (latestRunTimeElapsedMillis == Long.MAX_VALUE) {
+ return now >= earliestRunTimeElapsedMillis ? 1 : 0;
+ } else {
+ if (now <= earliestRunTimeElapsedMillis) {
+ return 0;
+ } else if (now >= latestRunTimeElapsedMillis) {
+ return 1;
+ } else {
+ return (float) (now - earliestRunTimeElapsedMillis)
+ / (float) (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis);
+ }
+ }
+ }
+
+ public Pair<Long, Long> getPersistedUtcTimes() {
+ return mPersistedUtcTimes;
+ }
+
+ public void clearPersistedUtcTimes() {
+ mPersistedUtcTimes = null;
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setChargingConstraintSatisfied(boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_CHARGING, state);
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setBatteryNotLowConstraintSatisfied(boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_BATTERY_NOT_LOW, state);
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setStorageNotLowConstraintSatisfied(boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_STORAGE_NOT_LOW, state);
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setTimingDelayConstraintSatisfied(boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_TIMING_DELAY, state);
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setDeadlineConstraintSatisfied(boolean state) {
+ if (setConstraintSatisfied(CONSTRAINT_DEADLINE, state)) {
+ // The constraint was changed. Update the ready flag.
+ mReadyDeadlineSatisfied = !job.isPeriodic() && hasDeadlineConstraint() && state;
+ return true;
+ }
+ return false;
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setIdleConstraintSatisfied(boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_IDLE, state);
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setConnectivityConstraintSatisfied(boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_CONNECTIVITY, state);
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setContentTriggerConstraintSatisfied(boolean state) {
+ return setConstraintSatisfied(CONSTRAINT_CONTENT_TRIGGER, state);
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setDeviceNotDozingConstraintSatisfied(boolean state, boolean whitelisted) {
+ dozeWhitelisted = whitelisted;
+ if (setConstraintSatisfied(CONSTRAINT_DEVICE_NOT_DOZING, state)) {
+ // The constraint was changed. Update the ready flag.
+ mReadyNotDozing = state || (job.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
+ return true;
+ }
+ return false;
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setBackgroundNotRestrictedConstraintSatisfied(boolean state) {
+ if (setConstraintSatisfied(CONSTRAINT_BACKGROUND_NOT_RESTRICTED, state)) {
+ // The constraint was changed. Update the ready flag.
+ mReadyNotRestrictedInBg = state;
+ return true;
+ }
+ return false;
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setQuotaConstraintSatisfied(boolean state) {
+ if (setConstraintSatisfied(CONSTRAINT_WITHIN_QUOTA, state)) {
+ // The constraint was changed. Update the ready flag.
+ mReadyWithinQuota = state;
+ return true;
+ }
+ return false;
+ }
+
+ /** @return true if the state was changed, false otherwise. */
+ boolean setUidActive(final boolean newActiveState) {
+ if (newActiveState != uidActive) {
+ uidActive = newActiveState;
+ return true;
+ }
+ return false; /* unchanged */
+ }
+
+ /** @return true if the constraint was changed, false otherwise. */
+ boolean setConstraintSatisfied(int constraint, boolean state) {
+ boolean old = (satisfiedConstraints&constraint) != 0;
+ if (old == state) {
+ return false;
+ }
+ satisfiedConstraints = (satisfiedConstraints&~constraint) | (state ? constraint : 0);
+ mSatisfiedConstraintsOfInterest = satisfiedConstraints & CONSTRAINTS_OF_INTEREST;
+ if (STATS_LOG_ENABLED && (STATSD_CONSTRAINTS_TO_LOG & constraint) != 0) {
+ StatsLog.write_non_chained(StatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED,
+ sourceUid, null, getBatteryName(), getProtoConstraint(constraint),
+ state ? StatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED__STATE__SATISFIED
+ : StatsLog.SCHEDULED_JOB_CONSTRAINT_CHANGED__STATE__UNSATISFIED);
+ }
+ return true;
+ }
+
+ boolean isConstraintSatisfied(int constraint) {
+ return (satisfiedConstraints&constraint) != 0;
+ }
+
+ boolean clearTrackingController(int which) {
+ if ((trackingControllers&which) != 0) {
+ trackingControllers &= ~which;
+ return true;
+ }
+ return false;
+ }
+
+ void setTrackingController(int which) {
+ trackingControllers |= which;
+ }
+
+ public long getLastSuccessfulRunTime() {
+ return mLastSuccessfulRunTime;
+ }
+
+ public long getLastFailedRunTime() {
+ return mLastFailedRunTime;
+ }
+
+ /**
+ * @return Whether or not this job is ready to run, based on its requirements.
+ */
+ public boolean isReady() {
+ return isReady(mSatisfiedConstraintsOfInterest);
+ }
+
+ /**
+ * @return Whether or not this job would be ready to run if it had the specified constraint
+ * granted, based on its requirements.
+ */
+ boolean wouldBeReadyWithConstraint(int constraint) {
+ boolean oldValue = false;
+ int satisfied = mSatisfiedConstraintsOfInterest;
+ switch (constraint) {
+ case CONSTRAINT_BACKGROUND_NOT_RESTRICTED:
+ oldValue = mReadyNotRestrictedInBg;
+ mReadyNotRestrictedInBg = true;
+ break;
+ case CONSTRAINT_DEADLINE:
+ oldValue = mReadyDeadlineSatisfied;
+ mReadyDeadlineSatisfied = true;
+ break;
+ case CONSTRAINT_DEVICE_NOT_DOZING:
+ oldValue = mReadyNotDozing;
+ mReadyNotDozing = true;
+ break;
+ case CONSTRAINT_WITHIN_QUOTA:
+ oldValue = mReadyWithinQuota;
+ mReadyWithinQuota = true;
+ break;
+ default:
+ satisfied |= constraint;
+ break;
+ }
+
+ boolean toReturn = isReady(satisfied);
+
+ switch (constraint) {
+ case CONSTRAINT_BACKGROUND_NOT_RESTRICTED:
+ mReadyNotRestrictedInBg = oldValue;
+ break;
+ case CONSTRAINT_DEADLINE:
+ mReadyDeadlineSatisfied = oldValue;
+ break;
+ case CONSTRAINT_DEVICE_NOT_DOZING:
+ mReadyNotDozing = oldValue;
+ break;
+ case CONSTRAINT_WITHIN_QUOTA:
+ mReadyWithinQuota = oldValue;
+ break;
+ }
+ return toReturn;
+ }
+
+ private boolean isReady(int satisfiedConstraints) {
+ // Quota constraints trumps all other constraints.
+ if (!mReadyWithinQuota) {
+ return false;
+ }
+ // Deadline constraint trumps other constraints besides quota (except for periodic jobs
+ // where deadline is an implementation detail. A periodic job should only run if its
+ // constraints are satisfied).
+ // DeviceNotDozing implicit constraint must be satisfied
+ // NotRestrictedInBackground implicit constraint must be satisfied
+ return mReadyNotDozing && mReadyNotRestrictedInBg && (mReadyDeadlineSatisfied
+ || isConstraintsSatisfied(satisfiedConstraints));
+ }
+
+ static final int CONSTRAINTS_OF_INTEREST = CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW
+ | CONSTRAINT_STORAGE_NOT_LOW | CONSTRAINT_TIMING_DELAY | CONSTRAINT_CONNECTIVITY
+ | CONSTRAINT_IDLE | CONSTRAINT_CONTENT_TRIGGER;
+
+ // Soft override covers all non-"functional" constraints
+ static final int SOFT_OVERRIDE_CONSTRAINTS =
+ CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW
+ | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE;
+
+ /**
+ * @return Whether the constraints set on this job are satisfied.
+ */
+ public boolean isConstraintsSatisfied() {
+ return isConstraintsSatisfied(mSatisfiedConstraintsOfInterest);
+ }
+
+ private boolean isConstraintsSatisfied(int satisfiedConstraints) {
+ if (overrideState == OVERRIDE_FULL) {
+ // force override: the job is always runnable
+ return true;
+ }
+
+ int sat = satisfiedConstraints;
+ if (overrideState == OVERRIDE_SOFT) {
+ // override: pretend all 'soft' requirements are satisfied
+ sat |= (requiredConstraints & SOFT_OVERRIDE_CONSTRAINTS);
+ }
+
+ return (sat & mRequiredConstraintsOfInterest) == mRequiredConstraintsOfInterest;
+ }
+
+ public boolean matches(int uid, int jobId) {
+ return this.job.getId() == jobId && this.callingUid == uid;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(128);
+ sb.append("JobStatus{");
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(" #");
+ UserHandle.formatUid(sb, callingUid);
+ sb.append("/");
+ sb.append(job.getId());
+ sb.append(' ');
+ sb.append(batteryName);
+ sb.append(" u=");
+ sb.append(getUserId());
+ sb.append(" s=");
+ sb.append(getSourceUid());
+ if (earliestRunTimeElapsedMillis != NO_EARLIEST_RUNTIME
+ || latestRunTimeElapsedMillis != NO_LATEST_RUNTIME) {
+ long now = sElapsedRealtimeClock.millis();
+ sb.append(" TIME=");
+ formatRunTime(sb, earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME, now);
+ sb.append(":");
+ formatRunTime(sb, latestRunTimeElapsedMillis, NO_LATEST_RUNTIME, now);
+ }
+ if (job.getRequiredNetwork() != null) {
+ sb.append(" NET");
+ }
+ if (job.isRequireCharging()) {
+ sb.append(" CHARGING");
+ }
+ if (job.isRequireBatteryNotLow()) {
+ sb.append(" BATNOTLOW");
+ }
+ if (job.isRequireStorageNotLow()) {
+ sb.append(" STORENOTLOW");
+ }
+ if (job.isRequireDeviceIdle()) {
+ sb.append(" IDLE");
+ }
+ if (job.isPeriodic()) {
+ sb.append(" PERIODIC");
+ }
+ if (job.isPersisted()) {
+ sb.append(" PERSISTED");
+ }
+ if ((satisfiedConstraints&CONSTRAINT_DEVICE_NOT_DOZING) == 0) {
+ sb.append(" WAIT:DEV_NOT_DOZING");
+ }
+ if (job.getTriggerContentUris() != null) {
+ sb.append(" URIS=");
+ sb.append(Arrays.toString(job.getTriggerContentUris()));
+ }
+ if (numFailures != 0) {
+ sb.append(" failures=");
+ sb.append(numFailures);
+ }
+ if (isReady()) {
+ sb.append(" READY");
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+
+ private void formatRunTime(PrintWriter pw, long runtime, long defaultValue, long now) {
+ if (runtime == defaultValue) {
+ pw.print("none");
+ } else {
+ TimeUtils.formatDuration(runtime - now, pw);
+ }
+ }
+
+ private void formatRunTime(StringBuilder sb, long runtime, long defaultValue, long now) {
+ if (runtime == defaultValue) {
+ sb.append("none");
+ } else {
+ TimeUtils.formatDuration(runtime - now, sb);
+ }
+ }
+
+ /**
+ * Convenience function to identify a job uniquely without pulling all the data that
+ * {@link #toString()} returns.
+ */
+ public String toShortString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(" #");
+ UserHandle.formatUid(sb, callingUid);
+ sb.append("/");
+ sb.append(job.getId());
+ sb.append(' ');
+ sb.append(batteryName);
+ return sb.toString();
+ }
+
+ /**
+ * Convenience function to identify a job uniquely without pulling all the data that
+ * {@link #toString()} returns.
+ */
+ public String toShortStringExceptUniqueId() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(Integer.toHexString(System.identityHashCode(this)));
+ sb.append(' ');
+ sb.append(batteryName);
+ return sb.toString();
+ }
+
+ /**
+ * Convenience function to dump data that identifies a job uniquely to proto. This is intended
+ * to mimic {@link #toShortString}.
+ */
+ public void writeToShortProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(JobStatusShortInfoProto.CALLING_UID, callingUid);
+ proto.write(JobStatusShortInfoProto.JOB_ID, job.getId());
+ proto.write(JobStatusShortInfoProto.BATTERY_NAME, batteryName);
+
+ proto.end(token);
+ }
+
+ void dumpConstraints(PrintWriter pw, int constraints) {
+ if ((constraints&CONSTRAINT_CHARGING) != 0) {
+ pw.print(" CHARGING");
+ }
+ if ((constraints& CONSTRAINT_BATTERY_NOT_LOW) != 0) {
+ pw.print(" BATTERY_NOT_LOW");
+ }
+ if ((constraints& CONSTRAINT_STORAGE_NOT_LOW) != 0) {
+ pw.print(" STORAGE_NOT_LOW");
+ }
+ if ((constraints&CONSTRAINT_TIMING_DELAY) != 0) {
+ pw.print(" TIMING_DELAY");
+ }
+ if ((constraints&CONSTRAINT_DEADLINE) != 0) {
+ pw.print(" DEADLINE");
+ }
+ if ((constraints&CONSTRAINT_IDLE) != 0) {
+ pw.print(" IDLE");
+ }
+ if ((constraints&CONSTRAINT_CONNECTIVITY) != 0) {
+ pw.print(" CONNECTIVITY");
+ }
+ if ((constraints&CONSTRAINT_CONTENT_TRIGGER) != 0) {
+ pw.print(" CONTENT_TRIGGER");
+ }
+ if ((constraints&CONSTRAINT_DEVICE_NOT_DOZING) != 0) {
+ pw.print(" DEVICE_NOT_DOZING");
+ }
+ if ((constraints&CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
+ pw.print(" BACKGROUND_NOT_RESTRICTED");
+ }
+ if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) {
+ pw.print(" WITHIN_QUOTA");
+ }
+ if (constraints != 0) {
+ pw.print(" [0x");
+ pw.print(Integer.toHexString(constraints));
+ pw.print("]");
+ }
+ }
+
+ /** Returns a {@link JobServerProtoEnums.Constraint} enum value for the given constraint. */
+ private int getProtoConstraint(int constraint) {
+ switch (constraint) {
+ case CONSTRAINT_BACKGROUND_NOT_RESTRICTED:
+ return JobServerProtoEnums.CONSTRAINT_BACKGROUND_NOT_RESTRICTED;
+ case CONSTRAINT_BATTERY_NOT_LOW:
+ return JobServerProtoEnums.CONSTRAINT_BATTERY_NOT_LOW;
+ case CONSTRAINT_CHARGING:
+ return JobServerProtoEnums.CONSTRAINT_CHARGING;
+ case CONSTRAINT_CONNECTIVITY:
+ return JobServerProtoEnums.CONSTRAINT_CONNECTIVITY;
+ case CONSTRAINT_CONTENT_TRIGGER:
+ return JobServerProtoEnums.CONSTRAINT_CONTENT_TRIGGER;
+ case CONSTRAINT_DEADLINE:
+ return JobServerProtoEnums.CONSTRAINT_DEADLINE;
+ case CONSTRAINT_DEVICE_NOT_DOZING:
+ return JobServerProtoEnums.CONSTRAINT_DEVICE_NOT_DOZING;
+ case CONSTRAINT_IDLE:
+ return JobServerProtoEnums.CONSTRAINT_IDLE;
+ case CONSTRAINT_STORAGE_NOT_LOW:
+ return JobServerProtoEnums.CONSTRAINT_STORAGE_NOT_LOW;
+ case CONSTRAINT_TIMING_DELAY:
+ return JobServerProtoEnums.CONSTRAINT_TIMING_DELAY;
+ case CONSTRAINT_WITHIN_QUOTA:
+ return JobServerProtoEnums.CONSTRAINT_WITHIN_QUOTA;
+ default:
+ return JobServerProtoEnums.CONSTRAINT_UNKNOWN;
+ }
+ }
+
+ /** Writes constraints to the given repeating proto field. */
+ void dumpConstraints(ProtoOutputStream proto, long fieldId, int constraints) {
+ if ((constraints & CONSTRAINT_CHARGING) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_CHARGING);
+ }
+ if ((constraints & CONSTRAINT_BATTERY_NOT_LOW) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_BATTERY_NOT_LOW);
+ }
+ if ((constraints & CONSTRAINT_STORAGE_NOT_LOW) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_STORAGE_NOT_LOW);
+ }
+ if ((constraints & CONSTRAINT_TIMING_DELAY) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_TIMING_DELAY);
+ }
+ if ((constraints & CONSTRAINT_DEADLINE) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_DEADLINE);
+ }
+ if ((constraints & CONSTRAINT_IDLE) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_IDLE);
+ }
+ if ((constraints & CONSTRAINT_CONNECTIVITY) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_CONNECTIVITY);
+ }
+ if ((constraints & CONSTRAINT_CONTENT_TRIGGER) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_CONTENT_TRIGGER);
+ }
+ if ((constraints & CONSTRAINT_DEVICE_NOT_DOZING) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_DEVICE_NOT_DOZING);
+ }
+ if ((constraints & CONSTRAINT_WITHIN_QUOTA) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_WITHIN_QUOTA);
+ }
+ if ((constraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
+ proto.write(fieldId, JobServerProtoEnums.CONSTRAINT_BACKGROUND_NOT_RESTRICTED);
+ }
+ }
+
+ private void dumpJobWorkItem(PrintWriter pw, String prefix, JobWorkItem work, int index) {
+ pw.print(prefix); pw.print(" #"); pw.print(index); pw.print(": #");
+ pw.print(work.getWorkId()); pw.print(" "); pw.print(work.getDeliveryCount());
+ pw.print("x "); pw.println(work.getIntent());
+ if (work.getGrants() != null) {
+ pw.print(prefix); pw.println(" URI grants:");
+ ((GrantedUriPermissions)work.getGrants()).dump(pw, prefix + " ");
+ }
+ }
+
+ private void dumpJobWorkItem(ProtoOutputStream proto, long fieldId, JobWorkItem work) {
+ final long token = proto.start(fieldId);
+
+ proto.write(JobStatusDumpProto.JobWorkItem.WORK_ID, work.getWorkId());
+ proto.write(JobStatusDumpProto.JobWorkItem.DELIVERY_COUNT, work.getDeliveryCount());
+ if (work.getIntent() != null) {
+ work.getIntent().writeToProto(proto, JobStatusDumpProto.JobWorkItem.INTENT);
+ }
+ Object grants = work.getGrants();
+ if (grants != null) {
+ ((GrantedUriPermissions) grants).dump(proto, JobStatusDumpProto.JobWorkItem.URI_GRANTS);
+ }
+
+ proto.end(token);
+ }
+
+ /**
+ * Returns a bucket name based on the normalized bucket indices, not the AppStandby constants.
+ */
+ String getBucketName() {
+ return bucketName(standbyBucket);
+ }
+
+ /**
+ * Returns a bucket name based on the normalized bucket indices, not the AppStandby constants.
+ */
+ static String bucketName(int standbyBucket) {
+ switch (standbyBucket) {
+ case 0: return "ACTIVE";
+ case 1: return "WORKING_SET";
+ case 2: return "FREQUENT";
+ case 3: return "RARE";
+ case 4: return "NEVER";
+ default:
+ return "Unknown: " + standbyBucket;
+ }
+ }
+
+ // Dumpsys infrastructure
+ public void dump(PrintWriter pw, String prefix, boolean full, long elapsedRealtimeMillis) {
+ pw.print(prefix); UserHandle.formatUid(pw, callingUid);
+ pw.print(" tag="); pw.println(tag);
+ pw.print(prefix);
+ pw.print("Source: uid="); UserHandle.formatUid(pw, getSourceUid());
+ pw.print(" user="); pw.print(getSourceUserId());
+ pw.print(" pkg="); pw.println(getSourcePackageName());
+ if (full) {
+ pw.print(prefix); pw.println("JobInfo:");
+ pw.print(prefix); pw.print(" Service: ");
+ pw.println(job.getService().flattenToShortString());
+ if (job.isPeriodic()) {
+ pw.print(prefix); pw.print(" PERIODIC: interval=");
+ TimeUtils.formatDuration(job.getIntervalMillis(), pw);
+ pw.print(" flex="); TimeUtils.formatDuration(job.getFlexMillis(), pw);
+ pw.println();
+ }
+ if (job.isPersisted()) {
+ pw.print(prefix); pw.println(" PERSISTED");
+ }
+ if (job.getPriority() != 0) {
+ pw.print(prefix); pw.print(" Priority: ");
+ pw.println(JobInfo.getPriorityString(job.getPriority()));
+ }
+ if (job.getFlags() != 0) {
+ pw.print(prefix); pw.print(" Flags: ");
+ pw.println(Integer.toHexString(job.getFlags()));
+ }
+ if (getInternalFlags() != 0) {
+ pw.print(prefix); pw.print(" Internal flags: ");
+ pw.print(Integer.toHexString(getInternalFlags()));
+
+ if ((getInternalFlags()&INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0) {
+ pw.print(" HAS_FOREGROUND_EXEMPTION");
+ }
+ pw.println();
+ }
+ pw.print(prefix); pw.print(" Requires: charging=");
+ pw.print(job.isRequireCharging()); pw.print(" batteryNotLow=");
+ pw.print(job.isRequireBatteryNotLow()); pw.print(" deviceIdle=");
+ pw.println(job.isRequireDeviceIdle());
+ if (job.getTriggerContentUris() != null) {
+ pw.print(prefix); pw.println(" Trigger content URIs:");
+ for (int i = 0; i < job.getTriggerContentUris().length; i++) {
+ JobInfo.TriggerContentUri trig = job.getTriggerContentUris()[i];
+ pw.print(prefix); pw.print(" ");
+ pw.print(Integer.toHexString(trig.getFlags()));
+ pw.print(' '); pw.println(trig.getUri());
+ }
+ if (job.getTriggerContentUpdateDelay() >= 0) {
+ pw.print(prefix); pw.print(" Trigger update delay: ");
+ TimeUtils.formatDuration(job.getTriggerContentUpdateDelay(), pw);
+ pw.println();
+ }
+ if (job.getTriggerContentMaxDelay() >= 0) {
+ pw.print(prefix); pw.print(" Trigger max delay: ");
+ TimeUtils.formatDuration(job.getTriggerContentMaxDelay(), pw);
+ pw.println();
+ }
+ }
+ if (job.getExtras() != null && !job.getExtras().maybeIsEmpty()) {
+ pw.print(prefix); pw.print(" Extras: ");
+ pw.println(job.getExtras().toShortString());
+ }
+ if (job.getTransientExtras() != null && !job.getTransientExtras().maybeIsEmpty()) {
+ pw.print(prefix); pw.print(" Transient extras: ");
+ pw.println(job.getTransientExtras().toShortString());
+ }
+ if (job.getClipData() != null) {
+ pw.print(prefix); pw.print(" Clip data: ");
+ StringBuilder b = new StringBuilder(128);
+ job.getClipData().toShortString(b);
+ pw.println(b);
+ }
+ if (uriPerms != null) {
+ pw.print(prefix); pw.println(" Granted URI permissions:");
+ uriPerms.dump(pw, prefix + " ");
+ }
+ if (job.getRequiredNetwork() != null) {
+ pw.print(prefix); pw.print(" Network type: ");
+ pw.println(job.getRequiredNetwork());
+ }
+ if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ pw.print(prefix); pw.print(" Network download bytes: ");
+ pw.println(mTotalNetworkDownloadBytes);
+ }
+ if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ pw.print(prefix); pw.print(" Network upload bytes: ");
+ pw.println(mTotalNetworkUploadBytes);
+ }
+ if (job.getMinLatencyMillis() != 0) {
+ pw.print(prefix); pw.print(" Minimum latency: ");
+ TimeUtils.formatDuration(job.getMinLatencyMillis(), pw);
+ pw.println();
+ }
+ if (job.getMaxExecutionDelayMillis() != 0) {
+ pw.print(prefix); pw.print(" Max execution delay: ");
+ TimeUtils.formatDuration(job.getMaxExecutionDelayMillis(), pw);
+ pw.println();
+ }
+ pw.print(prefix); pw.print(" Backoff: policy="); pw.print(job.getBackoffPolicy());
+ pw.print(" initial="); TimeUtils.formatDuration(job.getInitialBackoffMillis(), pw);
+ pw.println();
+ if (job.hasEarlyConstraint()) {
+ pw.print(prefix); pw.println(" Has early constraint");
+ }
+ if (job.hasLateConstraint()) {
+ pw.print(prefix); pw.println(" Has late constraint");
+ }
+ }
+ pw.print(prefix); pw.print("Required constraints:");
+ dumpConstraints(pw, requiredConstraints);
+ pw.println();
+ if (full) {
+ pw.print(prefix); pw.print("Satisfied constraints:");
+ dumpConstraints(pw, satisfiedConstraints);
+ pw.println();
+ pw.print(prefix); pw.print("Unsatisfied constraints:");
+ dumpConstraints(pw,
+ ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA) & ~satisfiedConstraints));
+ pw.println();
+ if (dozeWhitelisted) {
+ pw.print(prefix); pw.println("Doze whitelisted: true");
+ }
+ if (uidActive) {
+ pw.print(prefix); pw.println("Uid: active");
+ }
+ if (job.isExemptedFromAppStandby()) {
+ pw.print(prefix); pw.println("Is exempted from app standby");
+ }
+ }
+ if (trackingControllers != 0) {
+ pw.print(prefix); pw.print("Tracking:");
+ if ((trackingControllers&TRACKING_BATTERY) != 0) pw.print(" BATTERY");
+ if ((trackingControllers&TRACKING_CONNECTIVITY) != 0) pw.print(" CONNECTIVITY");
+ if ((trackingControllers&TRACKING_CONTENT) != 0) pw.print(" CONTENT");
+ if ((trackingControllers&TRACKING_IDLE) != 0) pw.print(" IDLE");
+ if ((trackingControllers&TRACKING_STORAGE) != 0) pw.print(" STORAGE");
+ if ((trackingControllers&TRACKING_TIME) != 0) pw.print(" TIME");
+ if ((trackingControllers & TRACKING_QUOTA) != 0) pw.print(" QUOTA");
+ pw.println();
+ }
+
+ pw.print(prefix); pw.println("Implicit constraints:");
+ pw.print(prefix); pw.print(" readyNotDozing: ");
+ pw.println(mReadyNotDozing);
+ pw.print(prefix); pw.print(" readyNotRestrictedInBg: ");
+ pw.println(mReadyNotRestrictedInBg);
+ if (!job.isPeriodic() && hasDeadlineConstraint()) {
+ pw.print(prefix); pw.print(" readyDeadlineSatisfied: ");
+ pw.println(mReadyDeadlineSatisfied);
+ }
+
+ if (changedAuthorities != null) {
+ pw.print(prefix); pw.println("Changed authorities:");
+ for (int i=0; i<changedAuthorities.size(); i++) {
+ pw.print(prefix); pw.print(" "); pw.println(changedAuthorities.valueAt(i));
+ }
+ if (changedUris != null) {
+ pw.print(prefix); pw.println("Changed URIs:");
+ for (int i=0; i<changedUris.size(); i++) {
+ pw.print(prefix); pw.print(" "); pw.println(changedUris.valueAt(i));
+ }
+ }
+ }
+ if (network != null) {
+ pw.print(prefix); pw.print("Network: "); pw.println(network);
+ }
+ if (pendingWork != null && pendingWork.size() > 0) {
+ pw.print(prefix); pw.println("Pending work:");
+ for (int i = 0; i < pendingWork.size(); i++) {
+ dumpJobWorkItem(pw, prefix, pendingWork.get(i), i);
+ }
+ }
+ if (executingWork != null && executingWork.size() > 0) {
+ pw.print(prefix); pw.println("Executing work:");
+ for (int i = 0; i < executingWork.size(); i++) {
+ dumpJobWorkItem(pw, prefix, executingWork.get(i), i);
+ }
+ }
+ pw.print(prefix); pw.print("Standby bucket: ");
+ pw.println(getBucketName());
+ if (standbyBucket > 0) {
+ pw.print(prefix); pw.print("Base heartbeat: ");
+ pw.println(baseHeartbeat);
+ }
+ if (whenStandbyDeferred != 0) {
+ pw.print(prefix); pw.print(" Deferred since: ");
+ TimeUtils.formatDuration(whenStandbyDeferred, elapsedRealtimeMillis, pw);
+ pw.println();
+ }
+ if (mFirstForceBatchedTimeElapsed != 0) {
+ pw.print(prefix);
+ pw.print(" Time since first force batch attempt: ");
+ TimeUtils.formatDuration(mFirstForceBatchedTimeElapsed, elapsedRealtimeMillis, pw);
+ pw.println();
+ }
+ pw.print(prefix); pw.print("Enqueue time: ");
+ TimeUtils.formatDuration(enqueueTime, elapsedRealtimeMillis, pw);
+ pw.println();
+ pw.print(prefix); pw.print("Run time: earliest=");
+ formatRunTime(pw, earliestRunTimeElapsedMillis, NO_EARLIEST_RUNTIME, elapsedRealtimeMillis);
+ pw.print(", latest=");
+ formatRunTime(pw, latestRunTimeElapsedMillis, NO_LATEST_RUNTIME, elapsedRealtimeMillis);
+ pw.print(", original latest=");
+ formatRunTime(pw, mOriginalLatestRunTimeElapsedMillis,
+ NO_LATEST_RUNTIME, elapsedRealtimeMillis);
+ pw.println();
+ if (numFailures != 0) {
+ pw.print(prefix); pw.print("Num failures: "); pw.println(numFailures);
+ }
+ if (mLastSuccessfulRunTime != 0) {
+ pw.print(prefix); pw.print("Last successful run: ");
+ pw.println(TimeMigrationUtils.formatMillisWithFixedFormat(mLastSuccessfulRunTime));
+ }
+ if (mLastFailedRunTime != 0) {
+ pw.print(prefix); pw.print("Last failed run: ");
+ pw.println(TimeMigrationUtils.formatMillisWithFixedFormat(mLastFailedRunTime));
+ }
+ }
+
+ public void dump(ProtoOutputStream proto, long fieldId, boolean full, long elapsedRealtimeMillis) {
+ final long token = proto.start(fieldId);
+
+ proto.write(JobStatusDumpProto.CALLING_UID, callingUid);
+ proto.write(JobStatusDumpProto.TAG, tag);
+ proto.write(JobStatusDumpProto.SOURCE_UID, getSourceUid());
+ proto.write(JobStatusDumpProto.SOURCE_USER_ID, getSourceUserId());
+ proto.write(JobStatusDumpProto.SOURCE_PACKAGE_NAME, getSourcePackageName());
+ proto.write(JobStatusDumpProto.INTERNAL_FLAGS, getInternalFlags());
+
+ if (full) {
+ final long jiToken = proto.start(JobStatusDumpProto.JOB_INFO);
+
+ job.getService().writeToProto(proto, JobStatusDumpProto.JobInfo.SERVICE);
+
+ proto.write(JobStatusDumpProto.JobInfo.IS_PERIODIC, job.isPeriodic());
+ proto.write(JobStatusDumpProto.JobInfo.PERIOD_INTERVAL_MS, job.getIntervalMillis());
+ proto.write(JobStatusDumpProto.JobInfo.PERIOD_FLEX_MS, job.getFlexMillis());
+
+ proto.write(JobStatusDumpProto.JobInfo.IS_PERSISTED, job.isPersisted());
+ proto.write(JobStatusDumpProto.JobInfo.PRIORITY, job.getPriority());
+ proto.write(JobStatusDumpProto.JobInfo.FLAGS, job.getFlags());
+
+ proto.write(JobStatusDumpProto.JobInfo.REQUIRES_CHARGING, job.isRequireCharging());
+ proto.write(JobStatusDumpProto.JobInfo.REQUIRES_BATTERY_NOT_LOW, job.isRequireBatteryNotLow());
+ proto.write(JobStatusDumpProto.JobInfo.REQUIRES_DEVICE_IDLE, job.isRequireDeviceIdle());
+
+ if (job.getTriggerContentUris() != null) {
+ for (int i = 0; i < job.getTriggerContentUris().length; i++) {
+ final long tcuToken = proto.start(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_URIS);
+ JobInfo.TriggerContentUri trig = job.getTriggerContentUris()[i];
+
+ proto.write(JobStatusDumpProto.JobInfo.TriggerContentUri.FLAGS, trig.getFlags());
+ Uri u = trig.getUri();
+ if (u != null) {
+ proto.write(JobStatusDumpProto.JobInfo.TriggerContentUri.URI, u.toString());
+ }
+
+ proto.end(tcuToken);
+ }
+ if (job.getTriggerContentUpdateDelay() >= 0) {
+ proto.write(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_UPDATE_DELAY_MS,
+ job.getTriggerContentUpdateDelay());
+ }
+ if (job.getTriggerContentMaxDelay() >= 0) {
+ proto.write(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_MAX_DELAY_MS,
+ job.getTriggerContentMaxDelay());
+ }
+ }
+ if (job.getExtras() != null && !job.getExtras().maybeIsEmpty()) {
+ job.getExtras().writeToProto(proto, JobStatusDumpProto.JobInfo.EXTRAS);
+ }
+ if (job.getTransientExtras() != null && !job.getTransientExtras().maybeIsEmpty()) {
+ job.getTransientExtras().writeToProto(proto, JobStatusDumpProto.JobInfo.TRANSIENT_EXTRAS);
+ }
+ if (job.getClipData() != null) {
+ job.getClipData().writeToProto(proto, JobStatusDumpProto.JobInfo.CLIP_DATA);
+ }
+ if (uriPerms != null) {
+ uriPerms.dump(proto, JobStatusDumpProto.JobInfo.GRANTED_URI_PERMISSIONS);
+ }
+ if (job.getRequiredNetwork() != null) {
+ job.getRequiredNetwork().writeToProto(proto, JobStatusDumpProto.JobInfo.REQUIRED_NETWORK);
+ }
+ if (mTotalNetworkDownloadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ proto.write(JobStatusDumpProto.JobInfo.TOTAL_NETWORK_DOWNLOAD_BYTES,
+ mTotalNetworkDownloadBytes);
+ }
+ if (mTotalNetworkUploadBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ proto.write(JobStatusDumpProto.JobInfo.TOTAL_NETWORK_UPLOAD_BYTES,
+ mTotalNetworkUploadBytes);
+ }
+ proto.write(JobStatusDumpProto.JobInfo.MIN_LATENCY_MS, job.getMinLatencyMillis());
+ proto.write(JobStatusDumpProto.JobInfo.MAX_EXECUTION_DELAY_MS, job.getMaxExecutionDelayMillis());
+
+ final long bpToken = proto.start(JobStatusDumpProto.JobInfo.BACKOFF_POLICY);
+ proto.write(JobStatusDumpProto.JobInfo.Backoff.POLICY, job.getBackoffPolicy());
+ proto.write(JobStatusDumpProto.JobInfo.Backoff.INITIAL_BACKOFF_MS,
+ job.getInitialBackoffMillis());
+ proto.end(bpToken);
+
+ proto.write(JobStatusDumpProto.JobInfo.HAS_EARLY_CONSTRAINT, job.hasEarlyConstraint());
+ proto.write(JobStatusDumpProto.JobInfo.HAS_LATE_CONSTRAINT, job.hasLateConstraint());
+
+ proto.end(jiToken);
+ }
+
+ dumpConstraints(proto, JobStatusDumpProto.REQUIRED_CONSTRAINTS, requiredConstraints);
+ if (full) {
+ dumpConstraints(proto, JobStatusDumpProto.SATISFIED_CONSTRAINTS, satisfiedConstraints);
+ dumpConstraints(proto, JobStatusDumpProto.UNSATISFIED_CONSTRAINTS,
+ ((requiredConstraints | CONSTRAINT_WITHIN_QUOTA) & ~satisfiedConstraints));
+ proto.write(JobStatusDumpProto.IS_DOZE_WHITELISTED, dozeWhitelisted);
+ proto.write(JobStatusDumpProto.IS_UID_ACTIVE, uidActive);
+ proto.write(JobStatusDumpProto.IS_EXEMPTED_FROM_APP_STANDBY,
+ job.isExemptedFromAppStandby());
+ }
+
+ // Tracking controllers
+ if ((trackingControllers&TRACKING_BATTERY) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_BATTERY);
+ }
+ if ((trackingControllers&TRACKING_CONNECTIVITY) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_CONNECTIVITY);
+ }
+ if ((trackingControllers&TRACKING_CONTENT) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_CONTENT);
+ }
+ if ((trackingControllers&TRACKING_IDLE) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_IDLE);
+ }
+ if ((trackingControllers&TRACKING_STORAGE) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_STORAGE);
+ }
+ if ((trackingControllers&TRACKING_TIME) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_TIME);
+ }
+ if ((trackingControllers & TRACKING_QUOTA) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_QUOTA);
+ }
+
+ // Implicit constraints
+ final long icToken = proto.start(JobStatusDumpProto.IMPLICIT_CONSTRAINTS);
+ proto.write(JobStatusDumpProto.ImplicitConstraints.IS_NOT_DOZING, mReadyNotDozing);
+ proto.write(JobStatusDumpProto.ImplicitConstraints.IS_NOT_RESTRICTED_IN_BG,
+ mReadyNotRestrictedInBg);
+ proto.end(icToken);
+
+ if (changedAuthorities != null) {
+ for (int k = 0; k < changedAuthorities.size(); k++) {
+ proto.write(JobStatusDumpProto.CHANGED_AUTHORITIES, changedAuthorities.valueAt(k));
+ }
+ }
+ if (changedUris != null) {
+ for (int i = 0; i < changedUris.size(); i++) {
+ Uri u = changedUris.valueAt(i);
+ proto.write(JobStatusDumpProto.CHANGED_URIS, u.toString());
+ }
+ }
+
+ if (network != null) {
+ network.writeToProto(proto, JobStatusDumpProto.NETWORK);
+ }
+
+ if (pendingWork != null && pendingWork.size() > 0) {
+ for (int i = 0; i < pendingWork.size(); i++) {
+ dumpJobWorkItem(proto, JobStatusDumpProto.PENDING_WORK, pendingWork.get(i));
+ }
+ }
+ if (executingWork != null && executingWork.size() > 0) {
+ for (int i = 0; i < executingWork.size(); i++) {
+ dumpJobWorkItem(proto, JobStatusDumpProto.EXECUTING_WORK, executingWork.get(i));
+ }
+ }
+
+ proto.write(JobStatusDumpProto.STANDBY_BUCKET, standbyBucket);
+ proto.write(JobStatusDumpProto.ENQUEUE_DURATION_MS, elapsedRealtimeMillis - enqueueTime);
+ proto.write(JobStatusDumpProto.TIME_SINCE_FIRST_DEFERRAL_MS,
+ whenStandbyDeferred == 0 ? 0 : elapsedRealtimeMillis - whenStandbyDeferred);
+ proto.write(JobStatusDumpProto.TIME_SINCE_FIRST_FORCE_BATCH_ATTEMPT_MS,
+ mFirstForceBatchedTimeElapsed == 0
+ ? 0 : elapsedRealtimeMillis - mFirstForceBatchedTimeElapsed);
+ if (earliestRunTimeElapsedMillis == NO_EARLIEST_RUNTIME) {
+ proto.write(JobStatusDumpProto.TIME_UNTIL_EARLIEST_RUNTIME_MS, 0);
+ } else {
+ proto.write(JobStatusDumpProto.TIME_UNTIL_EARLIEST_RUNTIME_MS,
+ earliestRunTimeElapsedMillis - elapsedRealtimeMillis);
+ }
+ if (latestRunTimeElapsedMillis == NO_LATEST_RUNTIME) {
+ proto.write(JobStatusDumpProto.TIME_UNTIL_LATEST_RUNTIME_MS, 0);
+ } else {
+ proto.write(JobStatusDumpProto.TIME_UNTIL_LATEST_RUNTIME_MS,
+ latestRunTimeElapsedMillis - elapsedRealtimeMillis);
+ }
+
+ proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures);
+ proto.write(JobStatusDumpProto.LAST_SUCCESSFUL_RUN_TIME, mLastSuccessfulRunTime);
+ proto.write(JobStatusDumpProto.LAST_FAILED_RUN_TIME, mLastFailedRunTime);
+
+ proto.end(token);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
new file mode 100644
index 0000000..b8cfac4
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -0,0 +1,2829 @@
+/*
+ * 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 android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+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.WORKING_INDEX;
+import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.AlarmManager;
+import android.app.AppGlobals;
+import android.app.IUidObserver;
+import android.app.usage.UsageStatsManagerInternal;
+import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.BatteryManager;
+import android.os.BatteryManagerInternal;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.KeyValueListParser;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseSetArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
+import com.android.server.job.ConstantsProto;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobServiceContext;
+import com.android.server.job.StateControllerProto;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Controller that tracks whether an app has exceeded its standby bucket quota.
+ *
+ * With initial defaults, each app in each bucket is given 10 minutes to run within its respective
+ * time window. Active jobs can run indefinitely, working set jobs can run for 10 minutes within a
+ * 2 hour window, frequent jobs get to run 10 minutes in an 8 hour window, and rare jobs get to run
+ * 10 minutes in a 24 hour window. The windows are rolling, so as soon as a job would have some
+ * quota based on its bucket, it will be eligible to run. When a job's bucket changes, its new
+ * quota is immediately applied to it.
+ *
+ * Job and session count limits are included to prevent abuse/spam. Each bucket has its own limit on
+ * the number of jobs or sessions that can run within the window. Regardless of bucket, apps will
+ * not be allowed to run more than 20 jobs within the past 10 minutes.
+ *
+ * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run
+ * freely when an app enters the foreground state and are restricted when the app leaves the
+ * foreground state. However, jobs that are started while the app is in the TOP state do not count
+ * towards any quota and are not restricted regardless of the app's state change.
+ *
+ * Jobs will not be throttled when the device is charging. The device is considered to be charging
+ * once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast.
+ *
+ * Note: all limits are enforced per bucket window unless explicitly stated otherwise.
+ * All stated values are configurable and subject to change. See {@link QcConstants} for current
+ * defaults.
+ *
+ * Test: atest com.android.server.job.controllers.QuotaControllerTest
+ */
+public final class QuotaController extends StateController {
+ private static final String TAG = "JobScheduler.Quota";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final String ALARM_TAG_CLEANUP = "*job.cleanup*";
+ private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*";
+
+ /**
+ * A sparse array of ArrayMaps, which is suitable for holding (userId, packageName)->object
+ * associations.
+ */
+ private static class UserPackageMap<T> {
+ private final SparseArray<ArrayMap<String, T>> mData = new SparseArray<>();
+
+ public void add(int userId, @NonNull String packageName, @Nullable T obj) {
+ ArrayMap<String, T> data = mData.get(userId);
+ if (data == null) {
+ data = new ArrayMap<String, T>();
+ mData.put(userId, data);
+ }
+ data.put(packageName, obj);
+ }
+
+ public void clear() {
+ for (int i = 0; i < mData.size(); ++i) {
+ mData.valueAt(i).clear();
+ }
+ }
+
+ /** Removes all the data for the user, if there was any. */
+ public void delete(int userId) {
+ mData.delete(userId);
+ }
+
+ /** Removes the data for the user and package, if there was any. */
+ public void delete(int userId, @NonNull String packageName) {
+ ArrayMap<String, T> data = mData.get(userId);
+ if (data != null) {
+ data.remove(packageName);
+ }
+ }
+
+ @Nullable
+ public T get(int userId, @NonNull String packageName) {
+ ArrayMap<String, T> data = mData.get(userId);
+ if (data != null) {
+ return data.get(packageName);
+ }
+ return null;
+ }
+
+ /** @see SparseArray#indexOfKey */
+ public int indexOfKey(int userId) {
+ return mData.indexOfKey(userId);
+ }
+
+ /** Returns the userId at the given index. */
+ public int keyAt(int index) {
+ return mData.keyAt(index);
+ }
+
+ /** Returns the package name at the given index. */
+ @NonNull
+ public String keyAt(int userIndex, int packageIndex) {
+ return mData.valueAt(userIndex).keyAt(packageIndex);
+ }
+
+ /** Returns the size of the outer (userId) array. */
+ public int numUsers() {
+ return mData.size();
+ }
+
+ public int numPackagesForUser(int userId) {
+ ArrayMap<String, T> data = mData.get(userId);
+ return data == null ? 0 : data.size();
+ }
+
+ /** Returns the value T at the given user and index. */
+ @Nullable
+ public T valueAt(int userIndex, int packageIndex) {
+ return mData.valueAt(userIndex).valueAt(packageIndex);
+ }
+
+ public void forEach(Consumer<T> consumer) {
+ for (int i = numUsers() - 1; i >= 0; --i) {
+ ArrayMap<String, T> data = mData.valueAt(i);
+ for (int j = data.size() - 1; j >= 0; --j) {
+ consumer.accept(data.valueAt(j));
+ }
+ }
+ }
+ }
+
+ /**
+ * Standardize the output of userId-packageName combo.
+ */
+ private static String string(int userId, String packageName) {
+ return "<" + userId + ">" + packageName;
+ }
+
+ private static final class Package {
+ public final String packageName;
+ public final int userId;
+
+ Package(int userId, String packageName) {
+ this.userId = userId;
+ this.packageName = packageName;
+ }
+
+ @Override
+ public String toString() {
+ return string(userId, packageName);
+ }
+
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(StateControllerProto.QuotaController.Package.USER_ID, userId);
+ proto.write(StateControllerProto.QuotaController.Package.NAME, packageName);
+
+ proto.end(token);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Package) {
+ Package other = (Package) obj;
+ return userId == other.userId && Objects.equals(packageName, other.packageName);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return packageName.hashCode() + userId;
+ }
+ }
+
+ private static int hashLong(long val) {
+ return (int) (val ^ (val >>> 32));
+ }
+
+ @VisibleForTesting
+ static class ExecutionStats {
+ /**
+ * The time after which this record should be considered invalid (out of date), in the
+ * elapsed realtime timebase.
+ */
+ public long expirationTimeElapsed;
+
+ public long windowSizeMs;
+ public int jobCountLimit;
+ public int sessionCountLimit;
+
+ /** The total amount of time the app ran in its respective bucket window size. */
+ public long executionTimeInWindowMs;
+ public int bgJobCountInWindow;
+
+ /** The total amount of time the app ran in the last {@link #MAX_PERIOD_MS}. */
+ public long executionTimeInMaxPeriodMs;
+ public int bgJobCountInMaxPeriod;
+
+ /**
+ * The number of {@link TimingSession}s within the bucket window size. This will include
+ * sessions that started before the window as long as they end within the window.
+ */
+ public int sessionCountInWindow;
+
+ /**
+ * The time after which the app will be under the bucket quota and can start running jobs
+ * again. This is only valid if
+ * {@link #executionTimeInWindowMs} >= {@link #mAllowedTimePerPeriodMs},
+ * {@link #executionTimeInMaxPeriodMs} >= {@link #mMaxExecutionTimeMs},
+ * {@link #bgJobCountInWindow} >= {@link #jobCountLimit}, or
+ * {@link #sessionCountInWindow} >= {@link #sessionCountLimit}.
+ */
+ public long inQuotaTimeElapsed;
+
+ /**
+ * The time after which {@link #jobCountInRateLimitingWindow} should be considered invalid,
+ * in the elapsed realtime timebase.
+ */
+ public long jobRateLimitExpirationTimeElapsed;
+
+ /**
+ * The number of jobs that ran in at least the last {@link #mRateLimitingWindowMs}.
+ * It may contain a few stale entries since cleanup won't happen exactly every
+ * {@link #mRateLimitingWindowMs}.
+ */
+ public int jobCountInRateLimitingWindow;
+
+ /**
+ * The time after which {@link #sessionCountInRateLimitingWindow} should be considered
+ * invalid, in the elapsed realtime timebase.
+ */
+ public long sessionRateLimitExpirationTimeElapsed;
+
+ /**
+ * The number of {@link TimingSession}s that ran in at least the last
+ * {@link #mRateLimitingWindowMs}. It may contain a few stale entries since cleanup won't
+ * happen exactly every {@link #mRateLimitingWindowMs}. This should only be considered
+ * valid before elapsed realtime has reached {@link #sessionRateLimitExpirationTimeElapsed}.
+ */
+ public int sessionCountInRateLimitingWindow;
+
+ @Override
+ public String toString() {
+ return "expirationTime=" + expirationTimeElapsed + ", "
+ + "windowSizeMs=" + windowSizeMs + ", "
+ + "jobCountLimit=" + jobCountLimit + ", "
+ + "sessionCountLimit=" + sessionCountLimit + ", "
+ + "executionTimeInWindow=" + executionTimeInWindowMs + ", "
+ + "bgJobCountInWindow=" + bgJobCountInWindow + ", "
+ + "executionTimeInMaxPeriod=" + executionTimeInMaxPeriodMs + ", "
+ + "bgJobCountInMaxPeriod=" + bgJobCountInMaxPeriod + ", "
+ + "sessionCountInWindow=" + sessionCountInWindow + ", "
+ + "inQuotaTime=" + inQuotaTimeElapsed + ", "
+ + "jobCountExpirationTime=" + jobRateLimitExpirationTimeElapsed + ", "
+ + "jobCountInRateLimitingWindow=" + jobCountInRateLimitingWindow + ", "
+ + "sessionCountExpirationTime=" + sessionRateLimitExpirationTimeElapsed + ", "
+ + "sessionCountInRateLimitingWindow=" + sessionCountInRateLimitingWindow;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ExecutionStats) {
+ ExecutionStats other = (ExecutionStats) obj;
+ return this.expirationTimeElapsed == other.expirationTimeElapsed
+ && this.windowSizeMs == other.windowSizeMs
+ && this.jobCountLimit == other.jobCountLimit
+ && this.sessionCountLimit == other.sessionCountLimit
+ && this.executionTimeInWindowMs == other.executionTimeInWindowMs
+ && this.bgJobCountInWindow == other.bgJobCountInWindow
+ && this.executionTimeInMaxPeriodMs == other.executionTimeInMaxPeriodMs
+ && this.sessionCountInWindow == other.sessionCountInWindow
+ && this.bgJobCountInMaxPeriod == other.bgJobCountInMaxPeriod
+ && this.inQuotaTimeElapsed == other.inQuotaTimeElapsed
+ && this.jobRateLimitExpirationTimeElapsed
+ == other.jobRateLimitExpirationTimeElapsed
+ && this.jobCountInRateLimitingWindow == other.jobCountInRateLimitingWindow
+ && this.sessionRateLimitExpirationTimeElapsed
+ == other.sessionRateLimitExpirationTimeElapsed
+ && this.sessionCountInRateLimitingWindow
+ == other.sessionCountInRateLimitingWindow;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ result = 31 * result + hashLong(expirationTimeElapsed);
+ result = 31 * result + hashLong(windowSizeMs);
+ result = 31 * result + hashLong(jobCountLimit);
+ result = 31 * result + hashLong(sessionCountLimit);
+ result = 31 * result + hashLong(executionTimeInWindowMs);
+ result = 31 * result + bgJobCountInWindow;
+ result = 31 * result + hashLong(executionTimeInMaxPeriodMs);
+ result = 31 * result + bgJobCountInMaxPeriod;
+ result = 31 * result + sessionCountInWindow;
+ result = 31 * result + hashLong(inQuotaTimeElapsed);
+ result = 31 * result + hashLong(jobRateLimitExpirationTimeElapsed);
+ result = 31 * result + jobCountInRateLimitingWindow;
+ result = 31 * result + hashLong(sessionRateLimitExpirationTimeElapsed);
+ result = 31 * result + sessionCountInRateLimitingWindow;
+ return result;
+ }
+ }
+
+ /** List of all tracked jobs keyed by source package-userId combo. */
+ private final UserPackageMap<ArraySet<JobStatus>> mTrackedJobs = new UserPackageMap<>();
+
+ /** Timer for each package-userId combo. */
+ private final UserPackageMap<Timer> mPkgTimers = new UserPackageMap<>();
+
+ /** List of all timing sessions for a package-userId combo, in chronological order. */
+ private final UserPackageMap<List<TimingSession>> mTimingSessions = new UserPackageMap<>();
+
+ /**
+ * List of alarm listeners for each package that listen for when each package comes back within
+ * quota.
+ */
+ private final UserPackageMap<QcAlarmListener> mInQuotaAlarmListeners = new UserPackageMap<>();
+
+ /** Cached calculation results for each app, with the standby buckets as the array indices. */
+ private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>();
+
+ /** List of UIDs currently in the foreground. */
+ private final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
+
+ /** Cached mapping of UIDs (for all users) to a list of packages in the UID. */
+ private final SparseSetArray<String> mUidToPackageCache = new SparseSetArray<>();
+
+ /**
+ * List of jobs that started while the UID was in the TOP state. There will be no more than
+ * 16 ({@link JobSchedulerService#MAX_JOB_CONTEXTS_COUNT}) running at once, so an ArraySet is
+ * fine.
+ */
+ private final ArraySet<JobStatus> mTopStartedJobs = new ArraySet<>();
+
+ private final ActivityManagerInternal mActivityManagerInternal;
+ private final AlarmManager mAlarmManager;
+ private final ChargingTracker mChargeTracker;
+ private final Handler mHandler;
+ private final QcConstants mQcConstants;
+
+ private volatile boolean mInParole;
+
+ /**
+ * If the QuotaController should throttle apps based on their standby bucket and job activity.
+ * If false, all jobs will have their CONSTRAINT_WITHIN_QUOTA bit set to true immediately and
+ * indefinitely.
+ */
+ private boolean mShouldThrottle;
+
+ /** How much time each app will have to run jobs within their standby bucket window. */
+ private long mAllowedTimePerPeriodMs = QcConstants.DEFAULT_ALLOWED_TIME_PER_PERIOD_MS;
+
+ /**
+ * The maximum amount of time an app can have its jobs running within a {@link #MAX_PERIOD_MS}
+ * window.
+ */
+ private long mMaxExecutionTimeMs = QcConstants.DEFAULT_MAX_EXECUTION_TIME_MS;
+
+ /**
+ * How much time the app should have before transitioning from out-of-quota to in-quota.
+ * This should not affect processing if the app is already in-quota.
+ */
+ private long mQuotaBufferMs = QcConstants.DEFAULT_IN_QUOTA_BUFFER_MS;
+
+ /**
+ * {@link #mAllowedTimePerPeriodMs} - {@link #mQuotaBufferMs}. This can be used to determine
+ * when an app will have enough quota to transition from out-of-quota to in-quota.
+ */
+ private long mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
+
+ /**
+ * {@link #mMaxExecutionTimeMs} - {@link #mQuotaBufferMs}. This can be used to determine when an
+ * app will have enough quota to transition from out-of-quota to in-quota.
+ */
+ private long mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+
+ /** The period of time used to rate limit recently run jobs. */
+ private long mRateLimitingWindowMs = QcConstants.DEFAULT_RATE_LIMITING_WINDOW_MS;
+
+ /** The maximum number of jobs that can run within the past {@link #mRateLimitingWindowMs}. */
+ private int mMaxJobCountPerRateLimitingWindow =
+ QcConstants.DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
+
+ /**
+ * The maximum number of {@link TimingSession}s that can run within the past {@link
+ * #mRateLimitingWindowMs}.
+ */
+ private int mMaxSessionCountPerRateLimitingWindow =
+ QcConstants.DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
+
+ private long mNextCleanupTimeElapsed = 0;
+ private final AlarmManager.OnAlarmListener mSessionCleanupAlarmListener =
+ new AlarmManager.OnAlarmListener() {
+ @Override
+ public void onAlarm() {
+ mHandler.obtainMessage(MSG_CLEAN_UP_SESSIONS).sendToTarget();
+ }
+ };
+
+ private final IUidObserver mUidObserver = new IUidObserver.Stub() {
+ @Override
+ public void onUidStateChanged(int uid, int procState, long procStateSeq) {
+ mHandler.obtainMessage(MSG_UID_PROCESS_STATE_CHANGED, uid, procState).sendToTarget();
+ }
+
+ @Override
+ public void onUidGone(int uid, boolean disabled) {
+ }
+
+ @Override
+ public void onUidActive(int uid) {
+ }
+
+ @Override
+ public void onUidIdle(int uid, boolean disabled) {
+ }
+
+ @Override
+ public void onUidCachedChanged(int uid, boolean cached) {
+ }
+ };
+
+ private final BroadcastReceiver mPackageAddedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) {
+ return;
+ }
+ if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ return;
+ }
+ final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ synchronized (mLock) {
+ mUidToPackageCache.remove(uid);
+ }
+ }
+ };
+
+ /**
+ * The rolling window size for each standby bucket. Within each window, an app will have 10
+ * minutes to run its jobs.
+ */
+ private final long[] mBucketPeriodsMs = new long[]{
+ QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS,
+ QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS,
+ QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS,
+ QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS
+ };
+
+ /** The maximum period any bucket can have. */
+ private static final long MAX_PERIOD_MS = 24 * 60 * MINUTE_IN_MILLIS;
+
+ /**
+ * The maximum number of jobs based on its standby bucket. For each max value count in the
+ * array, the app will not be allowed to run more than that many number of jobs within the
+ * latest time interval of its rolling window size.
+ *
+ * @see #mBucketPeriodsMs
+ */
+ private final int[] mMaxBucketJobCounts = new int[]{
+ QcConstants.DEFAULT_MAX_JOB_COUNT_ACTIVE,
+ QcConstants.DEFAULT_MAX_JOB_COUNT_WORKING,
+ QcConstants.DEFAULT_MAX_JOB_COUNT_FREQUENT,
+ QcConstants.DEFAULT_MAX_JOB_COUNT_RARE
+ };
+
+ /**
+ * The maximum number of {@link TimingSession}s based on its standby bucket. For each max value
+ * count in the array, the app will not be allowed to have more than that many number of
+ * {@link TimingSession}s within the latest time interval of its rolling window size.
+ *
+ * @see #mBucketPeriodsMs
+ */
+ private final int[] mMaxBucketSessionCounts = new int[]{
+ QcConstants.DEFAULT_MAX_SESSION_COUNT_ACTIVE,
+ QcConstants.DEFAULT_MAX_SESSION_COUNT_WORKING,
+ QcConstants.DEFAULT_MAX_SESSION_COUNT_FREQUENT,
+ QcConstants.DEFAULT_MAX_SESSION_COUNT_RARE
+ };
+
+ /**
+ * Treat two distinct {@link TimingSession}s as the same if they start and end within this
+ * amount of time of each other.
+ */
+ private long mTimingSessionCoalescingDurationMs =
+ QcConstants.DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
+
+ /** An app has reached its quota. The message should contain a {@link Package} object. */
+ private static final int MSG_REACHED_QUOTA = 0;
+ /** Drop any old timing sessions. */
+ private static final int MSG_CLEAN_UP_SESSIONS = 1;
+ /** Check if a package is now within its quota. */
+ private static final int MSG_CHECK_PACKAGE = 2;
+ /** Process state for a UID has changed. */
+ private static final int MSG_UID_PROCESS_STATE_CHANGED = 3;
+
+ public QuotaController(JobSchedulerService service) {
+ super(service);
+ mHandler = new QcHandler(mContext.getMainLooper());
+ mChargeTracker = new ChargingTracker();
+ mChargeTracker.startTracking();
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+ mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ mQcConstants = new QcConstants(mHandler);
+
+ final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+ mContext.registerReceiverAsUser(mPackageAddedReceiver, UserHandle.ALL, filter, null, null);
+
+ // Set up the app standby bucketing tracker
+ UsageStatsManagerInternal usageStats = LocalServices.getService(
+ UsageStatsManagerInternal.class);
+ usageStats.addAppIdleStateChangeListener(new StandbyTracker());
+
+ try {
+ ActivityManager.getService().registerUidObserver(mUidObserver,
+ ActivityManager.UID_OBSERVER_PROCSTATE,
+ ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null);
+ } catch (RemoteException e) {
+ // ignored; both services live in system_server
+ }
+
+ mShouldThrottle = !mConstants.USE_HEARTBEATS;
+ }
+
+ @Override
+ public void onSystemServicesReady() {
+ mQcConstants.start(mContext.getContentResolver());
+ }
+
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) {
+ final int userId = jobStatus.getSourceUserId();
+ final String pkgName = jobStatus.getSourcePackageName();
+ // Still need to track jobs even if mShouldThrottle is false in case it's set to true at
+ // some point.
+ ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, pkgName);
+ if (jobs == null) {
+ jobs = new ArraySet<>();
+ mTrackedJobs.add(userId, pkgName, jobs);
+ }
+ jobs.add(jobStatus);
+ jobStatus.setTrackingController(JobStatus.TRACKING_QUOTA);
+ if (mShouldThrottle) {
+ final boolean isWithinQuota = isWithinQuotaLocked(jobStatus);
+ setConstraintSatisfied(jobStatus, isWithinQuota);
+ if (!isWithinQuota) {
+ maybeScheduleStartAlarmLocked(userId, pkgName,
+ getEffectiveStandbyBucket(jobStatus));
+ }
+ } else {
+ // QuotaController isn't throttling, so always set to true.
+ jobStatus.setQuotaConstraintSatisfied(true);
+ }
+ }
+
+ @Override
+ public void prepareForExecutionLocked(JobStatus jobStatus) {
+ if (DEBUG) {
+ Slog.d(TAG, "Prepping for " + jobStatus.toShortString());
+ }
+
+ final int uid = jobStatus.getSourceUid();
+ if (mActivityManagerInternal.getUidProcessState(uid) <= ActivityManager.PROCESS_STATE_TOP) {
+ if (DEBUG) {
+ Slog.d(TAG, jobStatus.toShortString() + " is top started job");
+ }
+ mTopStartedJobs.add(jobStatus);
+ // Top jobs won't count towards quota so there's no need to involve the Timer.
+ return;
+ }
+
+ final int userId = jobStatus.getSourceUserId();
+ final String packageName = jobStatus.getSourcePackageName();
+ Timer timer = mPkgTimers.get(userId, packageName);
+ if (timer == null) {
+ timer = new Timer(uid, userId, packageName);
+ mPkgTimers.add(userId, packageName, timer);
+ }
+ timer.startTrackingJobLocked(jobStatus);
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+ boolean forUpdate) {
+ if (jobStatus.clearTrackingController(JobStatus.TRACKING_QUOTA)) {
+ Timer timer = mPkgTimers.get(jobStatus.getSourceUserId(),
+ jobStatus.getSourcePackageName());
+ if (timer != null) {
+ timer.stopTrackingJob(jobStatus);
+ }
+ ArraySet<JobStatus> jobs = mTrackedJobs.get(jobStatus.getSourceUserId(),
+ jobStatus.getSourcePackageName());
+ if (jobs != null) {
+ jobs.remove(jobStatus);
+ }
+ mTopStartedJobs.remove(jobStatus);
+ }
+ }
+
+ @Override
+ public void onConstantsUpdatedLocked() {
+ if (mShouldThrottle == mConstants.USE_HEARTBEATS) {
+ mShouldThrottle = !mConstants.USE_HEARTBEATS;
+
+ // Update job bookkeeping out of band.
+ BackgroundThread.getHandler().post(() -> {
+ synchronized (mLock) {
+ maybeUpdateAllConstraintsLocked();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onAppRemovedLocked(String packageName, int uid) {
+ if (packageName == null) {
+ Slog.wtf(TAG, "Told app removed but given null package name.");
+ return;
+ }
+ final int userId = UserHandle.getUserId(uid);
+ mTrackedJobs.delete(userId, packageName);
+ Timer timer = mPkgTimers.get(userId, packageName);
+ if (timer != null) {
+ if (timer.isActive()) {
+ Slog.wtf(TAG, "onAppRemovedLocked called before Timer turned off.");
+ timer.dropEverythingLocked();
+ }
+ mPkgTimers.delete(userId, packageName);
+ }
+ mTimingSessions.delete(userId, packageName);
+ QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+ if (alarmListener != null) {
+ mAlarmManager.cancel(alarmListener);
+ mInQuotaAlarmListeners.delete(userId, packageName);
+ }
+ mExecutionStatsCache.delete(userId, packageName);
+ mForegroundUids.delete(uid);
+ mUidToPackageCache.remove(uid);
+ }
+
+ @Override
+ public void onUserRemovedLocked(int userId) {
+ mTrackedJobs.delete(userId);
+ mPkgTimers.delete(userId);
+ mTimingSessions.delete(userId);
+ mInQuotaAlarmListeners.delete(userId);
+ mExecutionStatsCache.delete(userId);
+ mUidToPackageCache.clear();
+ }
+
+ private boolean isUidInForeground(int uid) {
+ if (UserHandle.isCore(uid)) {
+ return true;
+ }
+ synchronized (mLock) {
+ return mForegroundUids.get(uid);
+ }
+ }
+
+ /** @return true if the job was started while the app was in the TOP state. */
+ private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) {
+ return mTopStartedJobs.contains(jobStatus);
+ }
+
+ /** Returns the maximum amount of time this job could run for. */
+ public long getMaxJobExecutionTimeMsLocked(@NonNull final JobStatus jobStatus) {
+ // If quota is currently "free", then the job can run for the full amount of time.
+ if (mChargeTracker.isCharging()
+ || mInParole
+ || isTopStartedJobLocked(jobStatus)
+ || isUidInForeground(jobStatus.getSourceUid())) {
+ return JobServiceContext.EXECUTING_TIMESLICE_MILLIS;
+ }
+ return getRemainingExecutionTimeLocked(jobStatus);
+ }
+
+ /**
+ * Returns an appropriate standby bucket for the job, taking into account any standby
+ * exemptions.
+ */
+ private int getEffectiveStandbyBucket(@NonNull final JobStatus jobStatus) {
+ if (jobStatus.uidActive || jobStatus.getJob().isExemptedFromAppStandby()) {
+ // Treat these cases as if they're in the ACTIVE bucket so that they get throttled
+ // like other ACTIVE apps.
+ return ACTIVE_INDEX;
+ }
+ return jobStatus.getStandbyBucket();
+ }
+
+ @VisibleForTesting
+ boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) {
+ final int standbyBucket = getEffectiveStandbyBucket(jobStatus);
+ // A job is within quota if one of the following is true:
+ // 1. it was started while the app was in the TOP state
+ // 2. the app is currently in the foreground
+ // 3. the app overall is within its quota
+ return isTopStartedJobLocked(jobStatus)
+ || isUidInForeground(jobStatus.getSourceUid())
+ || isWithinQuotaLocked(
+ jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
+ }
+
+ @VisibleForTesting
+ boolean isWithinQuotaLocked(final int userId, @NonNull final String packageName,
+ final int standbyBucket) {
+ if (standbyBucket == NEVER_INDEX) return false;
+ // This check is needed in case the flag is toggled after a job has been registered.
+ if (!mShouldThrottle) return true;
+
+ // Quota constraint is not enforced while charging or when parole is on.
+ if (mChargeTracker.isCharging() || mInParole) {
+ return true;
+ }
+
+ ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+ return getRemainingExecutionTimeLocked(stats) > 0
+ && isUnderJobCountQuotaLocked(stats, standbyBucket)
+ && isUnderSessionCountQuotaLocked(stats, standbyBucket);
+ }
+
+ private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats,
+ final int standbyBucket) {
+ final long now = sElapsedRealtimeClock.millis();
+ final boolean isUnderAllowedTimeQuota =
+ (stats.jobRateLimitExpirationTimeElapsed <= now
+ || stats.jobCountInRateLimitingWindow < mMaxJobCountPerRateLimitingWindow);
+ return isUnderAllowedTimeQuota
+ && (stats.bgJobCountInWindow < mMaxBucketJobCounts[standbyBucket]);
+ }
+
+ private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats,
+ final int standbyBucket) {
+ final long now = sElapsedRealtimeClock.millis();
+ final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now
+ || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow);
+ return isUnderAllowedTimeQuota
+ && stats.sessionCountInWindow < mMaxBucketSessionCounts[standbyBucket];
+ }
+
+ @VisibleForTesting
+ long getRemainingExecutionTimeLocked(@NonNull final JobStatus jobStatus) {
+ return getRemainingExecutionTimeLocked(jobStatus.getSourceUserId(),
+ jobStatus.getSourcePackageName(),
+ getEffectiveStandbyBucket(jobStatus));
+ }
+
+ @VisibleForTesting
+ long getRemainingExecutionTimeLocked(final int userId, @NonNull final String packageName) {
+ final int standbyBucket = JobSchedulerService.standbyBucketForPackage(packageName,
+ userId, sElapsedRealtimeClock.millis());
+ return getRemainingExecutionTimeLocked(userId, packageName, standbyBucket);
+ }
+
+ /**
+ * Returns the amount of time, in milliseconds, that this job has remaining to run based on its
+ * current standby bucket. Time remaining could be negative if the app was moved from a less
+ * restricted to a more restricted bucket.
+ */
+ private long getRemainingExecutionTimeLocked(final int userId,
+ @NonNull final String packageName, final int standbyBucket) {
+ if (standbyBucket == NEVER_INDEX) {
+ return 0;
+ }
+ return getRemainingExecutionTimeLocked(
+ getExecutionStatsLocked(userId, packageName, standbyBucket));
+ }
+
+ private long getRemainingExecutionTimeLocked(@NonNull ExecutionStats stats) {
+ return Math.min(mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs,
+ mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs);
+ }
+
+ /**
+ * Returns the amount of time, in milliseconds, until the package would have reached its
+ * duration quota, assuming it has a job counting towards its quota the entire time. This takes
+ * into account any {@link TimingSession}s that may roll out of the window as the job is
+ * running.
+ */
+ @VisibleForTesting
+ long getTimeUntilQuotaConsumedLocked(final int userId, @NonNull final String packageName) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final int standbyBucket = JobSchedulerService.standbyBucketForPackage(
+ packageName, userId, nowElapsed);
+ if (standbyBucket == NEVER_INDEX) {
+ return 0;
+ }
+ List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
+ if (sessions == null || sessions.size() == 0) {
+ return mAllowedTimePerPeriodMs;
+ }
+
+ final ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+ final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
+ final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
+ final long allowedTimeRemainingMs = mAllowedTimePerPeriodMs - stats.executionTimeInWindowMs;
+ final long maxExecutionTimeRemainingMs =
+ mMaxExecutionTimeMs - stats.executionTimeInMaxPeriodMs;
+
+ // Regular ACTIVE case. Since the bucket size equals the allowed time, the app jobs can
+ // essentially run until they reach the maximum limit.
+ if (stats.windowSizeMs == mAllowedTimePerPeriodMs) {
+ return calculateTimeUntilQuotaConsumedLocked(
+ sessions, startMaxElapsed, maxExecutionTimeRemainingMs);
+ }
+
+ // Need to check both max time and period time in case one is less than the other.
+ // For example, max time remaining could be less than bucket time remaining, but sessions
+ // contributing to the max time remaining could phase out enough that we'd want to use the
+ // bucket value.
+ return Math.min(
+ calculateTimeUntilQuotaConsumedLocked(
+ sessions, startMaxElapsed, maxExecutionTimeRemainingMs),
+ calculateTimeUntilQuotaConsumedLocked(
+ sessions, startWindowElapsed, allowedTimeRemainingMs));
+ }
+
+ /**
+ * Calculates how much time it will take, in milliseconds, until the quota is fully consumed.
+ *
+ * @param windowStartElapsed The start of the window, in the elapsed realtime timebase.
+ * @param deadSpaceMs How much time can be allowed to count towards the quota
+ */
+ private long calculateTimeUntilQuotaConsumedLocked(@NonNull List<TimingSession> sessions,
+ final long windowStartElapsed, long deadSpaceMs) {
+ long timeUntilQuotaConsumedMs = 0;
+ long start = windowStartElapsed;
+ for (int i = 0; i < sessions.size(); ++i) {
+ TimingSession session = sessions.get(i);
+
+ if (session.endTimeElapsed < windowStartElapsed) {
+ // Outside of window. Ignore.
+ continue;
+ } else if (session.startTimeElapsed <= windowStartElapsed) {
+ // Overlapping session. Can extend time by portion of session in window.
+ timeUntilQuotaConsumedMs += session.endTimeElapsed - windowStartElapsed;
+ start = session.endTimeElapsed;
+ } else {
+ // Completely within the window. Can only consider if there's enough dead space
+ // to get to the start of the session.
+ long diff = session.startTimeElapsed - start;
+ if (diff > deadSpaceMs) {
+ break;
+ }
+ timeUntilQuotaConsumedMs += diff
+ + (session.endTimeElapsed - session.startTimeElapsed);
+ deadSpaceMs -= diff;
+ start = session.endTimeElapsed;
+ }
+ }
+ // Will be non-zero if the loop didn't look at any sessions.
+ timeUntilQuotaConsumedMs += deadSpaceMs;
+ if (timeUntilQuotaConsumedMs > mMaxExecutionTimeMs) {
+ Slog.wtf(TAG, "Calculated quota consumed time too high: " + timeUntilQuotaConsumedMs);
+ }
+ return timeUntilQuotaConsumedMs;
+ }
+
+ /** Returns the execution stats of the app in the most recent window. */
+ @VisibleForTesting
+ @NonNull
+ ExecutionStats getExecutionStatsLocked(final int userId, @NonNull final String packageName,
+ final int standbyBucket) {
+ return getExecutionStatsLocked(userId, packageName, standbyBucket, true);
+ }
+
+ @NonNull
+ private ExecutionStats getExecutionStatsLocked(final int userId,
+ @NonNull final String packageName, final int standbyBucket,
+ final boolean refreshStatsIfOld) {
+ if (standbyBucket == NEVER_INDEX) {
+ Slog.wtf(TAG, "getExecutionStatsLocked called for a NEVER app.");
+ return new ExecutionStats();
+ }
+ ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+ if (appStats == null) {
+ appStats = new ExecutionStats[mBucketPeriodsMs.length];
+ mExecutionStatsCache.add(userId, packageName, appStats);
+ }
+ ExecutionStats stats = appStats[standbyBucket];
+ if (stats == null) {
+ stats = new ExecutionStats();
+ appStats[standbyBucket] = stats;
+ }
+ if (refreshStatsIfOld) {
+ final long bucketWindowSizeMs = mBucketPeriodsMs[standbyBucket];
+ final int jobCountLimit = mMaxBucketJobCounts[standbyBucket];
+ final int sessionCountLimit = mMaxBucketSessionCounts[standbyBucket];
+ Timer timer = mPkgTimers.get(userId, packageName);
+ if ((timer != null && timer.isActive())
+ || stats.expirationTimeElapsed <= sElapsedRealtimeClock.millis()
+ || stats.windowSizeMs != bucketWindowSizeMs
+ || stats.jobCountLimit != jobCountLimit
+ || stats.sessionCountLimit != sessionCountLimit) {
+ // The stats are no longer valid.
+ stats.windowSizeMs = bucketWindowSizeMs;
+ stats.jobCountLimit = jobCountLimit;
+ stats.sessionCountLimit = sessionCountLimit;
+ updateExecutionStatsLocked(userId, packageName, stats);
+ }
+ }
+
+ return stats;
+ }
+
+ @VisibleForTesting
+ void updateExecutionStatsLocked(final int userId, @NonNull final String packageName,
+ @NonNull ExecutionStats stats) {
+ stats.executionTimeInWindowMs = 0;
+ stats.bgJobCountInWindow = 0;
+ stats.executionTimeInMaxPeriodMs = 0;
+ stats.bgJobCountInMaxPeriod = 0;
+ stats.sessionCountInWindow = 0;
+ stats.inQuotaTimeElapsed = 0;
+
+ Timer timer = mPkgTimers.get(userId, packageName);
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ stats.expirationTimeElapsed = nowElapsed + MAX_PERIOD_MS;
+ if (timer != null && timer.isActive()) {
+ stats.executionTimeInWindowMs =
+ stats.executionTimeInMaxPeriodMs = timer.getCurrentDuration(nowElapsed);
+ stats.bgJobCountInWindow = stats.bgJobCountInMaxPeriod = timer.getBgJobCount();
+ // If the timer is active, the value will be stale at the next method call, so
+ // invalidate now.
+ stats.expirationTimeElapsed = nowElapsed;
+ if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ nowElapsed - mAllowedTimeIntoQuotaMs + stats.windowSizeMs);
+ }
+ if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ nowElapsed - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
+ }
+ }
+
+ List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
+ if (sessions == null || sessions.size() == 0) {
+ return;
+ }
+
+ final long startWindowElapsed = nowElapsed - stats.windowSizeMs;
+ final long startMaxElapsed = nowElapsed - MAX_PERIOD_MS;
+ int sessionCountInWindow = 0;
+ // The minimum time between the start time and the beginning of the sessions that were
+ // looked at --> how much time the stats will be valid for.
+ long emptyTimeMs = Long.MAX_VALUE;
+ // Sessions are non-overlapping and in order of occurrence, so iterating backwards will get
+ // the most recent ones.
+ final int loopStart = sessions.size() - 1;
+ for (int i = loopStart; i >= 0; --i) {
+ TimingSession session = sessions.get(i);
+
+ // Window management.
+ if (startWindowElapsed < session.endTimeElapsed) {
+ final long start;
+ if (startWindowElapsed < session.startTimeElapsed) {
+ start = session.startTimeElapsed;
+ emptyTimeMs =
+ Math.min(emptyTimeMs, session.startTimeElapsed - startWindowElapsed);
+ } else {
+ // The session started before the window but ended within the window. Only
+ // include the portion that was within the window.
+ start = startWindowElapsed;
+ emptyTimeMs = 0;
+ }
+
+ stats.executionTimeInWindowMs += session.endTimeElapsed - start;
+ stats.bgJobCountInWindow += session.bgJobCount;
+ if (stats.executionTimeInWindowMs >= mAllowedTimeIntoQuotaMs) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ start + stats.executionTimeInWindowMs - mAllowedTimeIntoQuotaMs
+ + stats.windowSizeMs);
+ }
+ if (stats.bgJobCountInWindow >= stats.jobCountLimit) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ session.endTimeElapsed + stats.windowSizeMs);
+ }
+ if (i == loopStart
+ || (sessions.get(i + 1).startTimeElapsed - session.endTimeElapsed)
+ > mTimingSessionCoalescingDurationMs) {
+ // Coalesce sessions if they are very close to each other in time
+ sessionCountInWindow++;
+
+ if (sessionCountInWindow >= stats.sessionCountLimit) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ session.endTimeElapsed + stats.windowSizeMs);
+ }
+ }
+ }
+
+ // Max period check.
+ if (startMaxElapsed < session.startTimeElapsed) {
+ stats.executionTimeInMaxPeriodMs +=
+ session.endTimeElapsed - session.startTimeElapsed;
+ stats.bgJobCountInMaxPeriod += session.bgJobCount;
+ emptyTimeMs = Math.min(emptyTimeMs, session.startTimeElapsed - startMaxElapsed);
+ if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ session.startTimeElapsed + stats.executionTimeInMaxPeriodMs
+ - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
+ }
+ } else if (startMaxElapsed < session.endTimeElapsed) {
+ // The session started before the window but ended within the window. Only include
+ // the portion that was within the window.
+ stats.executionTimeInMaxPeriodMs += session.endTimeElapsed - startMaxElapsed;
+ stats.bgJobCountInMaxPeriod += session.bgJobCount;
+ emptyTimeMs = 0;
+ if (stats.executionTimeInMaxPeriodMs >= mMaxExecutionTimeIntoQuotaMs) {
+ stats.inQuotaTimeElapsed = Math.max(stats.inQuotaTimeElapsed,
+ startMaxElapsed + stats.executionTimeInMaxPeriodMs
+ - mMaxExecutionTimeIntoQuotaMs + MAX_PERIOD_MS);
+ }
+ } else {
+ // This session ended before the window. No point in going any further.
+ break;
+ }
+ }
+ stats.expirationTimeElapsed = nowElapsed + emptyTimeMs;
+ stats.sessionCountInWindow = sessionCountInWindow;
+ }
+
+ /** Invalidate ExecutionStats for all apps. */
+ @VisibleForTesting
+ void invalidateAllExecutionStatsLocked() {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ mExecutionStatsCache.forEach((appStats) -> {
+ if (appStats != null) {
+ for (int i = 0; i < appStats.length; ++i) {
+ ExecutionStats stats = appStats[i];
+ if (stats != null) {
+ stats.expirationTimeElapsed = nowElapsed;
+ }
+ }
+ }
+ });
+ }
+
+ @VisibleForTesting
+ void invalidateAllExecutionStatsLocked(final int userId,
+ @NonNull final String packageName) {
+ ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+ if (appStats != null) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ for (int i = 0; i < appStats.length; ++i) {
+ ExecutionStats stats = appStats[i];
+ if (stats != null) {
+ stats.expirationTimeElapsed = nowElapsed;
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void incrementJobCount(final int userId, @NonNull final String packageName, int count) {
+ final long now = sElapsedRealtimeClock.millis();
+ ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+ if (appStats == null) {
+ appStats = new ExecutionStats[mBucketPeriodsMs.length];
+ mExecutionStatsCache.add(userId, packageName, appStats);
+ }
+ for (int i = 0; i < appStats.length; ++i) {
+ ExecutionStats stats = appStats[i];
+ if (stats == null) {
+ stats = new ExecutionStats();
+ appStats[i] = stats;
+ }
+ if (stats.jobRateLimitExpirationTimeElapsed <= now) {
+ stats.jobRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
+ stats.jobCountInRateLimitingWindow = 0;
+ }
+ stats.jobCountInRateLimitingWindow += count;
+ }
+ }
+
+ private void incrementTimingSessionCount(final int userId, @NonNull final String packageName) {
+ final long now = sElapsedRealtimeClock.millis();
+ ExecutionStats[] appStats = mExecutionStatsCache.get(userId, packageName);
+ if (appStats == null) {
+ appStats = new ExecutionStats[mBucketPeriodsMs.length];
+ mExecutionStatsCache.add(userId, packageName, appStats);
+ }
+ for (int i = 0; i < appStats.length; ++i) {
+ ExecutionStats stats = appStats[i];
+ if (stats == null) {
+ stats = new ExecutionStats();
+ appStats[i] = stats;
+ }
+ if (stats.sessionRateLimitExpirationTimeElapsed <= now) {
+ stats.sessionRateLimitExpirationTimeElapsed = now + mRateLimitingWindowMs;
+ stats.sessionCountInRateLimitingWindow = 0;
+ }
+ stats.sessionCountInRateLimitingWindow++;
+ }
+ }
+
+ @VisibleForTesting
+ void saveTimingSession(final int userId, @NonNull final String packageName,
+ @NonNull final TimingSession session) {
+ synchronized (mLock) {
+ List<TimingSession> sessions = mTimingSessions.get(userId, packageName);
+ if (sessions == null) {
+ sessions = new ArrayList<>();
+ mTimingSessions.add(userId, packageName, sessions);
+ }
+ sessions.add(session);
+ // Adding a new session means that the current stats are now incorrect.
+ invalidateAllExecutionStatsLocked(userId, packageName);
+
+ maybeScheduleCleanupAlarmLocked();
+ }
+ }
+
+ private final class EarliestEndTimeFunctor implements Consumer<List<TimingSession>> {
+ public long earliestEndElapsed = Long.MAX_VALUE;
+
+ @Override
+ public void accept(List<TimingSession> sessions) {
+ if (sessions != null && sessions.size() > 0) {
+ earliestEndElapsed = Math.min(earliestEndElapsed, sessions.get(0).endTimeElapsed);
+ }
+ }
+
+ void reset() {
+ earliestEndElapsed = Long.MAX_VALUE;
+ }
+ }
+
+ private final EarliestEndTimeFunctor mEarliestEndTimeFunctor = new EarliestEndTimeFunctor();
+
+ /** Schedule a cleanup alarm if necessary and there isn't already one scheduled. */
+ @VisibleForTesting
+ void maybeScheduleCleanupAlarmLocked() {
+ if (mNextCleanupTimeElapsed > sElapsedRealtimeClock.millis()) {
+ // There's already an alarm scheduled. Just stick with that one. There's no way we'll
+ // end up scheduling an earlier alarm.
+ if (DEBUG) {
+ Slog.v(TAG, "Not scheduling cleanup since there's already one at "
+ + mNextCleanupTimeElapsed + " (in " + (mNextCleanupTimeElapsed
+ - sElapsedRealtimeClock.millis()) + "ms)");
+ }
+ return;
+ }
+ mEarliestEndTimeFunctor.reset();
+ mTimingSessions.forEach(mEarliestEndTimeFunctor);
+ final long earliestEndElapsed = mEarliestEndTimeFunctor.earliestEndElapsed;
+ if (earliestEndElapsed == Long.MAX_VALUE) {
+ // Couldn't find a good time to clean up. Maybe this was called after we deleted all
+ // timing sessions.
+ if (DEBUG) {
+ Slog.d(TAG, "Didn't find a time to schedule cleanup");
+ }
+ return;
+ }
+ // Need to keep sessions for all apps up to the max period, regardless of their current
+ // standby bucket.
+ long nextCleanupElapsed = earliestEndElapsed + MAX_PERIOD_MS;
+ if (nextCleanupElapsed - mNextCleanupTimeElapsed <= 10 * MINUTE_IN_MILLIS) {
+ // No need to clean up too often. Delay the alarm if the next cleanup would be too soon
+ // after it.
+ nextCleanupElapsed += 10 * MINUTE_IN_MILLIS;
+ }
+ mNextCleanupTimeElapsed = nextCleanupElapsed;
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextCleanupElapsed, ALARM_TAG_CLEANUP,
+ mSessionCleanupAlarmListener, mHandler);
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduled next cleanup for " + mNextCleanupTimeElapsed);
+ }
+ }
+
+ private void handleNewChargingStateLocked() {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final boolean isCharging = mChargeTracker.isCharging();
+ if (DEBUG) {
+ Slog.d(TAG, "handleNewChargingStateLocked: " + isCharging);
+ }
+ // Deal with Timers first.
+ mPkgTimers.forEach((t) -> t.onStateChangedLocked(nowElapsed, isCharging));
+ // Now update jobs.
+ maybeUpdateAllConstraintsLocked();
+ }
+
+ private void maybeUpdateAllConstraintsLocked() {
+ boolean changed = false;
+ for (int u = 0; u < mTrackedJobs.numUsers(); ++u) {
+ final int userId = mTrackedJobs.keyAt(u);
+ for (int p = 0; p < mTrackedJobs.numPackagesForUser(userId); ++p) {
+ final String packageName = mTrackedJobs.keyAt(u, p);
+ changed |= maybeUpdateConstraintForPkgLocked(userId, packageName);
+ }
+ }
+ if (changed) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+
+ /**
+ * Update the CONSTRAINT_WITHIN_QUOTA bit for all of the Jobs for a given package.
+ *
+ * @return true if at least one job had its bit changed
+ */
+ private boolean maybeUpdateConstraintForPkgLocked(final int userId,
+ @NonNull final String packageName) {
+ ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName);
+ if (jobs == null || jobs.size() == 0) {
+ return false;
+ }
+
+ // Quota is the same for all jobs within a package.
+ final int realStandbyBucket = jobs.valueAt(0).getStandbyBucket();
+ final boolean realInQuota = isWithinQuotaLocked(userId, packageName, realStandbyBucket);
+ boolean changed = false;
+ for (int i = jobs.size() - 1; i >= 0; --i) {
+ final JobStatus js = jobs.valueAt(i);
+ if (isTopStartedJobLocked(js)) {
+ // Job was started while the app was in the TOP state so we should allow it to
+ // finish.
+ changed |= js.setQuotaConstraintSatisfied(true);
+ } else if (realStandbyBucket != ACTIVE_INDEX
+ && realStandbyBucket == getEffectiveStandbyBucket(js)) {
+ // An app in the ACTIVE bucket may be out of quota while the job could be in quota
+ // for some reason. Therefore, avoid setting the real value here and check each job
+ // individually.
+ changed |= setConstraintSatisfied(js, realInQuota);
+ } else {
+ // This job is somehow exempted. Need to determine its own quota status.
+ changed |= setConstraintSatisfied(js, isWithinQuotaLocked(js));
+ }
+ }
+ if (!realInQuota) {
+ // Don't want to use the effective standby bucket here since that bump the bucket to
+ // ACTIVE for one of the jobs, which doesn't help with other jobs that aren't
+ // exempted.
+ maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
+ } else {
+ QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+ if (alarmListener != null && alarmListener.isWaiting()) {
+ mAlarmManager.cancel(alarmListener);
+ // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
+ alarmListener.setTriggerTime(0);
+ }
+ }
+ return changed;
+ }
+
+ private class UidConstraintUpdater implements Consumer<JobStatus> {
+ private final UserPackageMap<Integer> mToScheduleStartAlarms = new UserPackageMap<>();
+ public boolean wasJobChanged;
+
+ @Override
+ public void accept(JobStatus jobStatus) {
+ wasJobChanged |= setConstraintSatisfied(jobStatus, isWithinQuotaLocked(jobStatus));
+ final int userId = jobStatus.getSourceUserId();
+ final String packageName = jobStatus.getSourcePackageName();
+ final int realStandbyBucket = jobStatus.getStandbyBucket();
+ if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
+ QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+ if (alarmListener != null && alarmListener.isWaiting()) {
+ mAlarmManager.cancel(alarmListener);
+ // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
+ alarmListener.setTriggerTime(0);
+ }
+ } else {
+ mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
+ }
+ }
+
+ void postProcess() {
+ for (int u = 0; u < mToScheduleStartAlarms.numUsers(); ++u) {
+ final int userId = mToScheduleStartAlarms.keyAt(u);
+ for (int p = 0; p < mToScheduleStartAlarms.numPackagesForUser(userId); ++p) {
+ final String packageName = mToScheduleStartAlarms.keyAt(u, p);
+ final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName);
+ maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket);
+ }
+ }
+ }
+
+ void reset() {
+ wasJobChanged = false;
+ mToScheduleStartAlarms.clear();
+ }
+ }
+
+ private final UidConstraintUpdater mUpdateUidConstraints = new UidConstraintUpdater();
+
+ private boolean maybeUpdateConstraintForUidLocked(final int uid) {
+ mService.getJobStore().forEachJobForSourceUid(uid, mUpdateUidConstraints);
+
+ mUpdateUidConstraints.postProcess();
+ boolean changed = mUpdateUidConstraints.wasJobChanged;
+ mUpdateUidConstraints.reset();
+ return changed;
+ }
+
+ /**
+ * Maybe schedule a non-wakeup alarm for the next time this package will have quota to run
+ * again. This should only be called if the package is already out of quota.
+ */
+ @VisibleForTesting
+ void maybeScheduleStartAlarmLocked(final int userId, @NonNull final String packageName,
+ final int standbyBucket) {
+ if (standbyBucket == NEVER_INDEX) {
+ return;
+ }
+
+ final String pkgString = string(userId, packageName);
+ ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
+ final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket);
+ final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats,
+ standbyBucket);
+
+ QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
+ if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
+ && stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
+ && isUnderJobCountQuota
+ && isUnderTimingSessionCountQuota) {
+ // Already in quota. Why was this method called?
+ if (DEBUG) {
+ Slog.e(TAG, "maybeScheduleStartAlarmLocked called for " + pkgString
+ + " even though it already has "
+ + getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
+ + "ms in its quota.");
+ }
+ if (alarmListener != null) {
+ // Cancel any pending alarm.
+ mAlarmManager.cancel(alarmListener);
+ // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
+ alarmListener.setTriggerTime(0);
+ }
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
+ return;
+ }
+
+ if (alarmListener == null) {
+ alarmListener = new QcAlarmListener(userId, packageName);
+ mInQuotaAlarmListeners.add(userId, packageName, alarmListener);
+ }
+
+ // The time this app will have quota again.
+ long inQuotaTimeElapsed = stats.inQuotaTimeElapsed;
+ if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) {
+ // App hit the rate limit.
+ inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
+ stats.jobRateLimitExpirationTimeElapsed);
+ }
+ if (!isUnderTimingSessionCountQuota
+ && stats.sessionCountInWindow < stats.sessionCountLimit) {
+ // App hit the rate limit.
+ inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
+ stats.sessionRateLimitExpirationTimeElapsed);
+ }
+ // Only schedule the alarm if:
+ // 1. There isn't one currently scheduled
+ // 2. The new alarm is significantly earlier than the previous alarm (which could be the
+ // case if the package moves into a higher standby bucket). If it's earlier but not
+ // significantly so, then we essentially delay the job a few extra minutes.
+ // 3. The alarm is after the current alarm by more than the quota buffer.
+ // TODO: this might be overengineering. Simplify if proven safe.
+ if (!alarmListener.isWaiting()
+ || inQuotaTimeElapsed < alarmListener.getTriggerTimeElapsed() - 3 * MINUTE_IN_MILLIS
+ || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed) {
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduling start alarm for " + pkgString);
+ }
+ // If the next time this app will have quota is at least 3 minutes before the
+ // alarm is supposed to go off, reschedule the alarm.
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, inQuotaTimeElapsed,
+ ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler);
+ alarmListener.setTriggerTime(inQuotaTimeElapsed);
+ } else if (DEBUG) {
+ Slog.d(TAG, "No need to schedule start alarm for " + pkgString);
+ }
+ }
+
+ private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, boolean isWithinQuota) {
+ if (!isWithinQuota && jobStatus.getWhenStandbyDeferred() == 0) {
+ // Mark that the job is being deferred due to buckets.
+ jobStatus.setWhenStandbyDeferred(sElapsedRealtimeClock.millis());
+ }
+ return jobStatus.setQuotaConstraintSatisfied(isWithinQuota);
+ }
+
+ private final class ChargingTracker extends BroadcastReceiver {
+ /**
+ * Track whether we're charging. This has a slightly different definition than that of
+ * BatteryController.
+ */
+ private boolean mCharging;
+
+ ChargingTracker() {
+ }
+
+ public void startTracking() {
+ IntentFilter filter = new IntentFilter();
+
+ // Charging/not charging.
+ filter.addAction(BatteryManager.ACTION_CHARGING);
+ filter.addAction(BatteryManager.ACTION_DISCHARGING);
+ mContext.registerReceiver(this, filter);
+
+ // Initialise tracker state.
+ BatteryManagerInternal batteryManagerInternal =
+ LocalServices.getService(BatteryManagerInternal.class);
+ mCharging = batteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
+ }
+
+ public boolean isCharging() {
+ return mCharging;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ synchronized (mLock) {
+ final String action = intent.getAction();
+ if (BatteryManager.ACTION_CHARGING.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Received charging intent, fired @ "
+ + sElapsedRealtimeClock.millis());
+ }
+ mCharging = true;
+ handleNewChargingStateLocked();
+ } else if (BatteryManager.ACTION_DISCHARGING.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Disconnected from power.");
+ }
+ mCharging = false;
+ handleNewChargingStateLocked();
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static final class TimingSession {
+ // Start timestamp in elapsed realtime timebase.
+ public final long startTimeElapsed;
+ // End timestamp in elapsed realtime timebase.
+ public final long endTimeElapsed;
+ // How many background jobs ran during this session.
+ public final int bgJobCount;
+
+ private final int mHashCode;
+
+ TimingSession(long startElapsed, long endElapsed, int bgJobCount) {
+ this.startTimeElapsed = startElapsed;
+ this.endTimeElapsed = endElapsed;
+ this.bgJobCount = bgJobCount;
+
+ int hashCode = 0;
+ hashCode = 31 * hashCode + hashLong(startTimeElapsed);
+ hashCode = 31 * hashCode + hashLong(endTimeElapsed);
+ hashCode = 31 * hashCode + bgJobCount;
+ mHashCode = hashCode;
+ }
+
+ @Override
+ public String toString() {
+ return "TimingSession{" + startTimeElapsed + "->" + endTimeElapsed + ", " + bgJobCount
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof TimingSession) {
+ TimingSession other = (TimingSession) obj;
+ return startTimeElapsed == other.startTimeElapsed
+ && endTimeElapsed == other.endTimeElapsed
+ && bgJobCount == other.bgJobCount;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ pw.print(startTimeElapsed);
+ pw.print(" -> ");
+ pw.print(endTimeElapsed);
+ pw.print(" (");
+ pw.print(endTimeElapsed - startTimeElapsed);
+ pw.print("), ");
+ pw.print(bgJobCount);
+ pw.print(" bg jobs.");
+ pw.println();
+ }
+
+ public void dump(@NonNull ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(StateControllerProto.QuotaController.TimingSession.START_TIME_ELAPSED,
+ startTimeElapsed);
+ proto.write(StateControllerProto.QuotaController.TimingSession.END_TIME_ELAPSED,
+ endTimeElapsed);
+ proto.write(StateControllerProto.QuotaController.TimingSession.BG_JOB_COUNT,
+ bgJobCount);
+
+ proto.end(token);
+ }
+ }
+
+ private final class Timer {
+ private final Package mPkg;
+ private final int mUid;
+
+ // List of jobs currently running for this app that started when the app wasn't in the
+ // foreground.
+ private final ArraySet<JobStatus> mRunningBgJobs = new ArraySet<>();
+ private long mStartTimeElapsed;
+ private int mBgJobCount;
+
+ Timer(int uid, int userId, String packageName) {
+ mPkg = new Package(userId, packageName);
+ mUid = uid;
+ }
+
+ void startTrackingJobLocked(@NonNull JobStatus jobStatus) {
+ if (isTopStartedJobLocked(jobStatus)) {
+ // We intentionally don't pay attention to fg state changes after a TOP job has
+ // started.
+ if (DEBUG) {
+ Slog.v(TAG,
+ "Timer ignoring " + jobStatus.toShortString() + " because isTop");
+ }
+ return;
+ }
+ if (DEBUG) {
+ Slog.v(TAG, "Starting to track " + jobStatus.toShortString());
+ }
+ // Always track jobs, even when charging.
+ mRunningBgJobs.add(jobStatus);
+ if (shouldTrackLocked()) {
+ mBgJobCount++;
+ incrementJobCount(mPkg.userId, mPkg.packageName, 1);
+ if (mRunningBgJobs.size() == 1) {
+ // Started tracking the first job.
+ mStartTimeElapsed = sElapsedRealtimeClock.millis();
+ // Starting the timer means that all cached execution stats are now incorrect.
+ invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
+ scheduleCutoff();
+ }
+ }
+ }
+
+ void stopTrackingJob(@NonNull JobStatus jobStatus) {
+ if (DEBUG) {
+ Slog.v(TAG, "Stopping tracking of " + jobStatus.toShortString());
+ }
+ synchronized (mLock) {
+ if (mRunningBgJobs.size() == 0) {
+ // maybeStopTrackingJobLocked can be called when an app cancels a job, so a
+ // timer may not be running when it's asked to stop tracking a job.
+ if (DEBUG) {
+ Slog.d(TAG, "Timer isn't tracking any jobs but still told to stop");
+ }
+ return;
+ }
+ if (mRunningBgJobs.remove(jobStatus)
+ && !mChargeTracker.isCharging() && mRunningBgJobs.size() == 0) {
+ emitSessionLocked(sElapsedRealtimeClock.millis());
+ cancelCutoff();
+ }
+ }
+ }
+
+ /**
+ * Stops tracking all jobs and cancels any pending alarms. This should only be called if
+ * the Timer is not going to be used anymore.
+ */
+ void dropEverythingLocked() {
+ mRunningBgJobs.clear();
+ cancelCutoff();
+ }
+
+ private void emitSessionLocked(long nowElapsed) {
+ if (mBgJobCount <= 0) {
+ // Nothing to emit.
+ return;
+ }
+ TimingSession ts = new TimingSession(mStartTimeElapsed, nowElapsed, mBgJobCount);
+ saveTimingSession(mPkg.userId, mPkg.packageName, ts);
+ mBgJobCount = 0;
+ // Don't reset the tracked jobs list as we need to keep tracking the current number
+ // of jobs.
+ // However, cancel the currently scheduled cutoff since it's not currently useful.
+ cancelCutoff();
+ incrementTimingSessionCount(mPkg.userId, mPkg.packageName);
+ }
+
+ /**
+ * Returns true if the Timer is actively tracking, as opposed to passively ref counting
+ * during charging.
+ */
+ public boolean isActive() {
+ synchronized (mLock) {
+ return mBgJobCount > 0;
+ }
+ }
+
+ boolean isRunning(JobStatus jobStatus) {
+ return mRunningBgJobs.contains(jobStatus);
+ }
+
+ long getCurrentDuration(long nowElapsed) {
+ synchronized (mLock) {
+ return !isActive() ? 0 : nowElapsed - mStartTimeElapsed;
+ }
+ }
+
+ int getBgJobCount() {
+ synchronized (mLock) {
+ return mBgJobCount;
+ }
+ }
+
+ private boolean shouldTrackLocked() {
+ return !mChargeTracker.isCharging() && !mForegroundUids.get(mUid);
+ }
+
+ void onStateChangedLocked(long nowElapsed, boolean isQuotaFree) {
+ if (isQuotaFree) {
+ emitSessionLocked(nowElapsed);
+ } else if (!isActive() && shouldTrackLocked()) {
+ // Start timing from unplug.
+ if (mRunningBgJobs.size() > 0) {
+ mStartTimeElapsed = nowElapsed;
+ // NOTE: this does have the unfortunate consequence that if the device is
+ // repeatedly plugged in and unplugged, or an app changes foreground state
+ // very frequently, the job count for a package may be artificially high.
+ mBgJobCount = mRunningBgJobs.size();
+ incrementJobCount(mPkg.userId, mPkg.packageName, mBgJobCount);
+ // Starting the timer means that all cached execution stats are now
+ // incorrect.
+ invalidateAllExecutionStatsLocked(mPkg.userId, mPkg.packageName);
+ // Schedule cutoff since we're now actively tracking for quotas again.
+ scheduleCutoff();
+ }
+ }
+ }
+
+ void rescheduleCutoff() {
+ cancelCutoff();
+ scheduleCutoff();
+ }
+
+ private void scheduleCutoff() {
+ // Each package can only be in one standby bucket, so we only need to have one
+ // message per timer. We only need to reschedule when restarting timer or when
+ // standby bucket changes.
+ synchronized (mLock) {
+ if (!isActive()) {
+ return;
+ }
+ Message msg = mHandler.obtainMessage(MSG_REACHED_QUOTA, mPkg);
+ final long timeRemainingMs = getTimeUntilQuotaConsumedLocked(mPkg.userId,
+ mPkg.packageName);
+ if (DEBUG) {
+ Slog.i(TAG, "Job for " + mPkg + " has " + timeRemainingMs + "ms left.");
+ }
+ // If the job was running the entire time, then the system would be up, so it's
+ // fine to use uptime millis for these messages.
+ mHandler.sendMessageDelayed(msg, timeRemainingMs);
+ }
+ }
+
+ private void cancelCutoff() {
+ mHandler.removeMessages(MSG_REACHED_QUOTA, mPkg);
+ }
+
+ public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
+ pw.print("Timer{");
+ pw.print(mPkg);
+ pw.print("} ");
+ if (isActive()) {
+ pw.print("started at ");
+ pw.print(mStartTimeElapsed);
+ pw.print(" (");
+ pw.print(sElapsedRealtimeClock.millis() - mStartTimeElapsed);
+ pw.print("ms ago)");
+ } else {
+ pw.print("NOT active");
+ }
+ pw.print(", ");
+ pw.print(mBgJobCount);
+ pw.print(" running bg jobs");
+ pw.println();
+ pw.increaseIndent();
+ for (int i = 0; i < mRunningBgJobs.size(); i++) {
+ JobStatus js = mRunningBgJobs.valueAt(i);
+ if (predicate.test(js)) {
+ pw.println(js.toShortString());
+ }
+ }
+ pw.decreaseIndent();
+ }
+
+ public void dump(ProtoOutputStream proto, long fieldId, Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+
+ mPkg.writeToProto(proto, StateControllerProto.QuotaController.Timer.PKG);
+ proto.write(StateControllerProto.QuotaController.Timer.IS_ACTIVE, isActive());
+ proto.write(StateControllerProto.QuotaController.Timer.START_TIME_ELAPSED,
+ mStartTimeElapsed);
+ proto.write(StateControllerProto.QuotaController.Timer.BG_JOB_COUNT, mBgJobCount);
+ for (int i = 0; i < mRunningBgJobs.size(); i++) {
+ JobStatus js = mRunningBgJobs.valueAt(i);
+ if (predicate.test(js)) {
+ js.writeToShortProto(proto,
+ StateControllerProto.QuotaController.Timer.RUNNING_JOBS);
+ }
+ }
+
+ proto.end(token);
+ }
+ }
+
+ /**
+ * Tracking of app assignments to standby buckets
+ */
+ final class StandbyTracker extends AppIdleStateChangeListener {
+
+ @Override
+ public void onAppIdleStateChanged(final String packageName, final @UserIdInt int userId,
+ boolean idle, int bucket, int reason) {
+ // Update job bookkeeping out of band.
+ BackgroundThread.getHandler().post(() -> {
+ final int bucketIndex = JobSchedulerService.standbyBucketToBucketIndex(bucket);
+ if (DEBUG) {
+ Slog.i(TAG, "Moving pkg " + string(userId, packageName) + " to bucketIndex "
+ + bucketIndex);
+ }
+ synchronized (mLock) {
+ ArraySet<JobStatus> jobs = mTrackedJobs.get(userId, packageName);
+ if (jobs == null || jobs.size() == 0) {
+ return;
+ }
+ for (int i = jobs.size() - 1; i >= 0; i--) {
+ JobStatus js = jobs.valueAt(i);
+ js.setStandbyBucket(bucketIndex);
+ }
+ Timer timer = mPkgTimers.get(userId, packageName);
+ if (timer != null && timer.isActive()) {
+ timer.rescheduleCutoff();
+ }
+ if (!mShouldThrottle || maybeUpdateConstraintForPkgLocked(userId,
+ packageName)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onParoleStateChanged(final boolean isParoleOn) {
+ mInParole = isParoleOn;
+ if (DEBUG) {
+ Slog.i(TAG, "Global parole state now " + (isParoleOn ? "ON" : "OFF"));
+ }
+ // Update job bookkeeping out of band.
+ BackgroundThread.getHandler().post(() -> {
+ synchronized (mLock) {
+ maybeUpdateAllConstraintsLocked();
+ }
+ });
+ }
+ }
+
+ private final class DeleteTimingSessionsFunctor implements Consumer<List<TimingSession>> {
+ private final Predicate<TimingSession> mTooOld = new Predicate<TimingSession>() {
+ public boolean test(TimingSession ts) {
+ return ts.endTimeElapsed <= sElapsedRealtimeClock.millis() - MAX_PERIOD_MS;
+ }
+ };
+
+ @Override
+ public void accept(List<TimingSession> sessions) {
+ if (sessions != null) {
+ // Remove everything older than MAX_PERIOD_MS time ago.
+ sessions.removeIf(mTooOld);
+ }
+ }
+ }
+
+ private final DeleteTimingSessionsFunctor mDeleteOldSessionsFunctor =
+ new DeleteTimingSessionsFunctor();
+
+ @VisibleForTesting
+ void deleteObsoleteSessionsLocked() {
+ mTimingSessions.forEach(mDeleteOldSessionsFunctor);
+ }
+
+ private class QcHandler extends Handler {
+ QcHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ synchronized (mLock) {
+ switch (msg.what) {
+ case MSG_REACHED_QUOTA: {
+ Package pkg = (Package) msg.obj;
+ if (DEBUG) {
+ Slog.d(TAG, "Checking if " + pkg + " has reached its quota.");
+ }
+
+ long timeRemainingMs = getRemainingExecutionTimeLocked(pkg.userId,
+ pkg.packageName);
+ if (timeRemainingMs <= 50) {
+ // Less than 50 milliseconds left. Start process of shutting down jobs.
+ if (DEBUG) Slog.d(TAG, pkg + " has reached its quota.");
+ if (maybeUpdateConstraintForPkgLocked(pkg.userId, pkg.packageName)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ } else {
+ // This could potentially happen if an old session phases out while a
+ // job is currently running.
+ // Reschedule message
+ Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg);
+ timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId,
+ pkg.packageName);
+ if (DEBUG) {
+ Slog.d(TAG, pkg + " has " + timeRemainingMs + "ms left.");
+ }
+ sendMessageDelayed(rescheduleMsg, timeRemainingMs);
+ }
+ break;
+ }
+ case MSG_CLEAN_UP_SESSIONS:
+ if (DEBUG) {
+ Slog.d(TAG, "Cleaning up timing sessions.");
+ }
+ deleteObsoleteSessionsLocked();
+ maybeScheduleCleanupAlarmLocked();
+
+ break;
+ case MSG_CHECK_PACKAGE: {
+ String packageName = (String) msg.obj;
+ int userId = msg.arg1;
+ if (DEBUG) {
+ Slog.d(TAG, "Checking pkg " + string(userId, packageName));
+ }
+ if (maybeUpdateConstraintForPkgLocked(userId, packageName)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ break;
+ }
+ case MSG_UID_PROCESS_STATE_CHANGED: {
+ final int uid = msg.arg1;
+ final int procState = msg.arg2;
+ final int userId = UserHandle.getUserId(uid);
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+
+ synchronized (mLock) {
+ boolean isQuotaFree;
+ if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ mForegroundUids.put(uid, true);
+ isQuotaFree = true;
+ } else {
+ mForegroundUids.delete(uid);
+ isQuotaFree = false;
+ }
+ // Update Timers first.
+ if (mPkgTimers.indexOfKey(userId) >= 0) {
+ ArraySet<String> packages = mUidToPackageCache.get(uid);
+ if (packages == null) {
+ try {
+ String[] pkgs = AppGlobals.getPackageManager()
+ .getPackagesForUid(uid);
+ if (pkgs != null) {
+ for (String pkg : pkgs) {
+ mUidToPackageCache.add(uid, pkg);
+ }
+ packages = mUidToPackageCache.get(uid);
+ }
+ } catch (RemoteException e) {
+ Slog.wtf(TAG, "Failed to get package list", e);
+ }
+ }
+ if (packages != null) {
+ for (int i = packages.size() - 1; i >= 0; --i) {
+ Timer t = mPkgTimers.get(userId, packages.valueAt(i));
+ if (t != null) {
+ t.onStateChangedLocked(nowElapsed, isQuotaFree);
+ }
+ }
+ }
+ }
+ if (maybeUpdateConstraintForUidLocked(uid)) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private class QcAlarmListener implements AlarmManager.OnAlarmListener {
+ private final int mUserId;
+ private final String mPackageName;
+ private volatile long mTriggerTimeElapsed;
+
+ QcAlarmListener(int userId, String packageName) {
+ mUserId = userId;
+ mPackageName = packageName;
+ }
+
+ boolean isWaiting() {
+ return mTriggerTimeElapsed > 0;
+ }
+
+ void setTriggerTime(long timeElapsed) {
+ mTriggerTimeElapsed = timeElapsed;
+ }
+
+ long getTriggerTimeElapsed() {
+ return mTriggerTimeElapsed;
+ }
+
+ @Override
+ public void onAlarm() {
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE, mUserId, 0, mPackageName).sendToTarget();
+ mTriggerTimeElapsed = 0;
+ }
+ }
+
+ @VisibleForTesting
+ class QcConstants extends ContentObserver {
+ private ContentResolver mResolver;
+ private final KeyValueListParser mParser = new KeyValueListParser(',');
+
+ private static final String KEY_ALLOWED_TIME_PER_PERIOD_MS = "allowed_time_per_period_ms";
+ private static final String KEY_IN_QUOTA_BUFFER_MS = "in_quota_buffer_ms";
+ private static final String KEY_WINDOW_SIZE_ACTIVE_MS = "window_size_active_ms";
+ private static final String KEY_WINDOW_SIZE_WORKING_MS = "window_size_working_ms";
+ private static final String KEY_WINDOW_SIZE_FREQUENT_MS = "window_size_frequent_ms";
+ private static final String KEY_WINDOW_SIZE_RARE_MS = "window_size_rare_ms";
+ private static final String KEY_MAX_EXECUTION_TIME_MS = "max_execution_time_ms";
+ private static final String KEY_MAX_JOB_COUNT_ACTIVE = "max_job_count_active";
+ private static final String KEY_MAX_JOB_COUNT_WORKING = "max_job_count_working";
+ private static final String KEY_MAX_JOB_COUNT_FREQUENT = "max_job_count_frequent";
+ private static final String KEY_MAX_JOB_COUNT_RARE = "max_job_count_rare";
+ private static final String KEY_RATE_LIMITING_WINDOW_MS = "rate_limiting_window_ms";
+ private static final String KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
+ "max_job_count_per_rate_limiting_window";
+ private static final String KEY_MAX_SESSION_COUNT_ACTIVE = "max_session_count_active";
+ private static final String KEY_MAX_SESSION_COUNT_WORKING = "max_session_count_working";
+ private static final String KEY_MAX_SESSION_COUNT_FREQUENT = "max_session_count_frequent";
+ private static final String KEY_MAX_SESSION_COUNT_RARE = "max_session_count_rare";
+ private static final String KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
+ "max_session_count_per_rate_limiting_window";
+ private static final String KEY_TIMING_SESSION_COALESCING_DURATION_MS =
+ "timing_session_coalescing_duration_ms";
+
+ private static final long DEFAULT_ALLOWED_TIME_PER_PERIOD_MS =
+ 10 * 60 * 1000L; // 10 minutes
+ private static final long DEFAULT_IN_QUOTA_BUFFER_MS =
+ 30 * 1000L; // 30 seconds
+ private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS =
+ DEFAULT_ALLOWED_TIME_PER_PERIOD_MS; // ACTIVE apps can run jobs at any time
+ private static final long DEFAULT_WINDOW_SIZE_WORKING_MS =
+ 2 * 60 * 60 * 1000L; // 2 hours
+ private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS =
+ 8 * 60 * 60 * 1000L; // 8 hours
+ private static final long DEFAULT_WINDOW_SIZE_RARE_MS =
+ 24 * 60 * 60 * 1000L; // 24 hours
+ private static final long DEFAULT_MAX_EXECUTION_TIME_MS =
+ 4 * HOUR_IN_MILLIS;
+ private static final long DEFAULT_RATE_LIMITING_WINDOW_MS =
+ 10 * MINUTE_IN_MILLIS;
+ private static final int DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 20;
+ private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = // 20/window = 120/hr = 1/session
+ DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
+ private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session
+ (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS);
+ private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session
+ (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS);
+ private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session
+ (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS);
+ private static final int DEFAULT_MAX_SESSION_COUNT_ACTIVE =
+ 20; // 120/hr
+ private static final int DEFAULT_MAX_SESSION_COUNT_WORKING =
+ 10; // 5/hr
+ private static final int DEFAULT_MAX_SESSION_COUNT_FREQUENT =
+ 8; // 1/hr
+ private static final int DEFAULT_MAX_SESSION_COUNT_RARE =
+ 3; // .125/hr
+ private static final int DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 20;
+ private static final long DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS = 5000; // 5 seconds
+
+ /** How much time each app will have to run jobs within their standby bucket window. */
+ public long ALLOWED_TIME_PER_PERIOD_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_MS;
+
+ /**
+ * How much time the package should have before transitioning from out-of-quota to in-quota.
+ * This should not affect processing if the package is already in-quota.
+ */
+ public long IN_QUOTA_BUFFER_MS = DEFAULT_IN_QUOTA_BUFFER_MS;
+
+ /**
+ * The quota window size of the particular standby bucket. Apps in this standby bucket are
+ * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
+ * WINDOW_SIZE_MS.
+ */
+ public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_WINDOW_SIZE_ACTIVE_MS;
+
+ /**
+ * The quota window size of the particular standby bucket. Apps in this standby bucket are
+ * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
+ * WINDOW_SIZE_MS.
+ */
+ public long WINDOW_SIZE_WORKING_MS = DEFAULT_WINDOW_SIZE_WORKING_MS;
+
+ /**
+ * The quota window size of the particular standby bucket. Apps in this standby bucket are
+ * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
+ * WINDOW_SIZE_MS.
+ */
+ public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_WINDOW_SIZE_FREQUENT_MS;
+
+ /**
+ * The quota window size of the particular standby bucket. Apps in this standby bucket are
+ * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_MS} within the past
+ * WINDOW_SIZE_MS.
+ */
+ public long WINDOW_SIZE_RARE_MS = DEFAULT_WINDOW_SIZE_RARE_MS;
+
+ /**
+ * The maximum amount of time an app can have its jobs running within a 24 hour window.
+ */
+ public long MAX_EXECUTION_TIME_MS = DEFAULT_MAX_EXECUTION_TIME_MS;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int MAX_JOB_COUNT_ACTIVE = DEFAULT_MAX_JOB_COUNT_ACTIVE;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int MAX_JOB_COUNT_WORKING = DEFAULT_MAX_JOB_COUNT_WORKING;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int MAX_JOB_COUNT_FREQUENT = DEFAULT_MAX_JOB_COUNT_FREQUENT;
+
+ /**
+ * The maximum number of jobs an app can run within this particular standby bucket's
+ * window size.
+ */
+ public int MAX_JOB_COUNT_RARE = DEFAULT_MAX_JOB_COUNT_RARE;
+
+ /** The period of time used to rate limit recently run jobs. */
+ public long RATE_LIMITING_WINDOW_MS = DEFAULT_RATE_LIMITING_WINDOW_MS;
+
+ /**
+ * The maximum number of jobs that can run within the past {@link #RATE_LIMITING_WINDOW_MS}.
+ */
+ public int MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW =
+ DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
+
+ /**
+ * The maximum number of {@link TimingSession}s an app can run within this particular
+ * standby bucket's window size.
+ */
+ public int MAX_SESSION_COUNT_ACTIVE = DEFAULT_MAX_SESSION_COUNT_ACTIVE;
+
+ /**
+ * The maximum number of {@link TimingSession}s an app can run within this particular
+ * standby bucket's window size.
+ */
+ public int MAX_SESSION_COUNT_WORKING = DEFAULT_MAX_SESSION_COUNT_WORKING;
+
+ /**
+ * The maximum number of {@link TimingSession}s an app can run within this particular
+ * standby bucket's window size.
+ */
+ public int MAX_SESSION_COUNT_FREQUENT = DEFAULT_MAX_SESSION_COUNT_FREQUENT;
+
+ /**
+ * The maximum number of {@link TimingSession}s an app can run within this particular
+ * standby bucket's window size.
+ */
+ public int MAX_SESSION_COUNT_RARE = DEFAULT_MAX_SESSION_COUNT_RARE;
+
+ /**
+ * The maximum number of {@link TimingSession}s that can run within the past
+ * {@link #ALLOWED_TIME_PER_PERIOD_MS}.
+ */
+ public int MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW =
+ DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW;
+
+ /**
+ * Treat two distinct {@link TimingSession}s as the same if they start and end within this
+ * amount of time of each other.
+ */
+ public long TIMING_SESSION_COALESCING_DURATION_MS =
+ DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS;
+
+ // Safeguards
+
+ /** The minimum number of jobs that any bucket will be allowed to run within its window. */
+ private static final int MIN_BUCKET_JOB_COUNT = 10;
+
+ /**
+ * The minimum number of {@link TimingSession}s that any bucket will be allowed to run
+ * within its window.
+ */
+ private static final int MIN_BUCKET_SESSION_COUNT = 1;
+
+ /** The minimum value that {@link #MAX_EXECUTION_TIME_MS} can have. */
+ private static final long MIN_MAX_EXECUTION_TIME_MS = 60 * MINUTE_IN_MILLIS;
+
+ /** The minimum value that {@link #MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
+ private static final int MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = 10;
+
+ /** The minimum value that {@link #MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW} can have. */
+ private static final int MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = 10;
+
+ /** The minimum value that {@link #RATE_LIMITING_WINDOW_MS} can have. */
+ private static final long MIN_RATE_LIMITING_WINDOW_MS = 30 * SECOND_IN_MILLIS;
+
+ QcConstants(Handler handler) {
+ super(handler);
+ }
+
+ private void start(ContentResolver resolver) {
+ mResolver = resolver;
+ mResolver.registerContentObserver(Settings.Global.getUriFor(
+ Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS), false, this);
+ updateConstants();
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ final String constants = Settings.Global.getString(
+ mResolver, Settings.Global.JOB_SCHEDULER_QUOTA_CONTROLLER_CONSTANTS);
+
+ try {
+ mParser.setString(constants);
+ } catch (Exception e) {
+ // Failed to parse the settings string, log this and move on with defaults.
+ Slog.e(TAG, "Bad jobscheduler quota controller settings", e);
+ }
+
+ ALLOWED_TIME_PER_PERIOD_MS = mParser.getDurationMillis(
+ KEY_ALLOWED_TIME_PER_PERIOD_MS, DEFAULT_ALLOWED_TIME_PER_PERIOD_MS);
+ IN_QUOTA_BUFFER_MS = mParser.getDurationMillis(
+ KEY_IN_QUOTA_BUFFER_MS, DEFAULT_IN_QUOTA_BUFFER_MS);
+ WINDOW_SIZE_ACTIVE_MS = mParser.getDurationMillis(
+ KEY_WINDOW_SIZE_ACTIVE_MS, DEFAULT_WINDOW_SIZE_ACTIVE_MS);
+ WINDOW_SIZE_WORKING_MS = mParser.getDurationMillis(
+ KEY_WINDOW_SIZE_WORKING_MS, DEFAULT_WINDOW_SIZE_WORKING_MS);
+ WINDOW_SIZE_FREQUENT_MS = mParser.getDurationMillis(
+ KEY_WINDOW_SIZE_FREQUENT_MS, DEFAULT_WINDOW_SIZE_FREQUENT_MS);
+ WINDOW_SIZE_RARE_MS = mParser.getDurationMillis(
+ KEY_WINDOW_SIZE_RARE_MS, DEFAULT_WINDOW_SIZE_RARE_MS);
+ MAX_EXECUTION_TIME_MS = mParser.getDurationMillis(
+ KEY_MAX_EXECUTION_TIME_MS, DEFAULT_MAX_EXECUTION_TIME_MS);
+ MAX_JOB_COUNT_ACTIVE = mParser.getInt(
+ KEY_MAX_JOB_COUNT_ACTIVE, DEFAULT_MAX_JOB_COUNT_ACTIVE);
+ MAX_JOB_COUNT_WORKING = mParser.getInt(
+ KEY_MAX_JOB_COUNT_WORKING, DEFAULT_MAX_JOB_COUNT_WORKING);
+ MAX_JOB_COUNT_FREQUENT = mParser.getInt(
+ KEY_MAX_JOB_COUNT_FREQUENT, DEFAULT_MAX_JOB_COUNT_FREQUENT);
+ MAX_JOB_COUNT_RARE = mParser.getInt(
+ KEY_MAX_JOB_COUNT_RARE, DEFAULT_MAX_JOB_COUNT_RARE);
+ RATE_LIMITING_WINDOW_MS = mParser.getLong(
+ KEY_RATE_LIMITING_WINDOW_MS, DEFAULT_RATE_LIMITING_WINDOW_MS);
+ MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
+ KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+ DEFAULT_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
+ MAX_SESSION_COUNT_ACTIVE = mParser.getInt(
+ KEY_MAX_SESSION_COUNT_ACTIVE, DEFAULT_MAX_SESSION_COUNT_ACTIVE);
+ MAX_SESSION_COUNT_WORKING = mParser.getInt(
+ KEY_MAX_SESSION_COUNT_WORKING, DEFAULT_MAX_SESSION_COUNT_WORKING);
+ MAX_SESSION_COUNT_FREQUENT = mParser.getInt(
+ KEY_MAX_SESSION_COUNT_FREQUENT, DEFAULT_MAX_SESSION_COUNT_FREQUENT);
+ MAX_SESSION_COUNT_RARE = mParser.getInt(
+ KEY_MAX_SESSION_COUNT_RARE, DEFAULT_MAX_SESSION_COUNT_RARE);
+ MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW = mParser.getInt(
+ KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+ DEFAULT_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
+ TIMING_SESSION_COALESCING_DURATION_MS = mParser.getLong(
+ KEY_TIMING_SESSION_COALESCING_DURATION_MS,
+ DEFAULT_TIMING_SESSION_COALESCING_DURATION_MS);
+
+ updateConstants();
+ }
+
+ @VisibleForTesting
+ void updateConstants() {
+ synchronized (mLock) {
+ boolean changed = false;
+
+ long newMaxExecutionTimeMs = Math.max(MIN_MAX_EXECUTION_TIME_MS,
+ Math.min(MAX_PERIOD_MS, MAX_EXECUTION_TIME_MS));
+ if (mMaxExecutionTimeMs != newMaxExecutionTimeMs) {
+ mMaxExecutionTimeMs = newMaxExecutionTimeMs;
+ mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+ changed = true;
+ }
+ long newAllowedTimeMs = Math.min(mMaxExecutionTimeMs,
+ Math.max(MINUTE_IN_MILLIS, ALLOWED_TIME_PER_PERIOD_MS));
+ if (mAllowedTimePerPeriodMs != newAllowedTimeMs) {
+ mAllowedTimePerPeriodMs = newAllowedTimeMs;
+ mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
+ changed = true;
+ }
+ // Make sure quota buffer is non-negative, not greater than allowed time per period,
+ // and no more than 5 minutes.
+ long newQuotaBufferMs = Math.max(0, Math.min(mAllowedTimePerPeriodMs,
+ Math.min(5 * MINUTE_IN_MILLIS, IN_QUOTA_BUFFER_MS)));
+ if (mQuotaBufferMs != newQuotaBufferMs) {
+ mQuotaBufferMs = newQuotaBufferMs;
+ mAllowedTimeIntoQuotaMs = mAllowedTimePerPeriodMs - mQuotaBufferMs;
+ mMaxExecutionTimeIntoQuotaMs = mMaxExecutionTimeMs - mQuotaBufferMs;
+ changed = true;
+ }
+ long newActivePeriodMs = Math.max(mAllowedTimePerPeriodMs,
+ Math.min(MAX_PERIOD_MS, WINDOW_SIZE_ACTIVE_MS));
+ if (mBucketPeriodsMs[ACTIVE_INDEX] != newActivePeriodMs) {
+ mBucketPeriodsMs[ACTIVE_INDEX] = newActivePeriodMs;
+ changed = true;
+ }
+ long newWorkingPeriodMs = Math.max(mAllowedTimePerPeriodMs,
+ Math.min(MAX_PERIOD_MS, WINDOW_SIZE_WORKING_MS));
+ if (mBucketPeriodsMs[WORKING_INDEX] != newWorkingPeriodMs) {
+ mBucketPeriodsMs[WORKING_INDEX] = newWorkingPeriodMs;
+ changed = true;
+ }
+ long newFrequentPeriodMs = Math.max(mAllowedTimePerPeriodMs,
+ Math.min(MAX_PERIOD_MS, WINDOW_SIZE_FREQUENT_MS));
+ if (mBucketPeriodsMs[FREQUENT_INDEX] != newFrequentPeriodMs) {
+ mBucketPeriodsMs[FREQUENT_INDEX] = newFrequentPeriodMs;
+ changed = true;
+ }
+ long newRarePeriodMs = Math.max(mAllowedTimePerPeriodMs,
+ Math.min(MAX_PERIOD_MS, WINDOW_SIZE_RARE_MS));
+ if (mBucketPeriodsMs[RARE_INDEX] != newRarePeriodMs) {
+ mBucketPeriodsMs[RARE_INDEX] = newRarePeriodMs;
+ changed = true;
+ }
+ long newRateLimitingWindowMs = Math.min(MAX_PERIOD_MS,
+ Math.max(MIN_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS));
+ if (mRateLimitingWindowMs != newRateLimitingWindowMs) {
+ mRateLimitingWindowMs = newRateLimitingWindowMs;
+ changed = true;
+ }
+ int newMaxJobCountPerRateLimitingWindow = Math.max(
+ MIN_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
+ if (mMaxJobCountPerRateLimitingWindow != newMaxJobCountPerRateLimitingWindow) {
+ mMaxJobCountPerRateLimitingWindow = newMaxJobCountPerRateLimitingWindow;
+ changed = true;
+ }
+ int newActiveMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_ACTIVE);
+ if (mMaxBucketJobCounts[ACTIVE_INDEX] != newActiveMaxJobCount) {
+ mMaxBucketJobCounts[ACTIVE_INDEX] = newActiveMaxJobCount;
+ changed = true;
+ }
+ int newWorkingMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_WORKING);
+ if (mMaxBucketJobCounts[WORKING_INDEX] != newWorkingMaxJobCount) {
+ mMaxBucketJobCounts[WORKING_INDEX] = newWorkingMaxJobCount;
+ changed = true;
+ }
+ int newFrequentMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_FREQUENT);
+ if (mMaxBucketJobCounts[FREQUENT_INDEX] != newFrequentMaxJobCount) {
+ mMaxBucketJobCounts[FREQUENT_INDEX] = newFrequentMaxJobCount;
+ changed = true;
+ }
+ int newRareMaxJobCount = Math.max(MIN_BUCKET_JOB_COUNT, MAX_JOB_COUNT_RARE);
+ if (mMaxBucketJobCounts[RARE_INDEX] != newRareMaxJobCount) {
+ mMaxBucketJobCounts[RARE_INDEX] = newRareMaxJobCount;
+ changed = true;
+ }
+ int newMaxSessionCountPerRateLimitPeriod = Math.max(
+ MIN_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
+ if (mMaxSessionCountPerRateLimitingWindow != newMaxSessionCountPerRateLimitPeriod) {
+ mMaxSessionCountPerRateLimitingWindow = newMaxSessionCountPerRateLimitPeriod;
+ changed = true;
+ }
+ int newActiveMaxSessionCount =
+ Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_ACTIVE);
+ if (mMaxBucketSessionCounts[ACTIVE_INDEX] != newActiveMaxSessionCount) {
+ mMaxBucketSessionCounts[ACTIVE_INDEX] = newActiveMaxSessionCount;
+ changed = true;
+ }
+ int newWorkingMaxSessionCount =
+ Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_WORKING);
+ if (mMaxBucketSessionCounts[WORKING_INDEX] != newWorkingMaxSessionCount) {
+ mMaxBucketSessionCounts[WORKING_INDEX] = newWorkingMaxSessionCount;
+ changed = true;
+ }
+ int newFrequentMaxSessionCount =
+ Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_FREQUENT);
+ if (mMaxBucketSessionCounts[FREQUENT_INDEX] != newFrequentMaxSessionCount) {
+ mMaxBucketSessionCounts[FREQUENT_INDEX] = newFrequentMaxSessionCount;
+ changed = true;
+ }
+ int newRareMaxSessionCount =
+ Math.max(MIN_BUCKET_SESSION_COUNT, MAX_SESSION_COUNT_RARE);
+ if (mMaxBucketSessionCounts[RARE_INDEX] != newRareMaxSessionCount) {
+ mMaxBucketSessionCounts[RARE_INDEX] = newRareMaxSessionCount;
+ changed = true;
+ }
+ long newSessionCoalescingDurationMs = Math.min(15 * MINUTE_IN_MILLIS,
+ Math.max(0, TIMING_SESSION_COALESCING_DURATION_MS));
+ if (mTimingSessionCoalescingDurationMs != newSessionCoalescingDurationMs) {
+ mTimingSessionCoalescingDurationMs = newSessionCoalescingDurationMs;
+ changed = true;
+ }
+
+ if (changed && mShouldThrottle) {
+ // Update job bookkeeping out of band.
+ BackgroundThread.getHandler().post(() -> {
+ synchronized (mLock) {
+ invalidateAllExecutionStatsLocked();
+ maybeUpdateAllConstraintsLocked();
+ }
+ });
+ }
+ }
+ }
+
+ private void dump(IndentingPrintWriter pw) {
+ pw.println();
+ pw.println("QuotaController:");
+ pw.increaseIndent();
+ pw.printPair(KEY_ALLOWED_TIME_PER_PERIOD_MS, ALLOWED_TIME_PER_PERIOD_MS).println();
+ pw.printPair(KEY_IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS).println();
+ pw.printPair(KEY_WINDOW_SIZE_ACTIVE_MS, WINDOW_SIZE_ACTIVE_MS).println();
+ pw.printPair(KEY_WINDOW_SIZE_WORKING_MS, WINDOW_SIZE_WORKING_MS).println();
+ pw.printPair(KEY_WINDOW_SIZE_FREQUENT_MS, WINDOW_SIZE_FREQUENT_MS).println();
+ pw.printPair(KEY_WINDOW_SIZE_RARE_MS, WINDOW_SIZE_RARE_MS).println();
+ pw.printPair(KEY_MAX_EXECUTION_TIME_MS, MAX_EXECUTION_TIME_MS).println();
+ pw.printPair(KEY_MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE).println();
+ pw.printPair(KEY_MAX_JOB_COUNT_WORKING, MAX_JOB_COUNT_WORKING).println();
+ pw.printPair(KEY_MAX_JOB_COUNT_FREQUENT, MAX_JOB_COUNT_FREQUENT).println();
+ pw.printPair(KEY_MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE).println();
+ pw.printPair(KEY_RATE_LIMITING_WINDOW_MS, RATE_LIMITING_WINDOW_MS).println();
+ pw.printPair(KEY_MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW).println();
+ pw.printPair(KEY_MAX_SESSION_COUNT_ACTIVE, MAX_SESSION_COUNT_ACTIVE).println();
+ pw.printPair(KEY_MAX_SESSION_COUNT_WORKING, MAX_SESSION_COUNT_WORKING).println();
+ pw.printPair(KEY_MAX_SESSION_COUNT_FREQUENT, MAX_SESSION_COUNT_FREQUENT).println();
+ pw.printPair(KEY_MAX_SESSION_COUNT_RARE, MAX_SESSION_COUNT_RARE).println();
+ pw.printPair(KEY_MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW).println();
+ pw.printPair(KEY_TIMING_SESSION_COALESCING_DURATION_MS,
+ TIMING_SESSION_COALESCING_DURATION_MS).println();
+ pw.decreaseIndent();
+ }
+
+ private void dump(ProtoOutputStream proto) {
+ final long qcToken = proto.start(ConstantsProto.QUOTA_CONTROLLER);
+ proto.write(ConstantsProto.QuotaController.ALLOWED_TIME_PER_PERIOD_MS,
+ ALLOWED_TIME_PER_PERIOD_MS);
+ proto.write(ConstantsProto.QuotaController.IN_QUOTA_BUFFER_MS, IN_QUOTA_BUFFER_MS);
+ proto.write(ConstantsProto.QuotaController.ACTIVE_WINDOW_SIZE_MS,
+ WINDOW_SIZE_ACTIVE_MS);
+ proto.write(ConstantsProto.QuotaController.WORKING_WINDOW_SIZE_MS,
+ WINDOW_SIZE_WORKING_MS);
+ proto.write(ConstantsProto.QuotaController.FREQUENT_WINDOW_SIZE_MS,
+ WINDOW_SIZE_FREQUENT_MS);
+ proto.write(ConstantsProto.QuotaController.RARE_WINDOW_SIZE_MS, WINDOW_SIZE_RARE_MS);
+ proto.write(ConstantsProto.QuotaController.MAX_EXECUTION_TIME_MS,
+ MAX_EXECUTION_TIME_MS);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_ACTIVE, MAX_JOB_COUNT_ACTIVE);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_WORKING,
+ MAX_JOB_COUNT_WORKING);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_FREQUENT,
+ MAX_JOB_COUNT_FREQUENT);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_RARE, MAX_JOB_COUNT_RARE);
+ proto.write(ConstantsProto.QuotaController.RATE_LIMITING_WINDOW_MS,
+ RATE_LIMITING_WINDOW_MS);
+ proto.write(ConstantsProto.QuotaController.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW);
+ proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_ACTIVE,
+ MAX_SESSION_COUNT_ACTIVE);
+ proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_WORKING,
+ MAX_SESSION_COUNT_WORKING);
+ proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_FREQUENT,
+ MAX_SESSION_COUNT_FREQUENT);
+ proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_RARE,
+ MAX_SESSION_COUNT_RARE);
+ proto.write(ConstantsProto.QuotaController.MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW,
+ MAX_SESSION_COUNT_PER_RATE_LIMITING_WINDOW);
+ proto.write(ConstantsProto.QuotaController.TIMING_SESSION_COALESCING_DURATION_MS,
+ TIMING_SESSION_COALESCING_DURATION_MS);
+ proto.end(qcToken);
+ }
+ }
+
+ //////////////////////// TESTING HELPERS /////////////////////////////
+
+ @VisibleForTesting
+ long getAllowedTimePerPeriodMs() {
+ return mAllowedTimePerPeriodMs;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ int[] getBucketMaxJobCounts() {
+ return mMaxBucketJobCounts;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ int[] getBucketMaxSessionCounts() {
+ return mMaxBucketSessionCounts;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ long[] getBucketWindowSizes() {
+ return mBucketPeriodsMs;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ SparseBooleanArray getForegroundUids() {
+ return mForegroundUids;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ Handler getHandler() {
+ return mHandler;
+ }
+
+ @VisibleForTesting
+ long getInQuotaBufferMs() {
+ return mQuotaBufferMs;
+ }
+
+ @VisibleForTesting
+ long getMaxExecutionTimeMs() {
+ return mMaxExecutionTimeMs;
+ }
+
+ @VisibleForTesting
+ int getMaxJobCountPerRateLimitingWindow() {
+ return mMaxJobCountPerRateLimitingWindow;
+ }
+
+ @VisibleForTesting
+ int getMaxSessionCountPerRateLimitingWindow() {
+ return mMaxSessionCountPerRateLimitingWindow;
+ }
+
+ @VisibleForTesting
+ long getRateLimitingWindowMs() {
+ return mRateLimitingWindowMs;
+ }
+
+ @VisibleForTesting
+ long getTimingSessionCoalescingDurationMs() {
+ return mTimingSessionCoalescingDurationMs;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ List<TimingSession> getTimingSessions(int userId, String packageName) {
+ return mTimingSessions.get(userId, packageName);
+ }
+
+ @VisibleForTesting
+ @NonNull
+ QcConstants getQcConstants() {
+ return mQcConstants;
+ }
+
+ //////////////////////////// DATA DUMP //////////////////////////////
+
+ @Override
+ public void dumpControllerStateLocked(final IndentingPrintWriter pw,
+ final Predicate<JobStatus> predicate) {
+ pw.println("Is throttling: " + mShouldThrottle);
+ pw.println("Is charging: " + mChargeTracker.isCharging());
+ pw.println("In parole: " + mInParole);
+ pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis());
+ pw.println();
+
+ pw.print("Foreground UIDs: ");
+ pw.println(mForegroundUids.toString());
+ pw.println();
+
+ pw.println("Cached UID->package map:");
+ pw.increaseIndent();
+ for (int i = 0; i < mUidToPackageCache.size(); ++i) {
+ final int uid = mUidToPackageCache.keyAt(i);
+ pw.print(uid);
+ pw.print(": ");
+ pw.println(mUidToPackageCache.get(uid));
+ }
+ pw.decreaseIndent();
+ pw.println();
+
+ mTrackedJobs.forEach((jobs) -> {
+ for (int j = 0; j < jobs.size(); j++) {
+ final JobStatus js = jobs.valueAt(j);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ pw.print("#");
+ js.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, js.getSourceUid());
+ if (mTopStartedJobs.contains(js)) {
+ pw.print(" (TOP)");
+ }
+ pw.println();
+
+ pw.increaseIndent();
+ pw.print(JobStatus.bucketName(getEffectiveStandbyBucket(js)));
+ pw.print(", ");
+ if (js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) {
+ pw.print("within quota");
+ } else {
+ pw.print("not within quota");
+ }
+ pw.print(", ");
+ pw.print(getRemainingExecutionTimeLocked(js));
+ pw.print("ms remaining in quota");
+ pw.decreaseIndent();
+ pw.println();
+ }
+ });
+
+ pw.println();
+ for (int u = 0; u < mPkgTimers.numUsers(); ++u) {
+ final int userId = mPkgTimers.keyAt(u);
+ for (int p = 0; p < mPkgTimers.numPackagesForUser(userId); ++p) {
+ final String pkgName = mPkgTimers.keyAt(u, p);
+ mPkgTimers.valueAt(u, p).dump(pw, predicate);
+ pw.println();
+ List<TimingSession> sessions = mTimingSessions.get(userId, pkgName);
+ if (sessions != null) {
+ pw.increaseIndent();
+ pw.println("Saved sessions:");
+ pw.increaseIndent();
+ for (int j = sessions.size() - 1; j >= 0; j--) {
+ TimingSession session = sessions.get(j);
+ session.dump(pw);
+ }
+ pw.decreaseIndent();
+ pw.decreaseIndent();
+ pw.println();
+ }
+ }
+ }
+
+ pw.println("Cached execution stats:");
+ pw.increaseIndent();
+ for (int u = 0; u < mExecutionStatsCache.numUsers(); ++u) {
+ final int userId = mExecutionStatsCache.keyAt(u);
+ for (int p = 0; p < mExecutionStatsCache.numPackagesForUser(userId); ++p) {
+ final String pkgName = mExecutionStatsCache.keyAt(u, p);
+ ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p);
+
+ pw.println(string(userId, pkgName));
+ pw.increaseIndent();
+ for (int i = 0; i < stats.length; ++i) {
+ ExecutionStats executionStats = stats[i];
+ if (executionStats != null) {
+ pw.print(JobStatus.bucketName(i));
+ pw.print(": ");
+ pw.println(executionStats);
+ }
+ }
+ pw.decreaseIndent();
+ }
+ }
+ pw.decreaseIndent();
+
+ pw.println();
+ pw.println("In quota alarms:");
+ pw.increaseIndent();
+ for (int u = 0; u < mInQuotaAlarmListeners.numUsers(); ++u) {
+ final int userId = mInQuotaAlarmListeners.keyAt(u);
+ for (int p = 0; p < mInQuotaAlarmListeners.numPackagesForUser(userId); ++p) {
+ final String pkgName = mInQuotaAlarmListeners.keyAt(u, p);
+ QcAlarmListener alarmListener = mInQuotaAlarmListeners.valueAt(u, p);
+
+ pw.print(string(userId, pkgName));
+ pw.print(": ");
+ if (alarmListener.isWaiting()) {
+ pw.println(alarmListener.getTriggerTimeElapsed());
+ } else {
+ pw.println("NOT WAITING");
+ }
+ }
+ }
+ pw.decreaseIndent();
+ }
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.QUOTA);
+
+ proto.write(StateControllerProto.QuotaController.IS_CHARGING, mChargeTracker.isCharging());
+ proto.write(StateControllerProto.QuotaController.IS_IN_PAROLE, mInParole);
+ proto.write(StateControllerProto.QuotaController.ELAPSED_REALTIME,
+ sElapsedRealtimeClock.millis());
+
+ for (int i = 0; i < mForegroundUids.size(); ++i) {
+ proto.write(StateControllerProto.QuotaController.FOREGROUND_UIDS,
+ mForegroundUids.keyAt(i));
+ }
+
+ mTrackedJobs.forEach((jobs) -> {
+ for (int j = 0; j < jobs.size(); j++) {
+ final JobStatus js = jobs.valueAt(j);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ final long jsToken = proto.start(
+ StateControllerProto.QuotaController.TRACKED_JOBS);
+ js.writeToShortProto(proto,
+ StateControllerProto.QuotaController.TrackedJob.INFO);
+ proto.write(StateControllerProto.QuotaController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.write(
+ StateControllerProto.QuotaController.TrackedJob.EFFECTIVE_STANDBY_BUCKET,
+ getEffectiveStandbyBucket(js));
+ proto.write(StateControllerProto.QuotaController.TrackedJob.IS_TOP_STARTED_JOB,
+ mTopStartedJobs.contains(js));
+ proto.write(StateControllerProto.QuotaController.TrackedJob.HAS_QUOTA,
+ js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
+ proto.write(StateControllerProto.QuotaController.TrackedJob.REMAINING_QUOTA_MS,
+ getRemainingExecutionTimeLocked(js));
+ proto.end(jsToken);
+ }
+ });
+
+ for (int u = 0; u < mPkgTimers.numUsers(); ++u) {
+ final int userId = mPkgTimers.keyAt(u);
+ for (int p = 0; p < mPkgTimers.numPackagesForUser(userId); ++p) {
+ final String pkgName = mPkgTimers.keyAt(u, p);
+ final long psToken = proto.start(
+ StateControllerProto.QuotaController.PACKAGE_STATS);
+ mPkgTimers.valueAt(u, p).dump(proto,
+ StateControllerProto.QuotaController.PackageStats.TIMER, predicate);
+
+ List<TimingSession> sessions = mTimingSessions.get(userId, pkgName);
+ if (sessions != null) {
+ for (int j = sessions.size() - 1; j >= 0; j--) {
+ TimingSession session = sessions.get(j);
+ session.dump(proto,
+ StateControllerProto.QuotaController.PackageStats.SAVED_SESSIONS);
+ }
+ }
+
+ ExecutionStats[] stats = mExecutionStatsCache.get(userId, pkgName);
+ if (stats != null) {
+ for (int bucketIndex = 0; bucketIndex < stats.length; ++bucketIndex) {
+ ExecutionStats es = stats[bucketIndex];
+ if (es == null) {
+ continue;
+ }
+ final long esToken = proto.start(
+ StateControllerProto.QuotaController.PackageStats.EXECUTION_STATS);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.STANDBY_BUCKET,
+ bucketIndex);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.EXPIRATION_TIME_ELAPSED,
+ es.expirationTimeElapsed);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.WINDOW_SIZE_MS,
+ es.windowSizeMs);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_LIMIT,
+ es.jobCountLimit);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_LIMIT,
+ es.sessionCountLimit);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_WINDOW_MS,
+ es.executionTimeInWindowMs);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_WINDOW,
+ es.bgJobCountInWindow);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.EXECUTION_TIME_IN_MAX_PERIOD_MS,
+ es.executionTimeInMaxPeriodMs);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.BG_JOB_COUNT_IN_MAX_PERIOD,
+ es.bgJobCountInMaxPeriod);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_WINDOW,
+ es.sessionCountInWindow);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.IN_QUOTA_TIME_ELAPSED,
+ es.inQuotaTimeElapsed);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_EXPIRATION_TIME_ELAPSED,
+ es.jobRateLimitExpirationTimeElapsed);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.JOB_COUNT_IN_RATE_LIMITING_WINDOW,
+ es.jobCountInRateLimitingWindow);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_EXPIRATION_TIME_ELAPSED,
+ es.sessionRateLimitExpirationTimeElapsed);
+ proto.write(
+ StateControllerProto.QuotaController.ExecutionStats.SESSION_COUNT_IN_RATE_LIMITING_WINDOW,
+ es.sessionCountInRateLimitingWindow);
+ proto.end(esToken);
+ }
+ }
+
+ QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, pkgName);
+ if (alarmListener != null) {
+ final long alToken = proto.start(
+ StateControllerProto.QuotaController.PackageStats.IN_QUOTA_ALARM_LISTENER);
+ proto.write(StateControllerProto.QuotaController.AlarmListener.IS_WAITING,
+ alarmListener.isWaiting());
+ proto.write(
+ StateControllerProto.QuotaController.AlarmListener.TRIGGER_TIME_ELAPSED,
+ alarmListener.getTriggerTimeElapsed());
+ proto.end(alToken);
+ }
+
+ proto.end(psToken);
+ }
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+
+ @Override
+ public void dumpConstants(IndentingPrintWriter pw) {
+ mQcConstants.dump(pw);
+ }
+
+ @Override
+ public void dumpConstants(ProtoOutputStream proto) {
+ mQcConstants.dump(proto);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
new file mode 100644
index 0000000..51be38b
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2014 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.server.job.JobSchedulerService.DEBUG;
+
+import android.content.Context;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobSchedulerService.Constants;
+import com.android.server.job.StateChangedListener;
+
+import java.util.function.Predicate;
+
+/**
+ * Incorporates shared controller logic between the various controllers of the JobManager.
+ * These are solely responsible for tracking a list of jobs, and notifying the JM when these
+ * are ready to run, or whether they must be stopped.
+ */
+public abstract class StateController {
+ private static final String TAG = "JobScheduler.SC";
+
+ protected final JobSchedulerService mService;
+ protected final StateChangedListener mStateChangedListener;
+ protected final Context mContext;
+ protected final Object mLock;
+ protected final Constants mConstants;
+
+ StateController(JobSchedulerService service) {
+ mService = service;
+ mStateChangedListener = service;
+ mContext = service.getTestableContext();
+ mLock = service.getLock();
+ mConstants = service.getConstants();
+ }
+
+ /**
+ * Called when the system boot phase has reached
+ * {@link com.android.server.SystemService#PHASE_SYSTEM_SERVICES_READY}.
+ */
+ public void onSystemServicesReady() {
+ }
+
+ /**
+ * Implement the logic here to decide whether a job should be tracked by this controller.
+ * This logic is put here so the JobManager can be completely agnostic of Controller logic.
+ * Also called when updating a task, so implementing controllers have to be aware of
+ * preexisting tasks.
+ */
+ public abstract void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob);
+
+ /**
+ * Optionally implement logic here to prepare the job to be executed.
+ */
+ public void prepareForExecutionLocked(JobStatus jobStatus) {
+ }
+
+ /**
+ * Remove task - this will happen if the task is cancelled, completed, etc.
+ */
+ public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob,
+ boolean forUpdate);
+
+ /**
+ * Called when a new job is being created to reschedule an old failed job.
+ */
+ public void rescheduleForFailureLocked(JobStatus newJob, JobStatus failureToReschedule) {
+ }
+
+ /**
+ * Called when the JobScheduler.Constants are updated.
+ */
+ public void onConstantsUpdatedLocked() {
+ }
+
+ /** Called when a package is uninstalled from the device (not for an update). */
+ public void onAppRemovedLocked(String packageName, int uid) {
+ }
+
+ /** Called when a user is removed from the device. */
+ public void onUserRemovedLocked(int userId) {
+ }
+
+ /**
+ * Called when JobSchedulerService has determined that the job is not ready to be run. The
+ * Controller can evaluate if it can or should do something to promote this job's readiness.
+ */
+ public void evaluateStateLocked(JobStatus jobStatus) {
+ }
+
+ /**
+ * Called when something with the UID has changed. The controller should re-evaluate any
+ * internal state tracking dependent on this UID.
+ */
+ public void reevaluateStateLocked(int uid) {
+ }
+
+ protected boolean wouldBeReadyWithConstraintLocked(JobStatus jobStatus, int constraint) {
+ // This is very cheap to check (just a few conditions on data in JobStatus).
+ final boolean jobWouldBeReady = jobStatus.wouldBeReadyWithConstraint(constraint);
+ if (DEBUG) {
+ Slog.v(TAG, "wouldBeReadyWithConstraintLocked: " + jobStatus.toShortString()
+ + " constraint=" + constraint
+ + " readyWithConstraint=" + jobWouldBeReady);
+ }
+ if (!jobWouldBeReady) {
+ // If the job wouldn't be ready, nothing to do here.
+ return false;
+ }
+
+ // This is potentially more expensive since JSS may have to query component
+ // presence.
+ return mService.areComponentsInPlaceLocked(jobStatus);
+ }
+
+ public abstract void dumpControllerStateLocked(IndentingPrintWriter pw,
+ Predicate<JobStatus> predicate);
+ public abstract void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate);
+
+ /** Dump any internal constants the Controller may have. */
+ public void dumpConstants(IndentingPrintWriter pw) {
+ }
+
+ /** Dump any internal constants the Controller may have. */
+ public void dumpConstants(ProtoOutputStream proto) {
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
new file mode 100644
index 0000000..51187df
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StorageController.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2017 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.server.job.JobSchedulerService.sElapsedRealtimeClock;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.UserHandle;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateControllerProto;
+import com.android.server.storage.DeviceStorageMonitorService;
+
+import java.util.function.Predicate;
+
+/**
+ * Simple controller that tracks the status of the device's storage.
+ */
+public final class StorageController extends StateController {
+ private static final String TAG = "JobScheduler.Storage";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ArraySet<JobStatus> mTrackedTasks = new ArraySet<JobStatus>();
+ private final StorageTracker mStorageTracker;
+
+ @VisibleForTesting
+ public StorageTracker getTracker() {
+ return mStorageTracker;
+ }
+
+ public StorageController(JobSchedulerService service) {
+ super(service);
+ mStorageTracker = new StorageTracker();
+ mStorageTracker.startTracking();
+ }
+
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
+ if (taskStatus.hasStorageNotLowConstraint()) {
+ mTrackedTasks.add(taskStatus);
+ taskStatus.setTrackingController(JobStatus.TRACKING_STORAGE);
+ taskStatus.setStorageNotLowConstraintSatisfied(mStorageTracker.isStorageNotLow());
+ }
+ }
+
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob,
+ boolean forUpdate) {
+ if (taskStatus.clearTrackingController(JobStatus.TRACKING_STORAGE)) {
+ mTrackedTasks.remove(taskStatus);
+ }
+ }
+
+ private void maybeReportNewStorageState() {
+ final boolean storageNotLow = mStorageTracker.isStorageNotLow();
+ boolean reportChange = false;
+ synchronized (mLock) {
+ for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
+ final JobStatus ts = mTrackedTasks.valueAt(i);
+ reportChange |= ts.setStorageNotLowConstraintSatisfied(storageNotLow);
+ }
+ }
+ if (storageNotLow) {
+ // Tell the scheduler that any ready jobs should be flushed.
+ mStateChangedListener.onRunJobNow(null);
+ } else if (reportChange) {
+ // Let the scheduler know that state has changed. This may or may not result in an
+ // execution.
+ mStateChangedListener.onControllerStateChanged();
+ }
+ }
+
+ public final class StorageTracker extends BroadcastReceiver {
+ /**
+ * Track whether storage is low.
+ */
+ private boolean mStorageLow;
+ /** Sequence number of last broadcast. */
+ private int mLastStorageSeq = -1;
+
+ public StorageTracker() {
+ }
+
+ public void startTracking() {
+ IntentFilter filter = new IntentFilter();
+
+ // Storage status. Just need to register, since STORAGE_LOW is a sticky
+ // broadcast we will receive that if it is currently active.
+ filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
+ filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
+ mContext.registerReceiver(this, filter);
+ }
+
+ public boolean isStorageNotLow() {
+ return !mStorageLow;
+ }
+
+ public int getSeq() {
+ return mLastStorageSeq;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ onReceiveInternal(intent);
+ }
+
+ @VisibleForTesting
+ public void onReceiveInternal(Intent intent) {
+ final String action = intent.getAction();
+ mLastStorageSeq = intent.getIntExtra(DeviceStorageMonitorService.EXTRA_SEQUENCE,
+ mLastStorageSeq);
+ if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Available storage too low to do work. @ "
+ + sElapsedRealtimeClock.millis());
+ }
+ mStorageLow = true;
+ maybeReportNewStorageState();
+ } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Available storage high enough to do work. @ "
+ + sElapsedRealtimeClock.millis());
+ }
+ mStorageLow = false;
+ maybeReportNewStorageState();
+ }
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(IndentingPrintWriter pw,
+ Predicate<JobStatus> predicate) {
+ pw.println("Not low: " + mStorageTracker.isStorageNotLow());
+ pw.println("Sequence: " + mStorageTracker.getSeq());
+ pw.println();
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ final JobStatus js = mTrackedTasks.valueAt(i);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ pw.print("#");
+ js.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, js.getSourceUid());
+ pw.println();
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.STORAGE);
+
+ proto.write(StateControllerProto.StorageController.IS_STORAGE_NOT_LOW,
+ mStorageTracker.isStorageNotLow());
+ proto.write(StateControllerProto.StorageController.LAST_BROADCAST_SEQUENCE_NUMBER,
+ mStorageTracker.getSeq());
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ final JobStatus js = mTrackedTasks.valueAt(i);
+ if (!predicate.test(js)) {
+ continue;
+ }
+ final long jsToken = proto.start(StateControllerProto.StorageController.TRACKED_JOBS);
+ js.writeToShortProto(proto, StateControllerProto.StorageController.TrackedJob.INFO);
+ proto.write(StateControllerProto.StorageController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.end(jsToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
new file mode 100644
index 0000000..4c11947
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java
@@ -0,0 +1,504 @@
+/*
+ * Copyright (C) 2014 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.server.job.JobSchedulerService.sElapsedRealtimeClock;
+
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.app.AlarmManager.OnAlarmListener;
+import android.content.Context;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.util.Log;
+import android.util.Slog;
+import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateControllerProto;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.function.Predicate;
+
+/**
+ * This class sets an alarm for the next expiring job, and determines whether a job's minimum
+ * delay has been satisfied.
+ */
+public final class TimeController extends StateController {
+ private static final String TAG = "JobScheduler.Time";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ /** Deadline alarm tag for logging purposes */
+ private final String DEADLINE_TAG = "*job.deadline*";
+ /** Delay alarm tag for logging purposes */
+ private final String DELAY_TAG = "*job.delay*";
+
+ private long mNextJobExpiredElapsedMillis;
+ private long mNextDelayExpiredElapsedMillis;
+
+ private final boolean mChainedAttributionEnabled;
+
+ private AlarmManager mAlarmService = null;
+ /** List of tracked jobs, sorted asc. by deadline */
+ private final List<JobStatus> mTrackedJobs = new LinkedList<>();
+
+ public TimeController(JobSchedulerService service) {
+ super(service);
+
+ mNextJobExpiredElapsedMillis = Long.MAX_VALUE;
+ mNextDelayExpiredElapsedMillis = Long.MAX_VALUE;
+ mChainedAttributionEnabled = mService.isChainedAttributionEnabled();
+ }
+
+ /**
+ * Check if the job has a timing constraint, and if so determine where to insert it in our
+ * list.
+ */
+ @Override
+ public void maybeStartTrackingJobLocked(JobStatus job, JobStatus lastJob) {
+ if (job.hasTimingDelayConstraint() || job.hasDeadlineConstraint()) {
+ maybeStopTrackingJobLocked(job, null, false);
+
+ // First: check the constraints now, because if they are already satisfied
+ // then there is no need to track it. This gives us a fast path for a common
+ // pattern of having a job with a 0 deadline constraint ("run immediately").
+ // Unlike most controllers, once one of our constraints has been satisfied, it
+ // will never be unsatisfied (our time base can not go backwards).
+ final long nowElapsedMillis = sElapsedRealtimeClock.millis();
+ if (job.hasDeadlineConstraint() && evaluateDeadlineConstraint(job, nowElapsedMillis)) {
+ return;
+ } else if (job.hasTimingDelayConstraint() && evaluateTimingDelayConstraint(job,
+ nowElapsedMillis)) {
+ if (!job.hasDeadlineConstraint()) {
+ // If it doesn't have a deadline, we'll never have to touch it again.
+ return;
+ }
+ }
+
+ boolean isInsert = false;
+ ListIterator<JobStatus> it = mTrackedJobs.listIterator(mTrackedJobs.size());
+ while (it.hasPrevious()) {
+ JobStatus ts = it.previous();
+ if (ts.getLatestRunTimeElapsed() < job.getLatestRunTimeElapsed()) {
+ // Insert
+ isInsert = true;
+ break;
+ }
+ }
+ if (isInsert) {
+ it.next();
+ }
+ it.add(job);
+
+ job.setTrackingController(JobStatus.TRACKING_TIME);
+ WorkSource ws = deriveWorkSource(job.getSourceUid(), job.getSourcePackageName());
+
+ // Only update alarms if the job would be ready with the relevant timing constraint
+ // satisfied.
+ if (job.hasTimingDelayConstraint()
+ && wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
+ maybeUpdateDelayAlarmLocked(job.getEarliestRunTime(), ws);
+ }
+ if (job.hasDeadlineConstraint()
+ && wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) {
+ maybeUpdateDeadlineAlarmLocked(job.getLatestRunTimeElapsed(), ws);
+ }
+ }
+ }
+
+ /**
+ * When we stop tracking a job, we only need to update our alarms if the job we're no longer
+ * tracking was the one our alarms were based off of.
+ */
+ @Override
+ public void maybeStopTrackingJobLocked(JobStatus job, JobStatus incomingJob,
+ boolean forUpdate) {
+ if (job.clearTrackingController(JobStatus.TRACKING_TIME)) {
+ if (mTrackedJobs.remove(job)) {
+ checkExpiredDelaysAndResetAlarm();
+ checkExpiredDeadlinesAndResetAlarm();
+ }
+ }
+ }
+
+ @Override
+ public void evaluateStateLocked(JobStatus job) {
+ final long nowElapsedMillis = sElapsedRealtimeClock.millis();
+
+ // Check deadline constraint first because if it's satisfied, we avoid a little bit of
+ // unnecessary processing of the timing delay.
+ if (job.hasDeadlineConstraint()
+ && !job.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE)
+ && job.getLatestRunTimeElapsed() <= mNextJobExpiredElapsedMillis) {
+ if (evaluateDeadlineConstraint(job, nowElapsedMillis)) {
+ checkExpiredDeadlinesAndResetAlarm();
+ checkExpiredDelaysAndResetAlarm();
+ } else {
+ final boolean isAlarmForJob =
+ job.getLatestRunTimeElapsed() == mNextJobExpiredElapsedMillis;
+ final boolean wouldBeReady = wouldBeReadyWithConstraintLocked(
+ job, JobStatus.CONSTRAINT_DEADLINE);
+ if ((isAlarmForJob && !wouldBeReady) || (!isAlarmForJob && wouldBeReady)) {
+ checkExpiredDeadlinesAndResetAlarm();
+ }
+ }
+ }
+ if (job.hasTimingDelayConstraint()
+ && !job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY)
+ && job.getEarliestRunTime() <= mNextDelayExpiredElapsedMillis) {
+ if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) {
+ checkExpiredDelaysAndResetAlarm();
+ } else {
+ final boolean isAlarmForJob =
+ job.getEarliestRunTime() == mNextDelayExpiredElapsedMillis;
+ final boolean wouldBeReady = wouldBeReadyWithConstraintLocked(
+ job, JobStatus.CONSTRAINT_TIMING_DELAY);
+ if ((isAlarmForJob && !wouldBeReady) || (!isAlarmForJob && wouldBeReady)) {
+ checkExpiredDelaysAndResetAlarm();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void reevaluateStateLocked(int uid) {
+ checkExpiredDeadlinesAndResetAlarm();
+ checkExpiredDelaysAndResetAlarm();
+ }
+
+ /**
+ * Determines whether this controller can stop tracking the given job.
+ * The controller is no longer interested in a job once its time constraint is satisfied, and
+ * the job's deadline is fulfilled - unlike other controllers a time constraint can't toggle
+ * back and forth.
+ */
+ private boolean canStopTrackingJobLocked(JobStatus job) {
+ return (!job.hasTimingDelayConstraint()
+ || job.isConstraintSatisfied(JobStatus.CONSTRAINT_TIMING_DELAY))
+ && (!job.hasDeadlineConstraint()
+ || job.isConstraintSatisfied(JobStatus.CONSTRAINT_DEADLINE));
+ }
+
+ private void ensureAlarmServiceLocked() {
+ if (mAlarmService == null) {
+ mAlarmService = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ }
+ }
+
+ /**
+ * Checks list of jobs for ones that have an expired deadline, sending them to the JobScheduler
+ * if so, removing them from this list, and updating the alarm for the next expiry time.
+ */
+ @VisibleForTesting
+ void checkExpiredDeadlinesAndResetAlarm() {
+ synchronized (mLock) {
+ long nextExpiryTime = Long.MAX_VALUE;
+ int nextExpiryUid = 0;
+ String nextExpiryPackageName = null;
+ final long nowElapsedMillis = sElapsedRealtimeClock.millis();
+
+ ListIterator<JobStatus> it = mTrackedJobs.listIterator();
+ while (it.hasNext()) {
+ JobStatus job = it.next();
+ if (!job.hasDeadlineConstraint()) {
+ continue;
+ }
+
+ if (evaluateDeadlineConstraint(job, nowElapsedMillis)) {
+ if (job.isReady()) {
+ // If the job still isn't ready, there's no point trying to rush the
+ // Scheduler.
+ mStateChangedListener.onRunJobNow(job);
+ }
+ it.remove();
+ } else { // Sorted by expiry time, so take the next one and stop.
+ if (!wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_DEADLINE)) {
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Skipping " + job + " because deadline won't make it ready.");
+ }
+ continue;
+ }
+ nextExpiryTime = job.getLatestRunTimeElapsed();
+ nextExpiryUid = job.getSourceUid();
+ nextExpiryPackageName = job.getSourcePackageName();
+ break;
+ }
+ }
+ setDeadlineExpiredAlarmLocked(nextExpiryTime,
+ deriveWorkSource(nextExpiryUid, nextExpiryPackageName));
+ }
+ }
+
+ /** @return true if the job's deadline constraint is satisfied */
+ private boolean evaluateDeadlineConstraint(JobStatus job, long nowElapsedMillis) {
+ final long jobDeadline = job.getLatestRunTimeElapsed();
+
+ if (jobDeadline <= nowElapsedMillis) {
+ if (job.hasTimingDelayConstraint()) {
+ job.setTimingDelayConstraintSatisfied(true);
+ }
+ job.setDeadlineConstraintSatisfied(true);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handles alarm that notifies us that a job's delay has expired. Iterates through the list of
+ * tracked jobs and marks them as ready as appropriate.
+ */
+ @VisibleForTesting
+ void checkExpiredDelaysAndResetAlarm() {
+ synchronized (mLock) {
+ final long nowElapsedMillis = sElapsedRealtimeClock.millis();
+ long nextDelayTime = Long.MAX_VALUE;
+ int nextDelayUid = 0;
+ String nextDelayPackageName = null;
+ boolean ready = false;
+ Iterator<JobStatus> it = mTrackedJobs.iterator();
+ while (it.hasNext()) {
+ final JobStatus job = it.next();
+ if (!job.hasTimingDelayConstraint()) {
+ continue;
+ }
+ if (evaluateTimingDelayConstraint(job, nowElapsedMillis)) {
+ if (canStopTrackingJobLocked(job)) {
+ it.remove();
+ }
+ if (job.isReady()) {
+ ready = true;
+ }
+ } else {
+ if (!wouldBeReadyWithConstraintLocked(job, JobStatus.CONSTRAINT_TIMING_DELAY)) {
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Skipping " + job + " because delay won't make it ready.");
+ }
+ continue;
+ }
+ // If this job still doesn't have its delay constraint satisfied,
+ // then see if it is the next upcoming delay time for the alarm.
+ final long jobDelayTime = job.getEarliestRunTime();
+ if (nextDelayTime > jobDelayTime) {
+ nextDelayTime = jobDelayTime;
+ nextDelayUid = job.getSourceUid();
+ nextDelayPackageName = job.getSourcePackageName();
+ }
+ }
+ }
+ if (ready) {
+ mStateChangedListener.onControllerStateChanged();
+ }
+ setDelayExpiredAlarmLocked(nextDelayTime,
+ deriveWorkSource(nextDelayUid, nextDelayPackageName));
+ }
+ }
+
+ private WorkSource deriveWorkSource(int uid, @Nullable String packageName) {
+ if (mChainedAttributionEnabled) {
+ WorkSource ws = new WorkSource();
+ ws.createWorkChain()
+ .addNode(uid, packageName)
+ .addNode(Process.SYSTEM_UID, "JobScheduler");
+ return ws;
+ } else {
+ return packageName == null ? new WorkSource(uid) : new WorkSource(uid, packageName);
+ }
+ }
+
+ /** @return true if the job's delay constraint is satisfied */
+ private boolean evaluateTimingDelayConstraint(JobStatus job, long nowElapsedMillis) {
+ final long jobDelayTime = job.getEarliestRunTime();
+ if (jobDelayTime <= nowElapsedMillis) {
+ job.setTimingDelayConstraintSatisfied(true);
+ return true;
+ }
+ return false;
+ }
+
+ private void maybeUpdateDelayAlarmLocked(long delayExpiredElapsed, WorkSource ws) {
+ if (delayExpiredElapsed < mNextDelayExpiredElapsedMillis) {
+ setDelayExpiredAlarmLocked(delayExpiredElapsed, ws);
+ }
+ }
+
+ private void maybeUpdateDeadlineAlarmLocked(long deadlineExpiredElapsed, WorkSource ws) {
+ if (deadlineExpiredElapsed < mNextJobExpiredElapsedMillis) {
+ setDeadlineExpiredAlarmLocked(deadlineExpiredElapsed, ws);
+ }
+ }
+
+ /**
+ * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
+ * delay will expire.
+ * This alarm <b>will</b> wake up the phone.
+ */
+ private void setDelayExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) {
+ alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
+ if (mNextDelayExpiredElapsedMillis == alarmTimeElapsedMillis) {
+ return;
+ }
+ mNextDelayExpiredElapsedMillis = alarmTimeElapsedMillis;
+ updateAlarmWithListenerLocked(DELAY_TAG, mNextDelayExpiredListener,
+ mNextDelayExpiredElapsedMillis, ws);
+ }
+
+ /**
+ * Set an alarm with the {@link android.app.AlarmManager} for the next time at which a job's
+ * deadline will expire.
+ * This alarm <b>will</b> wake up the phone.
+ */
+ private void setDeadlineExpiredAlarmLocked(long alarmTimeElapsedMillis, WorkSource ws) {
+ alarmTimeElapsedMillis = maybeAdjustAlarmTime(alarmTimeElapsedMillis);
+ if (mNextJobExpiredElapsedMillis == alarmTimeElapsedMillis) {
+ return;
+ }
+ mNextJobExpiredElapsedMillis = alarmTimeElapsedMillis;
+ updateAlarmWithListenerLocked(DEADLINE_TAG, mDeadlineExpiredListener,
+ mNextJobExpiredElapsedMillis, ws);
+ }
+
+ private long maybeAdjustAlarmTime(long proposedAlarmTimeElapsedMillis) {
+ return Math.max(proposedAlarmTimeElapsedMillis, sElapsedRealtimeClock.millis());
+ }
+
+ private void updateAlarmWithListenerLocked(String tag, OnAlarmListener listener,
+ long alarmTimeElapsed, WorkSource ws) {
+ ensureAlarmServiceLocked();
+ if (alarmTimeElapsed == Long.MAX_VALUE) {
+ mAlarmService.cancel(listener);
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Setting " + tag + " for: " + alarmTimeElapsed);
+ }
+ mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTimeElapsed,
+ AlarmManager.WINDOW_HEURISTIC, 0, tag, listener, null, ws);
+ }
+ }
+
+ // Job/delay expiration alarm handling
+
+ private final OnAlarmListener mDeadlineExpiredListener = new OnAlarmListener() {
+ @Override
+ public void onAlarm() {
+ if (DEBUG) {
+ Slog.d(TAG, "Deadline-expired alarm fired");
+ }
+ checkExpiredDeadlinesAndResetAlarm();
+ }
+ };
+
+ private final OnAlarmListener mNextDelayExpiredListener = new OnAlarmListener() {
+ @Override
+ public void onAlarm() {
+ if (DEBUG) {
+ Slog.d(TAG, "Delay-expired alarm fired");
+ }
+ checkExpiredDelaysAndResetAlarm();
+ }
+ };
+
+ @VisibleForTesting
+ void recheckAlarmsLocked() {
+ checkExpiredDeadlinesAndResetAlarm();
+ checkExpiredDelaysAndResetAlarm();
+ }
+
+ @Override
+ public void dumpControllerStateLocked(IndentingPrintWriter pw,
+ Predicate<JobStatus> predicate) {
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ pw.println("Elapsed clock: " + nowElapsed);
+
+ pw.print("Next delay alarm in ");
+ TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw);
+ pw.println();
+ pw.print("Next deadline alarm in ");
+ TimeUtils.formatDuration(mNextJobExpiredElapsedMillis, nowElapsed, pw);
+ pw.println();
+ pw.println();
+
+ for (JobStatus ts : mTrackedJobs) {
+ if (!predicate.test(ts)) {
+ continue;
+ }
+ pw.print("#");
+ ts.printUniqueId(pw);
+ pw.print(" from ");
+ UserHandle.formatUid(pw, ts.getSourceUid());
+ pw.print(": Delay=");
+ if (ts.hasTimingDelayConstraint()) {
+ TimeUtils.formatDuration(ts.getEarliestRunTime(), nowElapsed, pw);
+ } else {
+ pw.print("N/A");
+ }
+ pw.print(", Deadline=");
+ if (ts.hasDeadlineConstraint()) {
+ TimeUtils.formatDuration(ts.getLatestRunTimeElapsed(), nowElapsed, pw);
+ } else {
+ pw.print("N/A");
+ }
+ pw.println();
+ }
+ }
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ Predicate<JobStatus> predicate) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.TIME);
+
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ proto.write(StateControllerProto.TimeController.NOW_ELAPSED_REALTIME, nowElapsed);
+ proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DELAY_ALARM_MS,
+ mNextDelayExpiredElapsedMillis - nowElapsed);
+ proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DEADLINE_ALARM_MS,
+ mNextJobExpiredElapsedMillis - nowElapsed);
+
+ for (JobStatus ts : mTrackedJobs) {
+ if (!predicate.test(ts)) {
+ continue;
+ }
+ final long tsToken = proto.start(StateControllerProto.TimeController.TRACKED_JOBS);
+ ts.writeToShortProto(proto, StateControllerProto.TimeController.TrackedJob.INFO);
+
+ proto.write(StateControllerProto.TimeController.TrackedJob.HAS_TIMING_DELAY_CONSTRAINT,
+ ts.hasTimingDelayConstraint());
+ proto.write(StateControllerProto.TimeController.TrackedJob.DELAY_TIME_REMAINING_MS,
+ ts.getEarliestRunTime() - nowElapsed);
+
+ proto.write(StateControllerProto.TimeController.TrackedJob.HAS_DEADLINE_CONSTRAINT,
+ ts.hasDeadlineConstraint());
+ proto.write(StateControllerProto.TimeController.TrackedJob.TIME_REMAINING_UNTIL_DEADLINE_MS,
+ ts.getLatestRunTimeElapsed() - nowElapsed);
+
+ proto.end(tsToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java
new file mode 100644
index 0000000..82c33f5
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/CarIdlenessTracker.java
@@ -0,0 +1,175 @@
+/*
+ * 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.idle;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+import android.util.Slog;
+
+import com.android.server.am.ActivityManagerService;
+import com.android.server.job.JobSchedulerService;
+
+import java.io.PrintWriter;
+
+public final class CarIdlenessTracker extends BroadcastReceiver implements IdlenessTracker {
+ private static final String TAG = "JobScheduler.CarIdlenessTracker";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ public static final String ACTION_GARAGE_MODE_ON =
+ "com.android.server.jobscheduler.GARAGE_MODE_ON";
+ public static final String ACTION_GARAGE_MODE_OFF =
+ "com.android.server.jobscheduler.GARAGE_MODE_OFF";
+
+ public static final String ACTION_FORCE_IDLE = "com.android.server.jobscheduler.FORCE_IDLE";
+ public static final String ACTION_UNFORCE_IDLE = "com.android.server.jobscheduler.UNFORCE_IDLE";
+
+ // After construction, mutations of idle/screen-on state will only happen
+ // on the main looper thread, either in onReceive() or in an alarm callback.
+ private boolean mIdle;
+ private boolean mGarageModeOn;
+ private boolean mForced;
+ private IdlenessListener mIdleListener;
+
+ public CarIdlenessTracker() {
+ // At boot we presume that the user has just "interacted" with the
+ // device in some meaningful way.
+ mIdle = false;
+ mGarageModeOn = false;
+ mForced = false;
+ }
+
+ @Override
+ public boolean isIdle() {
+ return mIdle;
+ }
+
+ @Override
+ public void startTracking(Context context, IdlenessListener listener) {
+ mIdleListener = listener;
+
+ IntentFilter filter = new IntentFilter();
+
+ // Screen state
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+
+ // State of GarageMode
+ filter.addAction(ACTION_GARAGE_MODE_ON);
+ filter.addAction(ACTION_GARAGE_MODE_OFF);
+
+ // Debugging/instrumentation
+ filter.addAction(ACTION_FORCE_IDLE);
+ filter.addAction(ACTION_UNFORCE_IDLE);
+ filter.addAction(ActivityManagerService.ACTION_TRIGGER_IDLE);
+
+ context.registerReceiver(this, filter);
+ }
+
+ @Override
+ public void dump(PrintWriter pw) {
+ pw.print(" mIdle: "); pw.println(mIdle);
+ pw.print(" mGarageModeOn: "); pw.println(mGarageModeOn);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ logIfDebug("Received action: " + action);
+
+ // Check for forced actions
+ if (action.equals(ACTION_FORCE_IDLE)) {
+ logIfDebug("Forcing idle...");
+ setForceIdleState(true);
+ } else if (action.equals(ACTION_UNFORCE_IDLE)) {
+ logIfDebug("Unforcing idle...");
+ setForceIdleState(false);
+ } else if (action.equals(Intent.ACTION_SCREEN_ON)) {
+ logIfDebug("Screen is on...");
+ handleScreenOn();
+ } else if (action.equals(ACTION_GARAGE_MODE_ON)) {
+ logIfDebug("GarageMode is on...");
+ mGarageModeOn = true;
+ updateIdlenessState();
+ } else if (action.equals(ACTION_GARAGE_MODE_OFF)) {
+ logIfDebug("GarageMode is off...");
+ mGarageModeOn = false;
+ updateIdlenessState();
+ } else if (action.equals(ActivityManagerService.ACTION_TRIGGER_IDLE)) {
+ if (!mGarageModeOn) {
+ logIfDebug("Idle trigger fired...");
+ triggerIdlenessOnce();
+ } else {
+ logIfDebug("TRIGGER_IDLE received but not changing state; idle="
+ + mIdle + " screen=" + mGarageModeOn);
+ }
+ }
+ }
+
+ private void setForceIdleState(boolean forced) {
+ mForced = forced;
+ updateIdlenessState();
+ }
+
+ private void updateIdlenessState() {
+ final boolean newState = (mForced || mGarageModeOn);
+ if (mIdle != newState) {
+ // State of idleness changed. Notifying idleness controller
+ logIfDebug("Device idleness changed. New idle=" + newState);
+ mIdle = newState;
+ mIdleListener.reportNewIdleState(mIdle);
+ } else {
+ // Nothing changed, device idleness is in the same state as new state
+ logIfDebug("Device idleness is the same. Current idle=" + newState);
+ }
+ }
+
+ private void triggerIdlenessOnce() {
+ // This is simply triggering idleness once until some constraint will switch it back off
+ if (mIdle) {
+ // Already in idle state. Nothing to do
+ logIfDebug("Device is already idle");
+ } else {
+ // Going idle once
+ logIfDebug("Device is going idle once");
+ mIdle = true;
+ mIdleListener.reportNewIdleState(mIdle);
+ }
+ }
+
+ private void handleScreenOn() {
+ if (mForced || mGarageModeOn) {
+ // Even though screen is on, the device remains idle
+ logIfDebug("Screen is on, but device cannot exit idle");
+ } else if (mIdle) {
+ // Exiting idle
+ logIfDebug("Device is exiting idle");
+ mIdle = false;
+ } else {
+ // Already in non-idle state. Nothing to do
+ logIfDebug("Device is already non-idle");
+ }
+ }
+
+ private static void logIfDebug(String msg) {
+ if (DEBUG) {
+ Slog.v(TAG, msg);
+ }
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java
new file mode 100644
index 0000000..a85bd40
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java
@@ -0,0 +1,175 @@
+/*
+ * 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.idle;
+
+import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
+
+import android.app.AlarmManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import android.util.Log;
+import android.util.Slog;
+import com.android.server.am.ActivityManagerService;
+import com.android.server.job.JobSchedulerService;
+
+import java.io.PrintWriter;
+
+public final class DeviceIdlenessTracker extends BroadcastReceiver implements IdlenessTracker {
+ private static final String TAG = "JobScheduler.DeviceIdlenessTracker";
+ private static final boolean DEBUG = JobSchedulerService.DEBUG
+ || Log.isLoggable(TAG, Log.DEBUG);
+
+ private AlarmManager mAlarm;
+
+ // After construction, mutations of idle/screen-on state will only happen
+ // on the main looper thread, either in onReceive() or in an alarm callback.
+ private long mInactivityIdleThreshold;
+ private long mIdleWindowSlop;
+ private boolean mIdle;
+ private boolean mScreenOn;
+ private boolean mDockIdle;
+ private IdlenessListener mIdleListener;
+
+ private AlarmManager.OnAlarmListener mIdleAlarmListener = () -> {
+ handleIdleTrigger();
+ };
+
+ public DeviceIdlenessTracker() {
+ // At boot we presume that the user has just "interacted" with the
+ // device in some meaningful way.
+ mIdle = false;
+ mScreenOn = true;
+ mDockIdle = false;
+ }
+
+ @Override
+ public boolean isIdle() {
+ return mIdle;
+ }
+
+ @Override
+ public void startTracking(Context context, IdlenessListener listener) {
+ mIdleListener = listener;
+ mInactivityIdleThreshold = context.getResources().getInteger(
+ com.android.internal.R.integer.config_jobSchedulerInactivityIdleThreshold);
+ mIdleWindowSlop = context.getResources().getInteger(
+ com.android.internal.R.integer.config_jobSchedulerIdleWindowSlop);
+ mAlarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+
+ IntentFilter filter = new IntentFilter();
+
+ // Screen state
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+
+ // Dreaming state
+ filter.addAction(Intent.ACTION_DREAMING_STARTED);
+ filter.addAction(Intent.ACTION_DREAMING_STOPPED);
+
+ // Debugging/instrumentation
+ filter.addAction(ActivityManagerService.ACTION_TRIGGER_IDLE);
+
+ // Wireless charging dock state
+ filter.addAction(Intent.ACTION_DOCK_IDLE);
+ filter.addAction(Intent.ACTION_DOCK_ACTIVE);
+
+ context.registerReceiver(this, filter);
+ }
+
+ @Override
+ public void dump(PrintWriter pw) {
+ pw.print(" mIdle: "); pw.println(mIdle);
+ pw.print(" mScreenOn: "); pw.println(mScreenOn);
+ pw.print(" mDockIdle: "); pw.println(mDockIdle);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (action.equals(Intent.ACTION_SCREEN_ON)
+ || action.equals(Intent.ACTION_DREAMING_STOPPED)
+ || action.equals(Intent.ACTION_DOCK_ACTIVE)) {
+ if (action.equals(Intent.ACTION_DOCK_ACTIVE)) {
+ if (!mScreenOn) {
+ // Ignore this intent during screen off
+ return;
+ } else {
+ mDockIdle = false;
+ }
+ } else {
+ mScreenOn = true;
+ mDockIdle = false;
+ }
+ if (DEBUG) {
+ Slog.v(TAG,"exiting idle : " + action);
+ }
+ //cancel the alarm
+ mAlarm.cancel(mIdleAlarmListener);
+ if (mIdle) {
+ // possible transition to not-idle
+ mIdle = false;
+ mIdleListener.reportNewIdleState(mIdle);
+ }
+ } else if (action.equals(Intent.ACTION_SCREEN_OFF)
+ || action.equals(Intent.ACTION_DREAMING_STARTED)
+ || action.equals(Intent.ACTION_DOCK_IDLE)) {
+ // when the screen goes off or dreaming starts or wireless charging dock in idle,
+ // we schedule the alarm that will tell us when we have decided the device is
+ // truly idle.
+ if (action.equals(Intent.ACTION_DOCK_IDLE)) {
+ if (!mScreenOn) {
+ // Ignore this intent during screen off
+ return;
+ } else {
+ mDockIdle = true;
+ }
+ } else {
+ mScreenOn = false;
+ mDockIdle = false;
+ }
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final long when = nowElapsed + mInactivityIdleThreshold;
+ if (DEBUG) {
+ Slog.v(TAG, "Scheduling idle : " + action + " now:" + nowElapsed + " when="
+ + when);
+ }
+ mAlarm.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ when, mIdleWindowSlop, "JS idleness", mIdleAlarmListener, null);
+ } else if (action.equals(ActivityManagerService.ACTION_TRIGGER_IDLE)) {
+ handleIdleTrigger();
+ }
+ }
+
+ private void handleIdleTrigger() {
+ // idle time starts now. Do not set mIdle if screen is on.
+ if (!mIdle && (!mScreenOn || mDockIdle)) {
+ if (DEBUG) {
+ Slog.v(TAG, "Idle trigger fired @ " + sElapsedRealtimeClock.millis());
+ }
+ mIdle = true;
+ mIdleListener.reportNewIdleState(mIdle);
+ } else {
+ if (DEBUG) {
+ Slog.v(TAG, "TRIGGER_IDLE received but not changing state; idle="
+ + mIdle + " screen=" + mScreenOn);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java
new file mode 100644
index 0000000..7ffd7cd
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessListener.java
@@ -0,0 +1,32 @@
+/*
+ * 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.idle;
+
+/**
+ * Interface through which an IdlenessTracker informs the job scheduler of
+ * changes in the device's inactivity state.
+ */
+public interface IdlenessListener {
+ /**
+ * Tell the job scheduler that the device's idle state has changed.
+ *
+ * @param deviceIsIdle {@code true} to indicate that the device is now considered
+ * to be idle; {@code false} to indicate that the device is now being interacted with,
+ * so jobs with idle constraints should not be run.
+ */
+ void reportNewIdleState(boolean deviceIsIdle);
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java
new file mode 100644
index 0000000..09f01c2
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/IdlenessTracker.java
@@ -0,0 +1,46 @@
+/*
+ * 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.idle;
+
+import android.content.Context;
+
+import java.io.PrintWriter;
+
+public interface IdlenessTracker {
+ /**
+ * One-time initialization: this method is called once, after construction of
+ * the IdlenessTracker instance. This is when the tracker should actually begin
+ * monitoring whatever signals it consumes in deciding when the device is in a
+ * non-interacting state. When the idle state changes thereafter, the given
+ * listener must be called to report the new state.
+ */
+ void startTracking(Context context, IdlenessListener listener);
+
+ /**
+ * Report whether the device is currently considered "idle" for purposes of
+ * running scheduled jobs with idleness constraints.
+ *
+ * @return {@code true} if the job scheduler should consider idleness
+ * constraints to be currently satisfied; {@code false} otherwise.
+ */
+ boolean isIdle();
+
+ /**
+ * Dump useful information about tracked idleness-related state in plaintext.
+ */
+ void dump(PrintWriter pw);
+}