OSDN Git Service

Add new "storage not low" job scheduler constraint.
authorDianne Hackborn <hackbod@google.com>
Sat, 18 Mar 2017 00:50:55 +0000 (17:50 -0700)
committerDianne Hackborn <hackbod@google.com>
Mon, 20 Mar 2017 20:41:08 +0000 (13:41 -0700)
This allows you to say that your job should run only when device
storage is not low.

Adds new command line interface to DeviceStorageMonitor to help
with driving the tests (modelled after BatteryService).

Test: new StorageConstraintTest suite.
Change-Id: I96bfb761cd8257b6f68dde43ce9cfb1a3b9d0acb

13 files changed:
api/current.txt
api/system-current.txt
api/test-current.txt
core/java/android/app/job/JobInfo.java
core/java/android/app/job/JobService.java
services/core/java/com/android/server/BatteryService.java
services/core/java/com/android/server/job/JobSchedulerService.java
services/core/java/com/android/server/job/JobSchedulerShellCommand.java
services/core/java/com/android/server/job/controllers/BatteryController.java
services/core/java/com/android/server/job/controllers/ConnectivityController.java
services/core/java/com/android/server/job/controllers/JobStatus.java
services/core/java/com/android/server/job/controllers/StorageController.java [new file with mode: 0644]
services/core/java/com/android/server/storage/DeviceStorageMonitorService.java

