diff options
Diffstat (limited to 'services/core/java/com/android/server/storage/DeviceStorageMonitorService.java')
-rw-r--r-- | services/core/java/com/android/server/storage/DeviceStorageMonitorService.java | 505 |
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); + } + } +} |