/* * 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 com.android.server.pm.PackageManagerService; 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; import dalvik.system.VMRuntime; /** * 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 final boolean mIsBootImageOnDisk; private final ContentResolver mResolver; private final long mTotalMemory; // on /data private final StatFs mDataFileStats; private final StatFs mSystemFileStats; private final 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 final Intent mStorageLowIntent; private final Intent mStorageOkIntent; private final Intent mStorageFullIntent; private final 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 final 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 (!mLowMemFlag && !mIsBootImageOnDisk) { Slog.i(TAG, "No boot image on disk due to lack of space. Sending notification"); sendNotification(); } 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); } public DeviceStorageMonitorService(Context context) { super(context); mLastReportedFreeMemTime = 0; mResolver = context.getContentResolver(); mIsBootImageOnDisk = isBootImageOnDisk(); //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); } private static boolean isBootImageOnDisk() { for (String instructionSet : PackageManagerService.getAllDexCodeInstructionSets()) { if (!VMRuntime.isBootClassPathOnDisk(instructionSet)) { return false; } } return true; } /** * Initializes the disk space threshold value and posts an empty message to * kickstart the process. */ @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 || !mIsBootImageOnDisk; } @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(" mIsBootImageOnDisk="); pw.print(mIsBootImageOnDisk); 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(mIsBootImageOnDisk ? com.android.internal.R.string.low_internal_storage_view_text : com.android.internal.R.string.low_internal_storage_view_text_no_boot); PendingIntent intent = PendingIntent.getActivityAsUser(context, 0, lowMemIntent, 0, null, UserHandle.CURRENT); Notification notification = new Notification.Builder(context) .setSmallIcon(com.android.internal.R.drawable.stat_notify_disk_full) .setTicker(title) .setColor(context.getResources().getColor( com.android.internal.R.color.system_notification_accent_color)) .setContentTitle(title) .setContentText(details) .setContentIntent(intent) .setStyle(new Notification.BigTextStyle() .bigText(details)) .setVisibility(Notification.VISIBILITY_PUBLIC) .setCategory(Notification.CATEGORY_SYSTEM) .build(); notification.flags |= Notification.FLAG_NO_CLEAR; 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); } } }