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
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);
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
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);
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
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);
*/
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;
}
/**
+ * 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() {
}
/**
+ * 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>
*
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>
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);
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;
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.
private static final String KEY_MIN_IDLE_COUNT = "min_idle_count";
private static final String KEY_MIN_CHARGING_COUNT = "min_charging_count";
private static final String KEY_MIN_BATTERY_NOT_LOW_COUNT = "min_battery_not_low_count";
+ private static final String KEY_MIN_STORAGE_NOT_LOW_COUNT = "min_storage_not_low_count";
private static final String KEY_MIN_CONNECTIVITY_COUNT = "min_connectivity_count";
private static final String KEY_MIN_CONTENT_COUNT = "min_content_count";
private static final String KEY_MIN_READY_JOBS_COUNT = "min_ready_jobs_count";
private static final 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;
*/
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.
*/
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,
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();
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));
class MaybeReadyJobQueueFunctor implements JobStatusFunctor {
int chargingCount;
int batteryNotLowCount;
+ int storageNotLowCount;
int idleCount;
int backoffCount;
int connectivityCount;
if (job.hasBatteryNotLowConstraint()) {
batteryNotLowCount++;
}
+ if (job.hasStorageNotLowConstraint()) {
+ storageNotLowCount++;
+ }
if (job.hasContentTriggerConstraint()) {
contentCount++;
}
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)) {
backoffCount = 0;
connectivityCount = 0;
batteryNotLowCount = 0;
+ storageNotLowCount = 0;
contentCount = 0;
runnableJobs = null;
}
}
}
+ 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++) {
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);
}
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();
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();
}
mStateChangedListener.onControllerStateChanged();
}
// Also tell the scheduler that any ready jobs should be flushed.
- if (stablePower) {
+ if (stablePower || batteryNotLow) {
mStateChangedListener.onRunJobNow(null);
}
}
* 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);
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;
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;
}
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);
}
}
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;
/**
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");
}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
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;
import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.concurrent.atomic.AtomicInteger;
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;
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;
@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);
}
};
}
}
+ 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;
// 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 " +
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) {
}
};
- 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)
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));
+ }
}
/**
* 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
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 =
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);
}
/**