Merge "Make job execution limits flexible." into sc-dev
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 930415f..b7a3f10 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -59,6 +59,12 @@
  * constraint on the JobInfo object that you are creating. Otherwise, the builder would throw an
  * exception when building. From Android version {@link Build.VERSION_CODES#Q} and onwards, it is
  * valid to schedule jobs with no constraints.
+ * <p> In Android version {@link Build.VERSION_CODES#LOLLIPOP}, jobs had a maximum execution time
+ * of one minute. Starting with Android version {@link Build.VERSION_CODES#M} and ending with
+ * Android version {@link Build.VERSION_CODES#R}, jobs had a maximum execution time of 10 minutes.
+ * Starting from Android version {@link Build.VERSION_CODES#S}, jobs will still be stopped after
+ * 10 minutes if the system is busy or needs the resources, but if not, jobs may continue running
+ * longer than 10 minutes.
  */
 public class JobInfo implements Parcelable {
     private static String TAG = "JobInfo";
@@ -1461,11 +1467,13 @@
          * possible with stronger guarantees than regular jobs. These "expedited" jobs will:
          * <ol>
          *     <li>Run as soon as possible</li>
-         *     <li>Be exempted from Doze and battery saver restrictions</li>
+         *     <li>Be less restricted during Doze and battery saver</li>
          *     <li>Have network access</li>
-         *     <li>Less likely to be killed than regular jobs</li>
+         *     <li>Be less likely to be killed than regular jobs</li>
+         *     <li>Be subject to background location throttling</li>
          * </ol>
          *
+         * <p>
          * Since these jobs have stronger guarantees than regular jobs, they will be subject to
          * stricter quotas. As long as an app has available expedited quota, jobs scheduled with
          * this set to true will run with these guarantees. If an app has run out of available
@@ -1475,9 +1483,18 @@
          * will immediately return {@link JobScheduler#RESULT_FAILURE} if the app does not have
          * available quota (and the job will not be successfully scheduled).
          *
+         * <p>
          * Expedited jobs may only set network, storage-not-low, and persistence constraints.
          * No other constraints are allowed.
          *
+         * <p>
+         * Assuming all constraints remain satisfied (including ideal system load conditions),
+         * expedited jobs are guaranteed to have a minimum allowed runtime of 1 minute. If your
+         * app has remaining expedited job quota, then the expedited job <i>may</i> potentially run
+         * longer until remaining quota is used up. Just like with regular jobs, quota is not
+         * consumed while the app is on top and visible to the user.
+         *
+         * <p>
          * Note: Even though expedited jobs are meant to run as soon as possible, they may be
          * deferred if the system is under heavy load or requested constraints are not satisfied.
          *
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 164781a..af97715 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -20,6 +20,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.app.UserSwitchObserver;
@@ -80,6 +81,8 @@
     static final int WORK_TYPE_BGUSER = 1 << 3;
     @VisibleForTesting
     static final int NUM_WORK_TYPES = 4;
+    private static final int ALL_WORK_TYPES =
+            WORK_TYPE_TOP | WORK_TYPE_EJ | WORK_TYPE_BG | WORK_TYPE_BGUSER;
 
     @IntDef(prefix = {"WORK_TYPE_"}, flag = true, value = {
             WORK_TYPE_NONE,
@@ -92,6 +95,23 @@
     public @interface WorkType {
     }
 
+    private static String workTypeToString(@WorkType int workType) {
+        switch (workType) {
+            case WORK_TYPE_NONE:
+                return "NONE";
+            case WORK_TYPE_TOP:
+                return "TOP";
+            case WORK_TYPE_EJ:
+                return "EJ";
+            case WORK_TYPE_BG:
+                return "BG";
+            case WORK_TYPE_BGUSER:
+                return "BGUSER";
+            default:
+                return "WORK(" + workType + ")";
+        }
+    }
+
     private final Object mLock;
     private final JobSchedulerService mService;
     private final Context mContext;
@@ -182,10 +202,16 @@
 
     int[] mRecycledWorkTypeForContext = new int[MAX_JOB_CONTEXTS_COUNT];
 
+    String[] mRecycledPreemptReasonForContext = new String[MAX_JOB_CONTEXTS_COUNT];
+
+    String[] mRecycledShouldStopJobReason = new String[MAX_JOB_CONTEXTS_COUNT];
+
     private final ArraySet<JobStatus> mRunningJobs = new ArraySet<>();
 
     private final WorkCountTracker mWorkCountTracker = new WorkCountTracker();
 
+    private WorkTypeConfig mWorkTypeConfig = CONFIG_LIMITS_SCREEN_OFF.normal;
+
     /** Wait for this long after screen off before adjusting the job concurrency. */
     private long mScreenOffAdjustmentDelayMs = DEFAULT_SCREEN_OFF_ADJUSTMENT_DELAY_MS;
 
@@ -353,23 +379,22 @@
         final WorkConfigLimitsPerMemoryTrimLevel workConfigs = mEffectiveInteractiveState
                 ? CONFIG_LIMITS_SCREEN_ON : CONFIG_LIMITS_SCREEN_OFF;
 
-        WorkTypeConfig workTypeConfig;
         switch (mLastMemoryTrimLevel) {
             case ProcessStats.ADJ_MEM_FACTOR_MODERATE:
-                workTypeConfig = workConfigs.moderate;
+                mWorkTypeConfig = workConfigs.moderate;
                 break;
             case ProcessStats.ADJ_MEM_FACTOR_LOW:
-                workTypeConfig = workConfigs.low;
+                mWorkTypeConfig = workConfigs.low;
                 break;
             case ProcessStats.ADJ_MEM_FACTOR_CRITICAL:
-                workTypeConfig = workConfigs.critical;
+                mWorkTypeConfig = workConfigs.critical;
                 break;
             default:
-                workTypeConfig = workConfigs.normal;
+                mWorkTypeConfig = workConfigs.normal;
                 break;
         }
 
-        mWorkCountTracker.setConfig(workTypeConfig);
+        mWorkCountTracker.setConfig(mWorkTypeConfig);
     }
 
     /**
@@ -401,13 +426,20 @@
         boolean[] slotChanged = mRecycledSlotChanged;
         int[] preferredUidForContext = mRecycledPreferredUidForContext;
         int[] workTypeForContext = mRecycledWorkTypeForContext;
+        String[] preemptReasonForContext = mRecycledPreemptReasonForContext;
+        String[] shouldStopJobReason = mRecycledShouldStopJobReason;
 
         updateCounterConfigLocked();
         // Reset everything since we'll re-evaluate the current state.
         mWorkCountTracker.resetCounts();
 
+        // Update the priorities of jobs that aren't running, and also count the pending work types.
+        // Do this before the following loop to hopefully reduce the cost of
+        // shouldStopRunningJobLocked().
+        updateNonRunningPriorities(pendingJobs, true);
+
         for (int i = 0; i < MAX_JOB_CONTEXTS_COUNT; i++) {
-            final JobServiceContext js = mService.mActiveServices.get(i);
+            final JobServiceContext js = activeServices.get(i);
             final JobStatus status = js.getRunningJobLocked();
 
             if ((contextIdToJobMap[i] = status) != null) {
@@ -417,14 +449,13 @@
 
             slotChanged[i] = false;
             preferredUidForContext[i] = js.getPreferredUid();
+            preemptReasonForContext[i] = null;
+            shouldStopJobReason[i] = shouldStopRunningJobLocked(js);
         }
         if (DEBUG) {
             Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial"));
         }
 
-        // Next, update the job priorities, and also count the pending FG / BG jobs.
-        updateNonRunningPriorities(pendingJobs, true);
-
         mWorkCountTracker.onCountDone();
 
         for (int i = 0; i < pendingJobs.size(); i++) {
@@ -434,8 +465,6 @@
                 continue;
             }
 
-            // TODO(171305774): make sure HPJs aren't pre-empted and add dedicated contexts for them
-
             // 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)
@@ -444,6 +473,9 @@
             int allWorkTypes = getJobWorkTypes(nextPending);
             int workType = mWorkCountTracker.canJobStart(allWorkTypes);
             boolean startingJob = false;
+            String preemptReason = null;
+            // TODO(141645789): rewrite this to look at empty contexts first so we don't
+            // unnecessarily preempt
             for (int j = 0; j < MAX_JOB_CONTEXTS_COUNT; j++) {
                 JobStatus job = contextIdToJobMap[j];
                 int preferredUid = preferredUidForContext[j];
@@ -464,6 +496,15 @@
                     continue;
                 }
                 if (job.getUid() != nextPending.getUid()) {
+                    // Maybe stop the job if it has had its day in the sun.
+                    final String reason = shouldStopJobReason[j];
+                    if (reason != null && mWorkCountTracker.canJobStart(allWorkTypes,
+                            activeServices.get(j).getRunningJobWorkType()) != WORK_TYPE_NONE) {
+                        // Right now, the way the code is set up, we don't need to explicitly
+                        // assign the new job to this context since we'll reassign when the
+                        // preempted job finally stops.
+                        preemptReason = reason;
+                    }
                     continue;
                 }
 
@@ -477,6 +518,7 @@
                     // the lowest-priority running job
                     minPriorityForPreemption = jobPriority;
                     selectedContextId = j;
+                    preemptReason = "higher priority job found";
                     // 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.
                 }
@@ -484,6 +526,7 @@
             if (selectedContextId != -1) {
                 contextIdToJobMap[selectedContextId] = nextPending;
                 slotChanged[selectedContextId] = true;
+                preemptReasonForContext[selectedContextId] = preemptReason;
             }
             if (startingJob) {
                 // Increase the counters when we're going to start a job.
@@ -509,7 +552,7 @@
                                 + activeServices.get(i).getRunningJobLocked());
                     }
                     // preferredUid will be set to uid of currently running job.
-                    activeServices.get(i).preemptExecutingJobLocked();
+                    activeServices.get(i).preemptExecutingJobLocked(preemptReasonForContext[i]);
                     preservePreferredUid = true;
                 } else {
                     final JobStatus pendingJob = contextIdToJobMap[i];
@@ -692,6 +735,91 @@
         noteConcurrency();
     }
 
+    /**
+     * Returns {@code null} if the job can continue running and a non-null String if the job should
+     * be stopped. The non-null String details the reason for stopping the job. A job will generally
+     * be stopped if there similar job types waiting to be run and stopping this job would allow
+     * another job to run, or if system state suggests the job should stop.
+     */
+    @Nullable
+    String shouldStopRunningJobLocked(@NonNull JobServiceContext context) {
+        final JobStatus js = context.getRunningJobLocked();
+        if (js == null) {
+            // This can happen when we try to assign newly found pending jobs to contexts.
+            return null;
+        }
+
+        if (context.isWithinExecutionGuaranteeTime()) {
+            return null;
+        }
+
+        // Update config in case memory usage has changed significantly.
+        updateCounterConfigLocked();
+
+        @WorkType final int workType = context.getRunningJobWorkType();
+
+        // We're over the minimum guaranteed runtime. Stop the job if we're over config limits or
+        // there are pending jobs that could replace this one.
+        if (mRunningJobs.size() > mWorkTypeConfig.getMaxTotal()
+                || mWorkCountTracker.isOverTypeLimit(workType)) {
+            return "too many jobs running";
+        }
+
+        final List<JobStatus> pendingJobs = mService.mPendingJobs;
+        final int numPending = pendingJobs.size();
+        if (numPending == 0) {
+            // All quiet. We can let this job run to completion.
+            return null;
+        }
+
+        // Only expedited jobs can replace expedited jobs.
+        if (js.shouldTreatAsExpeditedJob()) {
+            // Keep fg/bg user distinction.
+            if (workType == WORK_TYPE_BGUSER) {
+                // For now, let any bg user job replace a bg user expedited job.
+                // TODO: limit to ej once we have dedicated bg user ej slots.
+                if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_BGUSER) > 0) {
+                    return "blocking " + workTypeToString(workType) + " queue";
+                }
+            } else {
+                if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0) {
+                    return "blocking " + workTypeToString(workType) + " queue";
+                }
+            }
+
+            if (mPowerManager.isPowerSaveMode()) {
+                return "battery saver";
+            }
+            if (mPowerManager.isDeviceIdleMode()) {
+                return "deep doze";
+            }
+        }
+
+        // Easy check. If there are pending jobs of the same work type, then we know that
+        // something will replace this.
+        if (mWorkCountTracker.getPendingJobCount(workType) > 0) {
+            return "blocking " + workTypeToString(workType) + " queue";
+        }
+
+        // Harder check. We need to see if a different work type can replace this job.
+        int remainingWorkTypes = ALL_WORK_TYPES;
+        for (int i = 0; i < numPending; ++i) {
+            final JobStatus pending = pendingJobs.get(i);
+            final int workTypes = getJobWorkTypes(pending);
+            if ((workTypes & remainingWorkTypes) > 0
+                    && mWorkCountTracker.canJobStart(workTypes, workType) != WORK_TYPE_NONE) {
+                return "blocking other pending jobs";
+            }
+
+            remainingWorkTypes = remainingWorkTypes & ~workTypes;
+            if (remainingWorkTypes == 0) {
+                break;
+            }
+        }
+
+        return null;
+    }
+
     @GuardedBy("mLock")
     private String printPendingQueueLocked() {
         StringBuilder s = new StringBuilder("Pending queue: ");
@@ -1362,10 +1490,40 @@
             return WORK_TYPE_NONE;
         }
 
+        int canJobStart(int workTypes, @WorkType int replacingWorkType) {
+            final boolean changedNums;
+            int oldNumRunning = mNumRunningJobs.get(replacingWorkType);
+            if (replacingWorkType != WORK_TYPE_NONE && oldNumRunning > 0) {
+                mNumRunningJobs.put(replacingWorkType, oldNumRunning - 1);
+                // Lazy implementation to avoid lots of processing. Best way would be to go
+                // through the whole process of adjusting reservations, but the processing cost
+                // is likely not worth it.
+                mNumUnspecializedRemaining++;
+                changedNums = true;
+            } else {
+                changedNums = false;
+            }
+
+            final int ret = canJobStart(workTypes);
+            if (changedNums) {
+                mNumRunningJobs.put(replacingWorkType, oldNumRunning);
+                mNumUnspecializedRemaining--;
+            }
+            return ret;
+        }
+
+        int getPendingJobCount(@WorkType final int workType) {
+            return mNumPendingJobs.get(workType, 0);
+        }
+
         int getRunningJobCount(@WorkType final int workType) {
             return mNumRunningJobs.get(workType, 0);
         }
 
+        boolean isOverTypeLimit(@WorkType final int workType) {
+            return getRunningJobCount(workType) > mConfigAbsoluteMaxSlots.get(workType);
+        }
+
         public String toString() {
             StringBuilder sb = new StringBuilder();
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index fdbc086..00fc937 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -339,6 +339,7 @@
         public void onPropertiesChanged(DeviceConfig.Properties properties) {
             boolean apiQuotaScheduleUpdated = false;
             boolean concurrencyUpdated = false;
+            boolean runtimeUpdated = false;
             for (int controller = 0; controller < mControllers.size(); controller++) {
                 final StateController sc = mControllers.get(controller);
                 sc.prepareForUpdatedConstantsLocked();
@@ -377,6 +378,14 @@
                         case Constants.KEY_CONN_PREFETCH_RELAX_FRAC:
                             mConstants.updateConnectivityConstantsLocked();
                             break;
+                        case Constants.KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS:
+                        case Constants.KEY_RUNTIME_MIN_GUARANTEE_MS:
+                        case Constants.KEY_RUNTIME_MIN_EJ_GUARANTEE_MS:
+                            if (!runtimeUpdated) {
+                                mConstants.updateRuntimeConstantsLocked();
+                                runtimeUpdated = true;
+                            }
+                            break;
                         default:
                             if (name.startsWith(JobConcurrencyManager.CONFIG_KEY_PREFIX_CONCURRENCY)
                                     && !concurrencyUpdated) {
@@ -432,6 +441,11 @@
         private static final String KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT =
                 "aq_schedule_return_failure";
 
+        private static final String KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS =
+                "runtime_free_quota_max_limit_ms";
+        private static final String KEY_RUNTIME_MIN_GUARANTEE_MS = "runtime_min_guarantee_ms";
+        private static final String KEY_RUNTIME_MIN_EJ_GUARANTEE_MS = "runtime_min_ej_guarantee_ms";
+
         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;
@@ -445,6 +459,12 @@
         private static final long DEFAULT_API_QUOTA_SCHEDULE_WINDOW_MS = MINUTE_IN_MILLIS;
         private static final boolean DEFAULT_API_QUOTA_SCHEDULE_THROW_EXCEPTION = true;
         private static final boolean DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT = false;
+        @VisibleForTesting
+        public static final long DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = 30 * MINUTE_IN_MILLIS;
+        @VisibleForTesting
+        public static final long DEFAULT_RUNTIME_MIN_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS;
+        @VisibleForTesting
+        public static final long DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS = 3 * MINUTE_IN_MILLIS;
 
         /**
          * Minimum # of non-ACTIVE jobs for which the JMS will be happy running some work early.
@@ -509,6 +529,19 @@
         public boolean API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT =
                 DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT;
 
+        /** The maximum amount of time we will let a job run for when quota is "free". */
+        public long RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
+
+        /**
+         * The minimum amount of time we try to guarantee regular jobs will run for.
+         */
+        public long RUNTIME_MIN_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_GUARANTEE_MS;
+
+        /**
+         * The minimum amount of time we try to guarantee EJs will run for.
+         */
+        public long RUNTIME_MIN_EJ_GUARANTEE_MS = DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS;
+
         private void updateBatchingConstantsLocked() {
             MIN_READY_NON_ACTIVE_JOBS_COUNT = DeviceConfig.getInt(
                     DeviceConfig.NAMESPACE_JOB_SCHEDULER,
@@ -568,6 +601,25 @@
                     DEFAULT_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT);
         }
 
+        private void updateRuntimeConstantsLocked() {
+            DeviceConfig.Properties properties = DeviceConfig.getProperties(
+                    DeviceConfig.NAMESPACE_JOB_SCHEDULER,
+                    KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+                    KEY_RUNTIME_MIN_GUARANTEE_MS, KEY_RUNTIME_MIN_EJ_GUARANTEE_MS);
+
+            // Make sure min runtime for regular jobs is at least 10 minutes.
+            RUNTIME_MIN_GUARANTEE_MS = Math.max(10 * MINUTE_IN_MILLIS,
+                    properties.getLong(
+                            KEY_RUNTIME_MIN_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_GUARANTEE_MS));
+            // Make sure min runtime for expedited jobs is at least one minute.
+            RUNTIME_MIN_EJ_GUARANTEE_MS = Math.max(MINUTE_IN_MILLIS,
+                    properties.getLong(
+                            KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, DEFAULT_RUNTIME_MIN_EJ_GUARANTEE_MS));
+            RUNTIME_FREE_QUOTA_MAX_LIMIT_MS = Math.max(RUNTIME_MIN_GUARANTEE_MS,
+                    properties.getLong(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+                            DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS));
+        }
+
         void dump(IndentingPrintWriter pw) {
             pw.println("Settings:");
             pw.increaseIndent();
@@ -591,6 +643,11 @@
             pw.print(KEY_API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT,
                     API_QUOTA_SCHEDULE_RETURN_FAILURE_RESULT).println();
 
+            pw.print(KEY_RUNTIME_MIN_GUARANTEE_MS, RUNTIME_MIN_GUARANTEE_MS).println();
+            pw.print(KEY_RUNTIME_MIN_EJ_GUARANTEE_MS, RUNTIME_MIN_EJ_GUARANTEE_MS).println();
+            pw.print(KEY_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, RUNTIME_FREE_QUOTA_MAX_LIMIT_MS)
+                    .println();
+
             pw.decreaseIndent();
         }
 
@@ -1602,7 +1659,7 @@
      * 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#DEFAULT_EXECUTING_TIMESLICE_MILLIS}, but
+     * {@link Constants#DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS}, but
      * will lead to underscheduling at least, rather than if we had taken the last execution time
      * to be the start of the execution.
      *
@@ -2213,11 +2270,24 @@
         return isComponentUsable(job);
     }
 
+    /** Returns the minimum amount of time we should let this job run before timing out. */
+    public long getMinJobExecutionGuaranteeMs(JobStatus job) {
+        synchronized (mLock) {
+            if (job.shouldTreatAsExpeditedJob()) {
+                // Don't guarantee RESTRICTED jobs more than 5 minutes.
+                return job.getEffectiveStandbyBucket() != RESTRICTED_INDEX
+                        ? mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS
+                        : Math.min(mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, 5 * MINUTE_IN_MILLIS);
+            } else {
+                return mConstants.RUNTIME_MIN_GUARANTEE_MS;
+            }
+        }
+    }
+
     /** Returns the maximum amount of time this job could run for. */
     public long getMaxJobExecutionTimeMs(JobStatus job) {
         synchronized (mLock) {
-            return Math.min(mQuotaController.getMaxJobExecutionTimeMsLocked(job),
-                    JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS);
+            return mQuotaController.getMaxJobExecutionTimeMsLocked(job);
         }
     }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 0aca246..be91947 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -17,9 +17,9 @@
 package com.android.server.job;
 
 import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
-import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.job.IJobCallback;
 import android.app.job.IJobService;
@@ -76,14 +76,6 @@
     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 DEFAULT_EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000;  // 10mins.
-    /**
-     * Amount of time a RESTRICTED expedited job is allowed to execute for before being considered
-     * timed-out.
-     */
-    public static final long DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS =
-            DEFAULT_EXECUTING_TIMESLICE_MILLIS / 2;
     /** 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. */
@@ -110,6 +102,7 @@
     /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */
     private final JobCompletedListener mCompletedListener;
     private final JobConcurrencyManager mJobConcurrencyManager;
+    private final JobSchedulerService mService;
     /** Used for service binding, etc. */
     private final Context mContext;
     private final Object mLock;
@@ -149,6 +142,13 @@
     private long mExecutionStartTimeElapsed;
     /** Track when job will timeout. */
     private long mTimeoutElapsed;
+    /**
+     * The minimum amount of time the context will allow the job to run before checking whether to
+     * stop it or not.
+     */
+    private long mMinExecutionGuaranteeMillis;
+    /** The absolute maximum amount of time the job can run */
+    private long mMaxExecutionTimeMillis;
 
     // Debugging: reason this job was last stopped.
     public String mStoppedReason;
@@ -190,6 +190,7 @@
             IBatteryStats batteryStats, JobPackageTracker tracker, Looper looper) {
         mContext = service.getContext();
         mLock = service.getLock();
+        mService = service;
         mBatteryStats = batteryStats;
         mJobPackageTracker = tracker;
         mCallbackHandler = new JobServiceHandler(looper);
@@ -239,6 +240,9 @@
                     isDeadlineExpired, job.shouldTreatAsExpeditedJob(),
                     triggeredUris, triggeredAuthorities, job.network);
             mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis();
+            mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job);
+            mMaxExecutionTimeMillis =
+                    Math.max(mService.getMaxJobExecutionTimeMs(job), mMinExecutionGuaranteeMillis);
 
             final long whenDeferred = job.getWhenStandbyDeferred();
             if (whenDeferred > 0) {
@@ -352,8 +356,8 @@
     }
 
     @GuardedBy("mLock")
-    void preemptExecutingJobLocked() {
-        doCancelLocked(JobParameters.REASON_PREEMPT, "cancelled due to preemption");
+    void preemptExecutingJobLocked(@NonNull String reason) {
+        doCancelLocked(JobParameters.REASON_PREEMPT, reason);
     }
 
     int getPreferredUid() {
@@ -372,6 +376,11 @@
         return mTimeoutElapsed;
     }
 
+    boolean isWithinExecutionGuaranteeTime() {
+        return mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis
+                < sElapsedRealtimeClock.millis();
+    }
+
     @GuardedBy("mLock")
     boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId,
             String reason) {
@@ -607,7 +616,7 @@
     }
 
     @GuardedBy("mLock")
-    void doCancelLocked(int arg1, String debugReason) {
+    private void doCancelLocked(int stopReasonCode, String debugReason) {
         if (mVerb == VERB_FINISHED) {
             if (DEBUG) {
                 Slog.d(TAG,
@@ -615,8 +624,8 @@
             }
             return;
         }
-        mParams.setStopReason(arg1, debugReason);
-        if (arg1 == JobParameters.REASON_PREEMPT) {
+        mParams.setStopReason(stopReasonCode, debugReason);
+        if (stopReasonCode == JobParameters.REASON_PREEMPT) {
             mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
                     NO_PREFERRED_UID;
         }
@@ -767,11 +776,30 @@
                 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");
+                final long latestStopTimeElapsed =
+                        mExecutionStartTimeElapsed + mMaxExecutionTimeMillis;
+                final long nowElapsed = sElapsedRealtimeClock.millis();
+                if (nowElapsed >= latestStopTimeElapsed) {
+                    // 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");
+                } else {
+                    // We've given the app the minimum execution time. See if we should stop it or
+                    // let it continue running
+                    final String reason = mJobConcurrencyManager.shouldStopRunningJobLocked(this);
+                    if (reason != null) {
+                        Slog.i(TAG, "Stopping client after min execution time: "
+                                + getRunningJobNameLocked() + " because " + reason);
+                        mParams.setStopReason(JobParameters.REASON_TIMEOUT, reason);
+                        sendStopMessageLocked(reason);
+                    } else {
+                        Slog.i(TAG, "Letting " + getRunningJobNameLocked()
+                                + " continue to run past min execution time");
+                        scheduleOpTimeOutLocked();
+                    }
+                }
                 break;
             default:
                 Slog.e(TAG, "Handling timeout for an invalid job state: "
@@ -878,10 +906,16 @@
         final long timeoutMillis;
         switch (mVerb) {
             case VERB_EXECUTING:
-                timeoutMillis = mRunningJob.shouldTreatAsExpeditedJob()
-                        && mRunningJob.getStandbyBucket() == RESTRICTED_INDEX
-                        ? DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS
-                        : DEFAULT_EXECUTING_TIMESLICE_MILLIS;
+                final long earliestStopTimeElapsed =
+                        mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis;
+                final long latestStopTimeElapsed =
+                        mExecutionStartTimeElapsed + mMaxExecutionTimeMillis;
+                final long nowElapsed = sElapsedRealtimeClock.millis();
+                if (nowElapsed < earliestStopTimeElapsed) {
+                    timeoutMillis = earliestStopTimeElapsed - nowElapsed;
+                } else {
+                    timeoutMillis = latestStopTimeElapsed - nowElapsed;
+                }
                 break;
 
             case VERB_BINDING:
@@ -925,6 +959,13 @@
             pw.print(", timeout at: ");
             TimeUtils.formatDuration(mTimeoutElapsed - nowElapsed, pw);
             pw.println();
+            pw.print("Remaining execution limits: [");
+            TimeUtils.formatDuration(
+                    (mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis) - nowElapsed, pw);
+            pw.print(", ");
+            TimeUtils.formatDuration(
+                    (mExecutionStartTimeElapsed + mMaxExecutionTimeMillis) - nowElapsed, pw);
+            pw.println("]");
             pw.decreaseIndent();
         }
     }
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
index d249f2a..14484ff 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -325,6 +325,8 @@
      */
     private boolean isInsane(JobStatus jobStatus, Network network,
             NetworkCapabilities capabilities, Constants constants) {
+        // Use the maximum possible time since it gives us an upper bound, even though the job
+        // could end up stopping earlier.
         final long maxJobExecutionTimeMs = mService.getMaxJobExecutionTimeMs(jobStatus);
 
         final long downloadBytes = jobStatus.getEstimatedNetworkDownloadBytes();
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
index f2d10ac..2196b16 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -72,7 +72,6 @@
 import com.android.server.PowerAllowlistInternal;
 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 com.android.server.usage.AppStandbyInternal;
 import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
@@ -770,18 +769,38 @@
 
     /** 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()
-                || isTopStartedJobLocked(jobStatus)
-                || isUidInForeground(jobStatus.getSourceUid())) {
-            return JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS;
+        // Need to look at current proc state as well in the case where the job hasn't started yet.
+        final boolean isTop = mActivityManagerInternal
+                .getUidProcessState(jobStatus.getSourceUid()) <= ActivityManager.PROCESS_STATE_TOP;
+
+        if (!jobStatus.shouldTreatAsExpeditedJob()) {
+            // If quota is currently "free", then the job can run for the full amount of time.
+            if (mChargeTracker.isCharging()
+                    || isTop
+                    || isTopStartedJobLocked(jobStatus)
+                    || isUidInForeground(jobStatus.getSourceUid())) {
+                return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
+            }
+            return getTimeUntilQuotaConsumedLocked(
+                    jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
         }
-        if (jobStatus.shouldTreatAsExpeditedJob()) {
-            return jobStatus.getStandbyBucket() == RESTRICTED_INDEX
-                    ? JobServiceContext.DEFAULT_RESTRICTED_EXPEDITED_JOB_EXECUTING_TIMESLICE_MILLIS
-                    : JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS;
+
+        // Expedited job.
+        if (mChargeTracker.isCharging()) {
+            return mConstants.RUNTIME_FREE_QUOTA_MAX_LIMIT_MS;
         }
-        return getRemainingExecutionTimeLocked(jobStatus);
+        if (isTop || isTopStartedJobLocked(jobStatus)) {
+            return Math.max(mEJLimitsMs[ACTIVE_INDEX] / 2,
+                    getTimeUntilEJQuotaConsumedLocked(
+                            jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()));
+        }
+        if (isUidInForeground(jobStatus.getSourceUid())) {
+            return Math.max(mEJLimitsMs[WORKING_INDEX] / 2,
+                    getTimeUntilEJQuotaConsumedLocked(
+                            jobStatus.getSourceUserId(), jobStatus.getSourcePackageName()));
+        }
+        return getTimeUntilEJQuotaConsumedLocked(
+                jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
     }
 
     /** @return true if the job is within expedited job quota. */
@@ -3577,8 +3596,8 @@
                 mEJLimitsMs[RARE_INDEX] = newRareLimitMs;
                 mShouldReevaluateConstraints = true;
             }
-            // The limit must be in the range [0 minutes, rare limit].
-            long newRestrictedLimitMs = Math.max(0,
+            // The limit must be in the range [5 minutes, rare limit].
+            long newRestrictedLimitMs = Math.max(5 * MINUTE_IN_MILLIS,
                     Math.min(newRareLimitMs, EJ_LIMIT_RESTRICTED_MS));
             if (mEJLimitsMs[RESTRICTED_INDEX] != newRestrictedLimitMs) {
                 mEJLimitsMs[RESTRICTED_INDEX] = newRestrictedLimitMs;
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
index 8099eda..775276b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -63,7 +63,6 @@
 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.net.NetworkPolicyManagerInternal;
 
 import org.junit.Before;
@@ -144,8 +143,7 @@
                 .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
 
         final ConnectivityController controller = new ConnectivityController(mService);
-        when(mService.getMaxJobExecutionTimeMs(any()))
-                .thenReturn(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS);
+        when(mService.getMaxJobExecutionTimeMs(any())).thenReturn(10 * 60_000L);
 
         // Slow network is too slow
         assertFalse(controller.isSatisfied(createJobStatus(job), net,
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index c4c9173..b72121f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -82,8 +82,6 @@
 import com.android.server.LocalServices;
 import com.android.server.PowerAllowlistInternal;
 import com.android.server.job.JobSchedulerService;
-import com.android.server.job.JobSchedulerService.Constants;
-import com.android.server.job.JobServiceContext;
 import com.android.server.job.JobStore;
 import com.android.server.job.controllers.QuotaController.ExecutionStats;
 import com.android.server.job.controllers.QuotaController.QcConstants;
@@ -123,6 +121,7 @@
     private BroadcastReceiver mChargingReceiver;
     private QuotaController mQuotaController;
     private QuotaController.QcConstants mQcConstants;
+    private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants();
     private int mSourceUid;
     private PowerAllowlistInternal.TempAllowlistChangeListener mTempAllowlistListener;
     private IUidObserver mUidObserver;
@@ -158,7 +157,7 @@
         // Called in StateController constructor.
         when(mJobSchedulerService.getTestableContext()).thenReturn(mContext);
         when(mJobSchedulerService.getLock()).thenReturn(mJobSchedulerService);
-        when(mJobSchedulerService.getConstants()).thenReturn(mock(Constants.class));
+        when(mJobSchedulerService.getConstants()).thenReturn(mConstants);
         // Called in QuotaController constructor.
         IActivityManager activityManager = ActivityManager.getService();
         spyOn(activityManager);
@@ -1282,23 +1281,23 @@
     }
 
     @Test
-    public void testGetMaxJobExecutionTimeLocked() {
+    public void testGetMaxJobExecutionTimeLocked_Regular() {
         mQuotaController.saveTimingSession(0, SOURCE_PACKAGE,
                 createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS),
                         3 * MINUTE_IN_MILLIS, 5), false);
         JobStatus job = createJobStatus("testGetMaxJobExecutionTimeLocked", 0);
-        job.setStandbyBucket(RARE_INDEX);
+        setStandbyBucket(RARE_INDEX, job);
 
         setCharging();
         synchronized (mQuotaController.mLock) {
-            assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS,
+            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
         }
 
         setDischarging();
         setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
         synchronized (mQuotaController.mLock) {
-            assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS,
+            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
         }
 
@@ -1310,7 +1309,7 @@
         }
         setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
         synchronized (mQuotaController.mLock) {
-            assertEquals(JobServiceContext.DEFAULT_EXECUTING_TIMESLICE_MILLIS,
+            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
                     mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
             mQuotaController.maybeStopTrackingJobLocked(job, null, false);
         }
@@ -1322,6 +1321,81 @@
         }
     }
 
+    @Test
+    public void testGetMaxJobExecutionTimeLocked_EJ() {
+        final long timeUsedMs = 3 * MINUTE_IN_MILLIS;
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS),
+                        timeUsedMs, 5), true);
+        JobStatus job = createExpeditedJobStatus("testGetMaxJobExecutionTimeLocked_EJ", 0);
+        setStandbyBucket(RARE_INDEX, job);
+        mQuotaController.maybeStartTrackingJobLocked(job, null);
+
+        setCharging();
+        synchronized (mQuotaController.mLock) {
+            assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+        }
+
+        setDischarging();
+        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+        }
+
+        // Top-started job
+        setProcessState(ActivityManager.PROCESS_STATE_TOP);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.prepareForExecutionLocked(job);
+        }
+        setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+        }
+
+        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(mQcConstants.EJ_LIMIT_RARE_MS - timeUsedMs,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+        }
+
+        // Test used quota rolling out of window.
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.clearAppStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE);
+        }
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(sElapsedRealtimeClock.millis() - mQcConstants.EJ_WINDOW_SIZE_MS,
+                        timeUsedMs, 5), true);
+
+        setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+        }
+
+        // Top-started job
+        setProcessState(ActivityManager.PROCESS_STATE_TOP);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job, null);
+            mQuotaController.prepareForExecutionLocked(job);
+        }
+        setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+        }
+
+        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+        synchronized (mQuotaController.mLock) {
+            assertEquals(mQcConstants.EJ_LIMIT_RARE_MS,
+                    mQuotaController.getMaxJobExecutionTimeMsLocked(job));
+        }
+    }
+
     /**
      * Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket
      * window.
@@ -2508,7 +2582,7 @@
         assertEquals(15 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[WORKING_INDEX]);
         assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[FREQUENT_INDEX]);
         assertEquals(10 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[RARE_INDEX]);
-        assertEquals(0, mQuotaController.getEJLimitsMs()[RESTRICTED_INDEX]);
+        assertEquals(5 * MINUTE_IN_MILLIS, mQuotaController.getEJLimitsMs()[RESTRICTED_INDEX]);
         assertEquals(0, mQuotaController.getEjLimitSpecialAdditionMs());
         assertEquals(HOUR_IN_MILLIS, mQuotaController.getEJLimitWindowSizeMs());
         assertEquals(1, mQuotaController.getEJTopAppTimeChunkSizeMs());