index aab29be..50b0c19 100644 (file)
@@ -6748,6 +6748,7 @@ package android.app.job {
     method public boolean isRequireBatteryNotLow();
     method public boolean isRequireCharging();
     method public boolean isRequireDeviceIdle();
+    method public boolean isRequireStorageNotLow();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final int BACKOFF_POLICY_EXPONENTIAL = 1; // 0x1
     field public static final int BACKOFF_POLICY_LINEAR = 0; // 0x0
@@ -6775,6 +6776,7 @@ package android.app.job {
     method public android.app.job.JobInfo.Builder setRequiresBatteryNotLow(boolean);
     method public android.app.job.JobInfo.Builder setRequiresCharging(boolean);
     method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
+    method public android.app.job.JobInfo.Builder setRequiresStorageNotLow(boolean);
     method public android.app.job.JobInfo.Builder setTransientExtras(android.os.Bundle);
     method public android.app.job.JobInfo.Builder setTriggerContentMaxDelay(long);
     method public android.app.job.JobInfo.Builder setTriggerContentUpdateDelay(long);
index 5e6717c..71dc3bf 100644 (file)
@@ -7184,6 +7184,7 @@ package android.app.job {
     method public boolean isRequireBatteryNotLow();
     method public boolean isRequireCharging();
     method public boolean isRequireDeviceIdle();
+    method public boolean isRequireStorageNotLow();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final int BACKOFF_POLICY_EXPONENTIAL = 1; // 0x1
     field public static final int BACKOFF_POLICY_LINEAR = 0; // 0x0
@@ -7211,6 +7212,7 @@ package android.app.job {
     method public android.app.job.JobInfo.Builder setRequiresBatteryNotLow(boolean);
     method public android.app.job.JobInfo.Builder setRequiresCharging(boolean);
     method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
+    method public android.app.job.JobInfo.Builder setRequiresStorageNotLow(boolean);
     method public android.app.job.JobInfo.Builder setTransientExtras(android.os.Bundle);
     method public android.app.job.JobInfo.Builder setTriggerContentMaxDelay(long);
     method public android.app.job.JobInfo.Builder setTriggerContentUpdateDelay(long);
index f91bbb9..2e04bfe 100644 (file)
@@ -6775,6 +6775,7 @@ package android.app.job {
     method public boolean isRequireBatteryNotLow();
     method public boolean isRequireCharging();
     method public boolean isRequireDeviceIdle();
+    method public boolean isRequireStorageNotLow();
     method public void writeToParcel(android.os.Parcel, int);
     field public static final int BACKOFF_POLICY_EXPONENTIAL = 1; // 0x1
     field public static final int BACKOFF_POLICY_LINEAR = 0; // 0x0
@@ -6802,6 +6803,7 @@ package android.app.job {
     method public android.app.job.JobInfo.Builder setRequiresBatteryNotLow(boolean);
     method public android.app.job.JobInfo.Builder setRequiresCharging(boolean);
     method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
+    method public android.app.job.JobInfo.Builder setRequiresStorageNotLow(boolean);
     method public android.app.job.JobInfo.Builder setTransientExtras(android.os.Bundle);
     method public android.app.job.JobInfo.Builder setTriggerContentMaxDelay(long);
     method public android.app.job.JobInfo.Builder setTriggerContentUpdateDelay(long);
index 6652eee..78e4c0d 100644 (file)
@@ -189,6 +189,11 @@ public class JobInfo implements Parcelable {
      */
     public static final int CONSTRAINT_FLAG_DEVICE_IDLE = 1 << 2;
 
+    /**
+     * @hide
+     */
+    public static final int CONSTRAINT_FLAG_STORAGE_NOT_LOW = 1 << 3;
+
     private final int jobId;
     private final PersistableBundle extras;
     private final Bundle transientExtras;
@@ -273,6 +278,13 @@ public class JobInfo implements Parcelable {
     }
 
     /**
+     * Whether this job needs the device's storage to not be low.
+     */
+    public boolean isRequireStorageNotLow() {
+        return (constraintFlags & CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0;
+    }
+
+    /**
      * @hide
      */
     public int getConstraintFlags() {
@@ -710,15 +722,33 @@ public class JobInfo implements Parcelable {
         }
 
         /**
+         * Specify that to run this job, the device's available storage must not be low.
+         * This defaults to false.  If true, the job will only run when the device is not
+         * in a low storage state, which is generally the point where the user is given a
+         * "low storage" warning.
+         * @param storageNotLow Whether or not the device's available storage must not be low.
+         */
+        public Builder setRequiresStorageNotLow(boolean storageNotLow) {
+            mConstraintFlags = (mConstraintFlags&~CONSTRAINT_FLAG_STORAGE_NOT_LOW)
+                    | (storageNotLow ? CONSTRAINT_FLAG_STORAGE_NOT_LOW : 0);
+            return this;
+        }
+
+        /**
          * Add a new content: URI that will be monitored with a
          * {@link android.database.ContentObserver}, and will cause the job to execute if changed.
          * If you have any trigger content URIs associated with a job, it will not execute until
          * there has been a change report for one or more of them.
+         *
          * <p>Note that trigger URIs can not be used in combination with
          * {@link #setPeriodic(long)} or {@link #setPersisted(boolean)}.  To continually monitor
          * for content changes, you need to schedule a new JobInfo observing the same URIs
-         * before you finish execution of the JobService handling the most recent changes.</p>
-         * <p>Because because setting this property is not compatible with periodic or
+         * before you finish execution of the JobService handling the most recent changes.
+         * Following this pattern will ensure you do not lost any content changes: while your
+         * job is running, the system will continue monitoring for content changes, and propagate
+         * any it sees over to the next job you schedule.</p>
+         *
+         * <p>Because setting this property is not compatible with periodic or
          * persisted jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when
          * {@link android.app.job.JobInfo.Builder#build()} is called.</p>
          *
index 77307b7..f4019ce 100644 (file)
@@ -250,7 +250,7 @@ public abstract class JobService extends Service {
     public abstract boolean onStopJob(JobParameters params);
 
     /**
-     * Callback to inform the JobManager you've finished executing. This can be called from any
+     * Call this to inform the JobManager you've finished executing. This can be called from any
      * thread, as it will ultimately be run on your application's main thread. When the system
      * receives this message it will release the wakelock being held.
      * <p>
index c9dd116..98242f9 100644 (file)
@@ -489,7 +489,8 @@ public final class BatteryService extends SystemService {
                         mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL);
                     }
                 });
-            } else if (mSentLowBatteryBroadcast && mLastBatteryLevel >= mLowBatteryCloseWarningLevel) {
+            } else if (mSentLowBatteryBroadcast &&
+                    mBatteryProps.batteryLevel >= mLowBatteryCloseWarningLevel) {
                 mSentLowBatteryBroadcast = false;
                 final Intent statusIntent = new Intent(Intent.ACTION_BATTERY_OKAY);
                 statusIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
index 940f621..6e09ee2 100644 (file)
@@ -83,6 +83,7 @@ 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.StateController;
+import com.android.server.job.controllers.StorageController;
 import com.android.server.job.controllers.TimeController;
 
 import libcore.util.EmptyArray;
@@ -133,6 +134,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
     List<StateController> mControllers;
     /** Need direct access to this for testing. */
     BatteryController mBatteryController;
+    /** Need direct access to this for testing. */
+    StorageController mStorageController;
     /**
      * Queue of pending jobs. The JobServiceContext class will receive jobs from this list
      * when ready to execute them.
@@ -197,6 +200,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
         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";
@@ -211,6 +215,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
         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;
@@ -238,6 +243,11 @@ public final class JobSchedulerService extends com.android.server.SystemService
          */
         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.
          */
@@ -323,6 +333,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
                         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,
@@ -370,6 +382,9 @@ public final class JobSchedulerService extends com.android.server.SystemService
             pw.print("    "); pw.print(KEY_MIN_BATTERY_NOT_LOW_COUNT); pw.print("=");
             pw.print(MIN_BATTERY_NOT_LOW_COUNT); pw.println();
 
+            pw.print("    "); pw.print(KEY_MIN_STORAGE_NOT_LOW_COUNT); pw.print("=");
+            pw.print(MIN_STORAGE_NOT_LOW_COUNT); pw.println();
+
             pw.print("    "); pw.print(KEY_MIN_CONNECTIVITY_COUNT); pw.print("=");
             pw.print(MIN_CONNECTIVITY_COUNT); pw.println();
 
@@ -802,6 +817,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
         mControllers.add(IdleController.get(this));
         mBatteryController = BatteryController.get(this);
         mControllers.add(mBatteryController);
+        mStorageController = StorageController.get(this);
+        mControllers.add(mStorageController);
         mControllers.add(AppIdleController.get(this));
         mControllers.add(ContentObserverController.get(this));
         mControllers.add(DeviceIdleJobsController.get(this));
@@ -1202,6 +1219,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
         class MaybeReadyJobQueueFunctor implements JobStatusFunctor {
             int chargingCount;
             int batteryNotLowCount;
+            int storageNotLowCount;
             int idleCount;
             int backoffCount;
             int connectivityCount;
@@ -1242,6 +1260,9 @@ public final class JobSchedulerService extends com.android.server.SystemService
                     if (job.hasBatteryNotLowConstraint()) {
                         batteryNotLowCount++;
                     }
+                    if (job.hasStorageNotLowConstraint()) {
+                        storageNotLowCount++;
+                    }
                     if (job.hasContentTriggerConstraint()) {
                         contentCount++;
                     }
@@ -1261,6 +1282,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
                         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 ||
                         (runnableJobs != null
                                 && runnableJobs.size() >= mConstants.MIN_READY_JOBS_COUNT)) {
@@ -1285,6 +1307,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
                 backoffCount = 0;
                 connectivityCount = 0;
                 batteryNotLowCount = 0;
+                storageNotLowCount = 0;
                 contentCount = 0;
                 runnableJobs = null;
             }
@@ -1828,6 +1851,19 @@ public final class JobSchedulerService extends com.android.server.SystemService
         }
     }
 
+    int getStorageSeq() {
+        synchronized (mLock) {
+            return mStorageController != null ? mStorageController.getTracker().getSeq() : -1;
+        }
+    }
+
+    boolean getStorageNotLow() {
+        synchronized (mLock) {
+            return mStorageController != null
+                    ? mStorageController.getTracker().isStorageNotLow() : false;
+        }
+    }
+
     private String printContextIdToJobMap(JobStatus[] map, String initial) {
         StringBuilder s = new StringBuilder(initial + ": ");
         for (int i=0; i<map.length; i++) {
index ec23407..848704e 100644 (file)
@@ -54,6 +54,10 @@ public class JobSchedulerShellCommand extends ShellCommand {
                     return runGetBatteryCharging(pw);
                 case "get-battery-not-low":
                     return runGetBatteryNotLow(pw);
+                case "get-storage-seq":
+                    return runGetStorageSeq(pw);
+                case "get-storage-not-low":
+                    return runGetStorageNotLow(pw);
                 default:
                     return handleDefaultCommands(cmd);
             }
@@ -181,6 +185,18 @@ public class JobSchedulerShellCommand extends ShellCommand {
         return 0;
     }
 
+    private int runGetStorageSeq(PrintWriter pw) {
+        int seq = mInternal.getStorageSeq();
+        pw.println(seq);
+        return 0;
+    }
+
+    private int runGetStorageNotLow(PrintWriter pw) {
+        boolean val = mInternal.getStorageNotLow();
+        pw.println(val);
+        return 0;
+    }
+
     @Override
     public void onHelp() {
         final PrintWriter pw = getOutPrintWriter();
@@ -204,6 +220,10 @@ public class JobSchedulerShellCommand extends ShellCommand {
         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();
     }
 
index 05527be..91a962d 100644 (file)
@@ -120,7 +120,7 @@ public class BatteryController extends StateController {
             mStateChangedListener.onControllerStateChanged();
         }
         // Also tell the scheduler that any ready jobs should be flushed.
-        if (stablePower) {
+        if (stablePower || batteryNotLow) {
             mStateChangedListener.onRunJobNow(null);
         }
     }
index b65330a..94ca24c 100644 (file)
@@ -138,7 +138,7 @@ public class ConnectivityController extends StateController implements
      * We know the network has just come up. We want to run any jobs that are ready.
      */
     @Override
-    public synchronized void onNetworkActive() {
+    public void onNetworkActive() {
         synchronized (mLock) {
             for (int i = 0; i < mTrackedJobs.size(); i++) {
                 final JobStatus js = mTrackedJobs.get(i);
index 9a55fed..ebb53a1 100644 (file)
@@ -49,6 +49,7 @@ public final class JobStatus {
     static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING;
     static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE;
     static final int CONSTRAINT_BATTERY_NOT_LOW = JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW;
+    static final int CONSTRAINT_STORAGE_NOT_LOW = JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW;
     static final int CONSTRAINT_TIMING_DELAY = 1<<31;
     static final int CONSTRAINT_DEADLINE = 1<<30;
     static final int CONSTRAINT_UNMETERED = 1<<29;
@@ -334,6 +335,10 @@ public final class JobStatus {
         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;
     }
@@ -386,6 +391,10 @@ public final class JobStatus {
         return setConstraintSatisfied(CONSTRAINT_BATTERY_NOT_LOW, state);
     }
 
+    boolean setStorageNotLowConstraintSatisfied(boolean state) {
+        return setConstraintSatisfied(CONSTRAINT_STORAGE_NOT_LOW, state);
+    }
+
     boolean setTimingDelayConstraintSatisfied(boolean state) {
         return setConstraintSatisfied(CONSTRAINT_TIMING_DELAY, state);
     }
@@ -460,13 +469,14 @@ public final class JobStatus {
     }
 
     static final int CONSTRAINTS_OF_INTEREST =
-            CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_TIMING_DELAY |
+            CONSTRAINT_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW |
+            CONSTRAINT_TIMING_DELAY |
             CONSTRAINT_CONNECTIVITY | CONSTRAINT_UNMETERED | CONSTRAINT_NOT_ROAMING |
             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_CHARGING | CONSTRAINT_BATTERY_NOT_LOW | CONSTRAINT_STORAGE_NOT_LOW
                     | CONSTRAINT_TIMING_DELAY | CONSTRAINT_IDLE;
 
     /**
@@ -562,6 +572,9 @@ public final class JobStatus {
         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");
         }
diff --git a/services/core/java/com/android/server/job/controllers/StorageController.java b/services/core/java/com/android/server/job/controllers/StorageController.java
new file mode 100644 (file)
index 0000000..60ae5a7
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * 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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.job.JobSchedulerService;
+import com.android.server.job.StateChangedListener;
+import com.android.server.storage.DeviceStorageMonitorService;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Simple controller that tracks the status of the device's storage.
+ */
+public class StorageController extends StateController {
+    private static final String TAG = "JobScheduler.Stor";
+
+    private static final Object sCreationLock = new Object();
+    private static volatile StorageController sController;
+
+    private List<JobStatus> mTrackedTasks = new ArrayList<JobStatus>();
+    private StorageTracker mStorageTracker;
+
+    public static StorageController get(JobSchedulerService taskManagerService) {
+        synchronized (sCreationLock) {
+            if (sController == null) {
+                sController = new StorageController(taskManagerService,
+                        taskManagerService.getContext(), taskManagerService.getLock());
+            }
+        }
+        return sController;
+    }
+
+    @VisibleForTesting
+    public StorageTracker getTracker() {
+        return mStorageTracker;
+    }
+
+    @VisibleForTesting
+    public static StorageController getForTesting(StateChangedListener stateChangedListener,
+            Context context) {
+        return new StorageController(stateChangedListener, context, new Object());
+    }
+
+    private StorageController(StateChangedListener stateChangedListener, Context context,
+            Object lock) {
+        super(stateChangedListener, context, lock);
+        mStorageTracker = new StorageTracker();
+        mStorageTracker.startTracking();
+    }
+
+    @Override
+    public void maybeStartTrackingJobLocked(JobStatus taskStatus, JobStatus lastJob) {
+        if (taskStatus.hasStorageNotLowConstraint()) {
+            mTrackedTasks.add(taskStatus);
+            taskStatus.setStorageNotLowConstraintSatisfied(mStorageTracker.isStorageNotLow());
+        }
+    }
+
+    @Override
+    public void maybeStopTrackingJobLocked(JobStatus taskStatus, JobStatus incomingJob, boolean forUpdate) {
+        if (taskStatus.hasPowerConstraint()) {
+            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.get(i);
+                boolean previous = ts.setStorageNotLowConstraintSatisfied(storageNotLow);
+                if (previous != storageNotLow) {
+                    reportChange = true;
+                }
+            }
+        }
+        // Let the scheduler know that state has changed. This may or may not result in an
+        // execution.
+        if (reportChange) {
+            mStateChangedListener.onControllerStateChanged();
+        }
+        // Also tell the scheduler that any ready jobs should be flushed.
+        if (storageNotLow) {
+            mStateChangedListener.onRunJobNow(null);
+        }
+    }
+
+    public class StorageTracker extends BroadcastReceiver {
+        /**
+         * Track whether storage is low.
+         */
+        private boolean mStorageLow;
+        /** Sequence number of last broadcast. */
+        private int mLastBatterySeq = -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 mLastBatterySeq;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            onReceiveInternal(intent);
+        }
+
+        @VisibleForTesting
+        public void onReceiveInternal(Intent intent) {
+            final String action = intent.getAction();
+            mLastBatterySeq = intent.getIntExtra(DeviceStorageMonitorService.EXTRA_SEQUENCE,
+                    mLastBatterySeq);
+            if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Available storage too low to do work. @ "
+                            + SystemClock.elapsedRealtime());
+                }
+                mStorageLow = true;
+            } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(action)) {
+                if (DEBUG) {
+                    Slog.d(TAG, "Available stoage high enough to do work. @ "
+                            + SystemClock.elapsedRealtime());
+                }
+                mStorageLow = false;
+                maybeReportNewStorageState();
+            }
+        }
+    }
+
+    @Override
+    public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
+        pw.print("Storage: not low = ");
+        pw.print(mStorageTracker.isStorageNotLow());
+        pw.print(", seq=");
+        pw.println(mStorageTracker.getSeq());
+        pw.print("Tracking ");
+        pw.print(mTrackedTasks.size());
+        pw.println(":");
+        for (int i = 0; i < mTrackedTasks.size(); i++) {
+            final JobStatus js = mTrackedTasks.get(i);
+            if (!js.shouldDump(filterUid)) {
+                continue;
+            }
+            pw.print("  #");
+            js.printUniqueId(pw);
+            pw.print(" from ");
+            UserHandle.formatUid(pw, js.getSourceUid());
+            pw.println();
+        }
+    }
+}
index 12836db..0639eee 100644 (file)
@@ -34,10 +34,12 @@ import android.os.Binder;
 import android.os.Environment;
 import android.os.FileObserver;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.ResultReceiver;
 import android.os.ServiceManager;
+import android.os.ShellCallback;
+import android.os.ShellCommand;
 import android.os.StatFs;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -52,6 +54,7 @@ import android.util.TimeUtils;
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import dalvik.system.VMRuntime;
 
@@ -76,12 +79,19 @@ import dalvik.system.VMRuntime;
 public class DeviceStorageMonitorService extends SystemService {
     static final String TAG = "DeviceStorageMonitorService";
 
+    /**
+     * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}:
+     * Current int sequence number of the update.
+     */
+    public static final String EXTRA_SEQUENCE = "seq";
+
     // TODO: extend to watch and manage caches on all private volumes
 
     static final boolean DEBUG = false;
     static final boolean localLOGV = false;
 
     static final int DEVICE_MEMORY_WHAT = 1;
+    static final int FORCE_MEMORY_WHAT = 2;
     private static final int MONITOR_INTERVAL = 1; //in minutes
     private static final int LOW_MEMORY_NOTIFICATION_ID = 1;
 
@@ -112,6 +122,8 @@ public class DeviceStorageMonitorService extends SystemService {
     private static final File CACHE_PATH = Environment.getDownloadCacheDirectory();
 
     private long mThreadStartTime = -1;
+    boolean mUpdatesStopped;
+    AtomicInteger mSeq = new AtomicInteger(1);
     boolean mClearSucceeded = false;
     boolean mClearingCache;
     private final Intent mStorageLowIntent;
@@ -152,11 +164,17 @@ public class DeviceStorageMonitorService extends SystemService {
         @Override
         public void handleMessage(Message msg) {
             //don't handle an invalid message
-            if (msg.what != DEVICE_MEMORY_WHAT) {
-                Slog.e(TAG, "Will not process invalid message");
-                return;
+            switch (msg.what) {
+                case DEVICE_MEMORY_WHAT:
+                    checkMemory(msg.arg1 == _TRUE);
+                    return;
+                case FORCE_MEMORY_WHAT:
+                    forceMemory(msg.arg1, msg.arg2);
+                    return;
+                default:
+                    Slog.w(TAG, "Will not process invalid message");
+                    return;
             }
-            checkMemory(msg.arg1 == _TRUE);
         }
     };
 
@@ -239,12 +257,36 @@ public class DeviceStorageMonitorService extends SystemService {
         }
     }
 
+    void forceMemory(int opts, int seq) {
+        if ((opts&OPTION_UPDATES_STOPPED) == 0) {
+            if (mUpdatesStopped) {
+                mUpdatesStopped = false;
+                checkMemory(true);
+            }
+        } else {
+            mUpdatesStopped = true;
+            final boolean forceLow = (opts&OPTION_STORAGE_LOW) != 0;
+            if (mLowMemFlag != forceLow || (opts&OPTION_FORCE_UPDATE) != 0) {
+                mLowMemFlag = forceLow;
+                if (forceLow) {
+                    sendNotification(seq);
+                } else {
+                    cancelNotification(seq);
+                }
+            }
+        }
+    }
+
     void checkMemory(boolean checkCache) {
+        if (mUpdatesStopped) {
+            return;
+        }
+
         //if the thread that was started to clear cache is still running do nothing till its
         //finished clearing cache. Ideally this flag could be modified by clearCache
         // and should be accessed via a lock but even if it does this test will fail now and
         //hopefully the next time this flag will be set to the correct value.
-        if(mClearingCache) {
+        if (mClearingCache) {
             if(localLOGV) Slog.i(TAG, "Thread already running just skip");
             //make sure the thread is not hung for too long
             long diffTime = System.currentTimeMillis() - mThreadStartTime;
@@ -284,7 +326,7 @@ public class DeviceStorageMonitorService extends SystemService {
                         // We tried to clear the cache, but that didn't get us
                         // below the low storage limit.  Tell the user.
                         Slog.i(TAG, "Running low on memory. Sending notification");
-                        sendNotification();
+                        sendNotification(0);
                         mLowMemFlag = true;
                     } else {
                         if (localLOGV) Slog.v(TAG, "Running low on memory " +
@@ -295,13 +337,13 @@ public class DeviceStorageMonitorService extends SystemService {
                 mFreeMemAfterLastCacheClear = mFreeMem;
                 if (mLowMemFlag) {
                     Slog.i(TAG, "Memory available. Cancelling notification");
-                    cancelNotification();
+                    cancelNotification(0);
                     mLowMemFlag = false;
                 }
             }
             if (!mLowMemFlag && !mIsBootImageOnDisk && mFreeMem < BOOT_IMAGE_STORAGE_REQUIREMENT) {
                 Slog.i(TAG, "No boot image on disk due to lack of space. Sending notification");
-                sendNotification();
+                sendNotification(0);
                 mLowMemFlag = true;
             }
             if (mFreeMem < mMemFullThreshold) {
@@ -419,7 +461,7 @@ public class DeviceStorageMonitorService extends SystemService {
         }
     };
 
-    private final IBinder mRemoteService = new Binder() {
+    private final Binder mRemoteService = new Binder() {
         @Override
         protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
             if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
@@ -431,44 +473,157 @@ public class DeviceStorageMonitorService extends SystemService {
                 return;
             }
 
-            dumpImpl(pw);
+            dumpImpl(fd, pw, args);
+        }
+
+        @Override
+        public void onShellCommand(FileDescriptor in, FileDescriptor out,
+                FileDescriptor err, String[] args, ShellCallback callback,
+                ResultReceiver resultReceiver) {
+            (new Shell()).exec(this, in, out, err, args, callback, resultReceiver);
         }
     };
 
-    void dumpImpl(PrintWriter pw) {
-        final Context context = getContext();
+    class Shell extends ShellCommand {
+        @Override
+        public int onCommand(String cmd) {
+            return onShellCommand(this, cmd);
+        }
+
+        @Override
+        public void onHelp() {
+            PrintWriter pw = getOutPrintWriter();
+            dumpHelp(pw);
+        }
+    }
+
+    static final int OPTION_FORCE_UPDATE = 1<<0;
+    static final int OPTION_UPDATES_STOPPED = 1<<1;
+    static final int OPTION_STORAGE_LOW = 1<<2;
 
-        pw.println("Current DeviceStorageMonitor state:");
+    int parseOptions(Shell shell) {
+        String opt;
+        int opts = 0;
+        while ((opt = shell.getNextOption()) != null) {
+            if ("-f".equals(opt)) {
+                opts |= OPTION_FORCE_UPDATE;
+            }
+        }
+        return opts;
+    }
 
-        pw.print("  mFreeMem="); pw.print(Formatter.formatFileSize(context, mFreeMem));
-        pw.print(" mTotalMemory=");
-        pw.println(Formatter.formatFileSize(context, mTotalMemory));
+    int onShellCommand(Shell shell, String cmd) {
+        if (cmd == null) {
+            return shell.handleDefaultCommands(cmd);
+        }
+        PrintWriter pw = shell.getOutPrintWriter();
+        switch (cmd) {
+            case "force-low": {
+                int opts = parseOptions(shell);
+                getContext().enforceCallingOrSelfPermission(
+                        android.Manifest.permission.DEVICE_POWER, null);
+                int seq = mSeq.incrementAndGet();
+                mHandler.sendMessage(mHandler.obtainMessage(FORCE_MEMORY_WHAT,
+                        opts | OPTION_UPDATES_STOPPED | OPTION_STORAGE_LOW, seq));
+                if ((opts & OPTION_FORCE_UPDATE) != 0) {
+                    pw.println(seq);
+                }
+            } break;
+            case "force-not-low": {
+                int opts = parseOptions(shell);
+                getContext().enforceCallingOrSelfPermission(
+                        android.Manifest.permission.DEVICE_POWER, null);
+                int seq = mSeq.incrementAndGet();
+                mHandler.sendMessage(mHandler.obtainMessage(FORCE_MEMORY_WHAT,
+                        opts | OPTION_UPDATES_STOPPED, seq));
+                if ((opts & OPTION_FORCE_UPDATE) != 0) {
+                    pw.println(seq);
+                }
+            } break;
+            case "reset": {
+                int opts = parseOptions(shell);
+                getContext().enforceCallingOrSelfPermission(
+                        android.Manifest.permission.DEVICE_POWER, null);
+                int seq = mSeq.incrementAndGet();
+                mHandler.sendMessage(mHandler.obtainMessage(FORCE_MEMORY_WHAT,
+                        opts, seq));
+                if ((opts & OPTION_FORCE_UPDATE) != 0) {
+                    pw.println(seq);
+                }
+            } break;
+            default:
+                return shell.handleDefaultCommands(cmd);
+        }
+        return 0;
+    }
 
-        pw.print("  mFreeMemAfterLastCacheClear=");
-        pw.println(Formatter.formatFileSize(context, mFreeMemAfterLastCacheClear));
+    static void dumpHelp(PrintWriter pw) {
+        pw.println("Device storage monitor service (devicestoragemonitor) commands:");
+        pw.println("  help");
+        pw.println("    Print this help text.");
+        pw.println("  force-low [-f]");
+        pw.println("    Force storage to be low, freezing storage state.");
+        pw.println("    -f: force a storage change broadcast be sent, prints new sequence.");
+        pw.println("  force-not-low [-f]");
+        pw.println("    Force storage to not be low, freezing storage state.");
+        pw.println("    -f: force a storage change broadcast be sent, prints new sequence.");
+        pw.println("  reset [-f]");
+        pw.println("    Unfreeze storage state, returning to current real values.");
+        pw.println("    -f: force a storage change broadcast be sent, prints new sequence.");
+    }
 
-        pw.print("  mLastReportedFreeMem=");
-        pw.print(Formatter.formatFileSize(context, mLastReportedFreeMem));
-        pw.print(" mLastReportedFreeMemTime=");
-        TimeUtils.formatDuration(mLastReportedFreeMemTime, SystemClock.elapsedRealtime(), pw);
-        pw.println();
+    void dumpImpl(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (args == null || args.length == 0 || "-a".equals(args[0])) {
+            final Context context = getContext();
 
-        pw.print("  mLowMemFlag="); pw.print(mLowMemFlag);
-        pw.print(" mMemFullFlag="); pw.println(mMemFullFlag);
-        pw.print(" mIsBootImageOnDisk="); pw.print(mIsBootImageOnDisk);
+            pw.println("Current DeviceStorageMonitor state:");
 
-        pw.print("  mClearSucceeded="); pw.print(mClearSucceeded);
-        pw.print(" mClearingCache="); pw.println(mClearingCache);
+            pw.print("  mFreeMem=");
+            pw.print(Formatter.formatFileSize(context, mFreeMem));
+            pw.print(" mTotalMemory=");
+            pw.println(Formatter.formatFileSize(context, mTotalMemory));
 
-        pw.print("  mMemLowThreshold=");
-        pw.print(Formatter.formatFileSize(context, mMemLowThreshold));
-        pw.print(" mMemFullThreshold=");
-        pw.println(Formatter.formatFileSize(context, mMemFullThreshold));
+            pw.print("  mFreeMemAfterLastCacheClear=");
+            pw.println(Formatter.formatFileSize(context, mFreeMemAfterLastCacheClear));
 
-        pw.print("  mMemCacheStartTrimThreshold=");
-        pw.print(Formatter.formatFileSize(context, mMemCacheStartTrimThreshold));
-        pw.print(" mMemCacheTrimToThreshold=");
-        pw.println(Formatter.formatFileSize(context, mMemCacheTrimToThreshold));
+            pw.print("  mLastReportedFreeMem=");
+            pw.print(Formatter.formatFileSize(context, mLastReportedFreeMem));
+            pw.print(" mLastReportedFreeMemTime=");
+            TimeUtils.formatDuration(mLastReportedFreeMemTime, SystemClock.elapsedRealtime(), pw);
+            pw.println();
+
+            if (mUpdatesStopped) {
+                pw.print("  mUpdatesStopped=");
+                pw.print(mUpdatesStopped);
+                pw.print(" mSeq=");
+                pw.println(mSeq.get());
+            } else {
+                pw.print("  mClearSucceeded=");
+                pw.print(mClearSucceeded);
+                pw.print(" mClearingCache=");
+                pw.println(mClearingCache);
+            }
+
+            pw.print("  mLowMemFlag=");
+            pw.print(mLowMemFlag);
+            pw.print(" mMemFullFlag=");
+            pw.println(mMemFullFlag);
+
+            pw.print("  mMemLowThreshold=");
+            pw.print(Formatter.formatFileSize(context, mMemLowThreshold));
+            pw.print(" mMemFullThreshold=");
+            pw.println(Formatter.formatFileSize(context, mMemFullThreshold));
+
+            pw.print("  mMemCacheStartTrimThreshold=");
+            pw.print(Formatter.formatFileSize(context, mMemCacheStartTrimThreshold));
+            pw.print(" mMemCacheTrimToThreshold=");
+            pw.println(Formatter.formatFileSize(context, mMemCacheTrimToThreshold));
+
+            pw.print("  mIsBootImageOnDisk="); pw.println(mIsBootImageOnDisk);
+        } else {
+            Shell shell = new Shell();
+            shell.exec(mRemoteService, null, fd, null, args, null, new ResultReceiver(null));
+        }
     }
 
     /**
@@ -476,7 +631,7 @@ public class DeviceStorageMonitorService extends SystemService {
     * an error dialog indicating low disk space and launch the Installer
     * application
     */
-    private void sendNotification() {
+    private void sendNotification(int seq) {
         final Context context = getContext();
         if(localLOGV) Slog.i(TAG, "Sending low memory notification");
         //log the event to event log with the amount of free storage(in bytes) left on the device
@@ -514,13 +669,17 @@ public class DeviceStorageMonitorService extends SystemService {
         notification.flags |= Notification.FLAG_NO_CLEAR;
         notificationMgr.notifyAsUser(null, LOW_MEMORY_NOTIFICATION_ID, notification,
                 UserHandle.ALL);
-        context.sendStickyBroadcastAsUser(mStorageLowIntent, UserHandle.ALL);
+        Intent broadcast = new Intent(mStorageLowIntent);
+        if (seq != 0) {
+            broadcast.putExtra(EXTRA_SEQUENCE, seq);
+        }
+        context.sendStickyBroadcastAsUser(broadcast, UserHandle.ALL);
     }
 
     /**
      * Cancels low storage notification and sends OK intent.
      */
-    private void cancelNotification() {
+    private void cancelNotification(int seq) {
         final Context context = getContext();
         if(localLOGV) Slog.i(TAG, "Canceling low memory notification");
         NotificationManager mNotificationMgr =
@@ -530,7 +689,11 @@ public class DeviceStorageMonitorService extends SystemService {
         mNotificationMgr.cancelAsUser(null, LOW_MEMORY_NOTIFICATION_ID, UserHandle.ALL);
 
         context.removeStickyBroadcastAsUser(mStorageLowIntent, UserHandle.ALL);
-        context.sendBroadcastAsUser(mStorageOkIntent, UserHandle.ALL);
+        Intent broadcast = new Intent(mStorageOkIntent);
+        if (seq != 0) {
+            broadcast.putExtra(EXTRA_SEQUENCE, seq);
+        }
+        context.sendBroadcastAsUser(broadcast, UserHandle.ALL);
     }
 
     /**