summaryrefslogtreecommitdiffstats
path: root/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java
diff options
context:
space:
mode:
Diffstat (limited to 'services/core/java/com/android/server/storage/DeviceStorageMonitorService.java')
-rw-r--r--services/core/java/com/android/server/storage/DeviceStorageMonitorService.java505
1 files changed, 505 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java b/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java
new file mode 100644
index 0000000..8805084
--- /dev/null
+++ b/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2007-2008 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.storage;
+
+import com.android.server.EventLogTags;
+import com.android.server.SystemService;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.IPackageDataObserver;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+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.ServiceManager;
+import android.os.StatFs;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.storage.StorageManager;
+import android.provider.Settings;
+import android.text.format.Formatter;
+import android.util.EventLog;
+import android.util.Slog;
+import android.util.TimeUtils;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * This class implements a service to monitor the amount of disk
+ * storage space on the device. If the free storage on device is less
+ * than a tunable threshold value (a secure settings parameter;
+ * default 10%) a low memory notification is displayed to alert the
+ * user. If the user clicks on the low memory notification the
+ * Application Manager application gets launched to let the user free
+ * storage space.
+ *
+ * Event log events: A low memory event with the free storage on
+ * device in bytes is logged to the event log when the device goes low
+ * on storage space. The amount of free storage on the device is
+ * periodically logged to the event log. The log interval is a secure
+ * settings parameter with a default value of 12 hours. When the free
+ * storage differential goes below a threshold (again a secure
+ * settings parameter with a default value of 2MB), the free memory is
+ * logged to the event log.
+ */
+public class DeviceStorageMonitorService extends SystemService {
+ static final String TAG = "DeviceStorageMonitorService";
+
+ static final boolean DEBUG = false;
+ static final boolean localLOGV = false;
+
+ static final int DEVICE_MEMORY_WHAT = 1;
+ private static final int MONITOR_INTERVAL = 1; //in minutes
+ private static final int LOW_MEMORY_NOTIFICATION_ID = 1;
+
+ private static final int DEFAULT_FREE_STORAGE_LOG_INTERVAL_IN_MINUTES = 12*60; //in minutes
+ private static final long DEFAULT_DISK_FREE_CHANGE_REPORTING_THRESHOLD = 2 * 1024 * 1024; // 2MB
+ private static final long DEFAULT_CHECK_INTERVAL = MONITOR_INTERVAL*60*1000;
+
+ private long mFreeMem; // on /data
+ private long mFreeMemAfterLastCacheClear; // on /data
+ private long mLastReportedFreeMem;
+ private long mLastReportedFreeMemTime;
+ boolean mLowMemFlag=false;
+ private boolean mMemFullFlag=false;
+ private ContentResolver mResolver;
+ private long mTotalMemory; // on /data
+ private StatFs mDataFileStats;
+ private StatFs mSystemFileStats;
+ private StatFs mCacheFileStats;
+
+ private static final File DATA_PATH = Environment.getDataDirectory();
+ private static final File SYSTEM_PATH = Environment.getRootDirectory();
+ private static final File CACHE_PATH = Environment.getDownloadCacheDirectory();
+
+ private long mThreadStartTime = -1;
+ boolean mClearSucceeded = false;
+ boolean mClearingCache;
+ private Intent mStorageLowIntent;
+ private Intent mStorageOkIntent;
+ private Intent mStorageFullIntent;
+ private Intent mStorageNotFullIntent;
+ private CachePackageDataObserver mClearCacheObserver;
+ private CacheFileDeletedObserver mCacheFileDeletedObserver;
+ private static final int _TRUE = 1;
+ private static final int _FALSE = 0;
+ // This is the raw threshold that has been set at which we consider
+ // storage to be low.
+ long mMemLowThreshold;
+ // This is the threshold at which we start trying to flush caches
+ // to get below the low threshold limit. It is less than the low
+ // threshold; we will allow storage to get a bit beyond the limit
+ // before flushing and checking if we are actually low.
+ private long mMemCacheStartTrimThreshold;
+ // This is the threshold that we try to get to when deleting cache
+ // files. This is greater than the low threshold so that we will flush
+ // more files than absolutely needed, to reduce the frequency that
+ // flushing takes place.
+ private long mMemCacheTrimToThreshold;
+ private long mMemFullThreshold;
+
+ /**
+ * This string is used for ServiceManager access to this class.
+ */
+ static final String SERVICE = "devicestoragemonitor";
+
+ /**
+ * Handler that checks the amount of disk space on the device and sends a
+ * notification if the device runs low on disk space
+ */
+ private Handler mHandler = new Handler() {
+ @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;
+ }
+ checkMemory(msg.arg1 == _TRUE);
+ }
+ };
+
+ private class CachePackageDataObserver extends IPackageDataObserver.Stub {
+ public void onRemoveCompleted(String packageName, boolean succeeded) {
+ mClearSucceeded = succeeded;
+ mClearingCache = false;
+ if(localLOGV) Slog.i(TAG, " Clear succeeded:"+mClearSucceeded
+ +", mClearingCache:"+mClearingCache+" Forcing memory check");
+ postCheckMemoryMsg(false, 0);
+ }
+ }
+
+ private void restatDataDir() {
+ try {
+ mDataFileStats.restat(DATA_PATH.getAbsolutePath());
+ mFreeMem = (long) mDataFileStats.getAvailableBlocks() *
+ mDataFileStats.getBlockSize();
+ } catch (IllegalArgumentException e) {
+ // use the old value of mFreeMem
+ }
+ // Allow freemem to be overridden by debug.freemem for testing
+ String debugFreeMem = SystemProperties.get("debug.freemem");
+ if (!"".equals(debugFreeMem)) {
+ mFreeMem = Long.parseLong(debugFreeMem);
+ }
+ // Read the log interval from secure settings
+ long freeMemLogInterval = Settings.Global.getLong(mResolver,
+ Settings.Global.SYS_FREE_STORAGE_LOG_INTERVAL,
+ DEFAULT_FREE_STORAGE_LOG_INTERVAL_IN_MINUTES)*60*1000;
+ //log the amount of free memory in event log
+ long currTime = SystemClock.elapsedRealtime();
+ if((mLastReportedFreeMemTime == 0) ||
+ (currTime-mLastReportedFreeMemTime) >= freeMemLogInterval) {
+ mLastReportedFreeMemTime = currTime;
+ long mFreeSystem = -1, mFreeCache = -1;
+ try {
+ mSystemFileStats.restat(SYSTEM_PATH.getAbsolutePath());
+ mFreeSystem = (long) mSystemFileStats.getAvailableBlocks() *
+ mSystemFileStats.getBlockSize();
+ } catch (IllegalArgumentException e) {
+ // ignore; report -1
+ }
+ try {
+ mCacheFileStats.restat(CACHE_PATH.getAbsolutePath());
+ mFreeCache = (long) mCacheFileStats.getAvailableBlocks() *
+ mCacheFileStats.getBlockSize();
+ } catch (IllegalArgumentException e) {
+ // ignore; report -1
+ }
+ EventLog.writeEvent(EventLogTags.FREE_STORAGE_LEFT,
+ mFreeMem, mFreeSystem, mFreeCache);
+ }
+ // Read the reporting threshold from secure settings
+ long threshold = Settings.Global.getLong(mResolver,
+ Settings.Global.DISK_FREE_CHANGE_REPORTING_THRESHOLD,
+ DEFAULT_DISK_FREE_CHANGE_REPORTING_THRESHOLD);
+ // If mFree changed significantly log the new value
+ long delta = mFreeMem - mLastReportedFreeMem;
+ if (delta > threshold || delta < -threshold) {
+ mLastReportedFreeMem = mFreeMem;
+ EventLog.writeEvent(EventLogTags.FREE_STORAGE_CHANGED, mFreeMem);
+ }
+ }
+
+ private void clearCache() {
+ if (mClearCacheObserver == null) {
+ // Lazy instantiation
+ mClearCacheObserver = new CachePackageDataObserver();
+ }
+ mClearingCache = true;
+ try {
+ if (localLOGV) Slog.i(TAG, "Clearing cache");
+ IPackageManager.Stub.asInterface(ServiceManager.getService("package")).
+ freeStorageAndNotify(mMemCacheTrimToThreshold, mClearCacheObserver);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to get handle for PackageManger Exception: "+e);
+ mClearingCache = false;
+ mClearSucceeded = false;
+ }
+ }
+
+ void checkMemory(boolean checkCache) {
+ //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(localLOGV) Slog.i(TAG, "Thread already running just skip");
+ //make sure the thread is not hung for too long
+ long diffTime = System.currentTimeMillis() - mThreadStartTime;
+ if(diffTime > (10*60*1000)) {
+ Slog.w(TAG, "Thread that clears cache file seems to run for ever");
+ }
+ } else {
+ restatDataDir();
+ if (localLOGV) Slog.v(TAG, "freeMemory="+mFreeMem);
+
+ //post intent to NotificationManager to display icon if necessary
+ if (mFreeMem < mMemLowThreshold) {
+ if (checkCache) {
+ // We are allowed to clear cache files at this point to
+ // try to get down below the limit, because this is not
+ // the initial call after a cache clear has been attempted.
+ // In this case we will try a cache clear if our free
+ // space has gone below the cache clear limit.
+ if (mFreeMem < mMemCacheStartTrimThreshold) {
+ // We only clear the cache if the free storage has changed
+ // a significant amount since the last time.
+ if ((mFreeMemAfterLastCacheClear-mFreeMem)
+ >= ((mMemLowThreshold-mMemCacheStartTrimThreshold)/4)) {
+ // See if clearing cache helps
+ // Note that clearing cache is asynchronous and so we do a
+ // memory check again once the cache has been cleared.
+ mThreadStartTime = System.currentTimeMillis();
+ mClearSucceeded = false;
+ clearCache();
+ }
+ }
+ } else {
+ // This is a call from after clearing the cache. Note
+ // the amount of free storage at this point.
+ mFreeMemAfterLastCacheClear = mFreeMem;
+ if (!mLowMemFlag) {
+ // 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();
+ mLowMemFlag = true;
+ } else {
+ if (localLOGV) Slog.v(TAG, "Running low on memory " +
+ "notification already sent. do nothing");
+ }
+ }
+ } else {
+ mFreeMemAfterLastCacheClear = mFreeMem;
+ if (mLowMemFlag) {
+ Slog.i(TAG, "Memory available. Cancelling notification");
+ cancelNotification();
+ mLowMemFlag = false;
+ }
+ }
+ if (mFreeMem < mMemFullThreshold) {
+ if (!mMemFullFlag) {
+ sendFullNotification();
+ mMemFullFlag = true;
+ }
+ } else {
+ if (mMemFullFlag) {
+ cancelFullNotification();
+ mMemFullFlag = false;
+ }
+ }
+ }
+ if(localLOGV) Slog.i(TAG, "Posting Message again");
+ //keep posting messages to itself periodically
+ postCheckMemoryMsg(true, DEFAULT_CHECK_INTERVAL);
+ }
+
+ void postCheckMemoryMsg(boolean clearCache, long delay) {
+ // Remove queued messages
+ mHandler.removeMessages(DEVICE_MEMORY_WHAT);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(DEVICE_MEMORY_WHAT,
+ clearCache ?_TRUE : _FALSE, 0),
+ delay);
+ }
+
+ /**
+ * Constructor to run service. initializes the disk space threshold value
+ * and posts an empty message to kickstart the process.
+ */
+ @Override
+ public void onCreate(Context context) {
+ mLastReportedFreeMemTime = 0;
+ mResolver = context.getContentResolver();
+ //create StatFs object
+ mDataFileStats = new StatFs(DATA_PATH.getAbsolutePath());
+ mSystemFileStats = new StatFs(SYSTEM_PATH.getAbsolutePath());
+ mCacheFileStats = new StatFs(CACHE_PATH.getAbsolutePath());
+ //initialize total storage on device
+ mTotalMemory = (long)mDataFileStats.getBlockCount() *
+ mDataFileStats.getBlockSize();
+ mStorageLowIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_LOW);
+ mStorageLowIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mStorageOkIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_OK);
+ mStorageOkIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mStorageFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_FULL);
+ mStorageFullIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mStorageNotFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_NOT_FULL);
+ mStorageNotFullIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ }
+
+ @Override
+ public void onStart() {
+ // cache storage thresholds
+ final StorageManager sm = StorageManager.from(getContext());
+ mMemLowThreshold = sm.getStorageLowBytes(DATA_PATH);
+ mMemFullThreshold = sm.getStorageFullBytes(DATA_PATH);
+
+ mMemCacheStartTrimThreshold = ((mMemLowThreshold*3)+mMemFullThreshold)/4;
+ mMemCacheTrimToThreshold = mMemLowThreshold
+ + ((mMemLowThreshold-mMemCacheStartTrimThreshold)*2);
+ mFreeMemAfterLastCacheClear = mTotalMemory;
+ checkMemory(true);
+
+ mCacheFileDeletedObserver = new CacheFileDeletedObserver();
+ mCacheFileDeletedObserver.startWatching();
+
+ publishBinderService(SERVICE, mRemoteService);
+ publishLocalService(DeviceStorageMonitorInternal.class, mLocalService);
+ }
+
+ private final DeviceStorageMonitorInternal mLocalService = new DeviceStorageMonitorInternal() {
+ @Override
+ public void checkMemory() {
+ // force an early check
+ postCheckMemoryMsg(true, 0);
+ }
+
+ @Override
+ public boolean isMemoryLow() {
+ return mLowMemFlag;
+ }
+
+ @Override
+ public long getMemoryLowThreshold() {
+ return mMemLowThreshold;
+ }
+ };
+
+ private final IBinder mRemoteService = new Binder() {
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+
+ pw.println("Permission Denial: can't dump " + SERVICE + " from from pid="
+ + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ return;
+ }
+
+ dumpImpl(pw);
+ }
+ };
+
+ void dumpImpl(PrintWriter pw) {
+ final Context context = getContext();
+
+ pw.println("Current DeviceStorageMonitor state:");
+
+ pw.print(" mFreeMem="); pw.print(Formatter.formatFileSize(context, mFreeMem));
+ pw.print(" mTotalMemory=");
+ pw.println(Formatter.formatFileSize(context, mTotalMemory));
+
+ pw.print(" mFreeMemAfterLastCacheClear=");
+ pw.println(Formatter.formatFileSize(context, mFreeMemAfterLastCacheClear));
+
+ pw.print(" mLastReportedFreeMem=");
+ pw.print(Formatter.formatFileSize(context, mLastReportedFreeMem));
+ pw.print(" mLastReportedFreeMemTime=");
+ TimeUtils.formatDuration(mLastReportedFreeMemTime, SystemClock.elapsedRealtime(), pw);
+ pw.println();
+
+ pw.print(" mLowMemFlag="); pw.print(mLowMemFlag);
+ pw.print(" mMemFullFlag="); pw.println(mMemFullFlag);
+
+ pw.print(" mClearSucceeded="); pw.print(mClearSucceeded);
+ pw.print(" mClearingCache="); pw.println(mClearingCache);
+
+ 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));
+ }
+
+ /**
+ * This method sends a notification to NotificationManager to display
+ * an error dialog indicating low disk space and launch the Installer
+ * application
+ */
+ private void sendNotification() {
+ 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
+ EventLog.writeEvent(EventLogTags.LOW_STORAGE, mFreeMem);
+ // Pack up the values and broadcast them to everyone
+ Intent lowMemIntent = new Intent(Environment.isExternalStorageEmulated()
+ ? Settings.ACTION_INTERNAL_STORAGE_SETTINGS
+ : Intent.ACTION_MANAGE_PACKAGE_STORAGE);
+ lowMemIntent.putExtra("memory", mFreeMem);
+ lowMemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ NotificationManager mNotificationMgr =
+ (NotificationManager)context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ CharSequence title = context.getText(
+ com.android.internal.R.string.low_internal_storage_view_title);
+ CharSequence details = context.getText(
+ com.android.internal.R.string.low_internal_storage_view_text);
+ PendingIntent intent = PendingIntent.getActivityAsUser(context, 0, lowMemIntent, 0,
+ null, UserHandle.CURRENT);
+ Notification notification = new Notification();
+ notification.icon = com.android.internal.R.drawable.stat_notify_disk_full;
+ notification.tickerText = title;
+ notification.flags |= Notification.FLAG_NO_CLEAR;
+ notification.setLatestEventInfo(context, title, details, intent);
+ mNotificationMgr.notifyAsUser(null, LOW_MEMORY_NOTIFICATION_ID, notification,
+ UserHandle.ALL);
+ context.sendStickyBroadcastAsUser(mStorageLowIntent, UserHandle.ALL);
+ }
+
+ /**
+ * Cancels low storage notification and sends OK intent.
+ */
+ private void cancelNotification() {
+ final Context context = getContext();
+ if(localLOGV) Slog.i(TAG, "Canceling low memory notification");
+ NotificationManager mNotificationMgr =
+ (NotificationManager)context.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ //cancel notification since memory has been freed
+ mNotificationMgr.cancelAsUser(null, LOW_MEMORY_NOTIFICATION_ID, UserHandle.ALL);
+
+ context.removeStickyBroadcastAsUser(mStorageLowIntent, UserHandle.ALL);
+ context.sendBroadcastAsUser(mStorageOkIntent, UserHandle.ALL);
+ }
+
+ /**
+ * Send a notification when storage is full.
+ */
+ private void sendFullNotification() {
+ if(localLOGV) Slog.i(TAG, "Sending memory full notification");
+ getContext().sendStickyBroadcastAsUser(mStorageFullIntent, UserHandle.ALL);
+ }
+
+ /**
+ * Cancels memory full notification and sends "not full" intent.
+ */
+ private void cancelFullNotification() {
+ if(localLOGV) Slog.i(TAG, "Canceling memory full notification");
+ getContext().removeStickyBroadcastAsUser(mStorageFullIntent, UserHandle.ALL);
+ getContext().sendBroadcastAsUser(mStorageNotFullIntent, UserHandle.ALL);
+ }
+
+ private static class CacheFileDeletedObserver extends FileObserver {
+ public CacheFileDeletedObserver() {
+ super(Environment.getDownloadCacheDirectory().getAbsolutePath(), FileObserver.DELETE);
+ }
+
+ @Override
+ public void onEvent(int event, String path) {
+ EventLogTags.writeCacheFileDeleted(path);
+ }
+ }
+}