/* * 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; import com.android.server.am.ActivityManagerService; 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.os.Binder; import android.os.Handler; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.StatFs; import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Settings; import android.util.Config; import android.util.EventLog; import android.util.Slog; import android.provider.Settings; /** * 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. */ class DeviceStorageMonitorService extends Binder { private static final String TAG = "DeviceStorageMonitorService"; private static final boolean DEBUG = false; private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; private 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_THRESHOLD_PERCENTAGE = 10; 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 static final int DEFAULT_FULL_THRESHOLD_BYTES = 1024*1024; // 1MB private long mFreeMem; // on /data private long mLastReportedFreeMem; private long mLastReportedFreeMemTime; private boolean mLowMemFlag=false; private boolean mMemFullFlag=false; private Context mContext; private ContentResolver mContentResolver; private long mTotalMemory; // on /data private StatFs mDataFileStats; private StatFs mSystemFileStats; private StatFs mCacheFileStats; private static final String DATA_PATH = "/data"; private static final String SYSTEM_PATH = "/system"; private static final String CACHE_PATH = "/cache"; private long mThreadStartTime = -1; private boolean mClearSucceeded = false; private boolean mClearingCache; private Intent mStorageLowIntent; private Intent mStorageOkIntent; private Intent mStorageFullIntent; private Intent mStorageNotFullIntent; private CachePackageDataObserver mClearCacheObserver; private static final int _TRUE = 1; private static final int _FALSE = 0; private long mMemLowThreshold; private int 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 */ 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); } }; 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 final void restatDataDir() { try { mDataFileStats.restat(DATA_PATH); 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.Secure.getLong(mContentResolver, Settings.Secure.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); mFreeSystem = (long) mSystemFileStats.getAvailableBlocks() * mSystemFileStats.getBlockSize(); } catch (IllegalArgumentException e) { // ignore; report -1 } try { mCacheFileStats.restat(CACHE_PATH); mFreeCache = (long) mCacheFileStats.getAvailableBlocks() * mCacheFileStats.getBlockSize(); } catch (IllegalArgumentException e) { // ignore; report -1 } mCacheFileStats.restat(CACHE_PATH); EventLog.writeEvent(EventLogTags.FREE_STORAGE_LEFT, mFreeMem, mFreeSystem, mFreeCache); } // Read the reporting threshold from secure settings long threshold = Settings.Secure.getLong(mContentResolver, Settings.Secure.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 final 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(mMemLowThreshold, mClearCacheObserver); } catch (RemoteException e) { Slog.w(TAG, "Failed to get handle for PackageManger Exception: "+e); mClearingCache = false; mClearSucceeded = false; } } private final 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 (!mLowMemFlag) { if (checkCache) { // 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 { 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 { 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); } private 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); } /* * just query settings to retrieve the memory threshold. * Preferred this over using a ContentObserver since Settings.Secure caches the value * any way */ private long getMemThreshold() { int value = Settings.Secure.getInt( mContentResolver, Settings.Secure.SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE); if(localLOGV) Slog.v(TAG, "Threshold Percentage="+value); //evaluate threshold value return mTotalMemory*value; } /* * just query settings to retrieve the memory full threshold. * Preferred this over using a ContentObserver since Settings.Secure caches the value * any way */ private int getMemFullThreshold() { int value = Settings.Secure.getInt( mContentResolver, Settings.Secure.SYS_STORAGE_FULL_THRESHOLD_BYTES, DEFAULT_FULL_THRESHOLD_BYTES); if(localLOGV) Slog.v(TAG, "Full Threshold Bytes="+value); return value; } /** * Constructor to run service. initializes the disk space threshold value * and posts an empty message to kickstart the process. */ public DeviceStorageMonitorService(Context context) { mLastReportedFreeMemTime = 0; mContext = context; mContentResolver = mContext.getContentResolver(); //create StatFs object mDataFileStats = new StatFs(DATA_PATH); mSystemFileStats = new StatFs(SYSTEM_PATH); mCacheFileStats = new StatFs(CACHE_PATH); //initialize total storage on device mTotalMemory = ((long)mDataFileStats.getBlockCount() * mDataFileStats.getBlockSize())/100L; 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); // cache storage thresholds mMemLowThreshold = getMemThreshold(); mMemFullThreshold = getMemFullThreshold(); checkMemory(true); } /** * This method sends a notification to NotificationManager to display * an error dialog indicating low disk space and launch the Installer * application */ private final void sendNotification() { 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(Intent.ACTION_MANAGE_PACKAGE_STORAGE); lowMemIntent.putExtra("memory", mFreeMem); lowMemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); NotificationManager mNotificationMgr = (NotificationManager)mContext.getSystemService( Context.NOTIFICATION_SERVICE); CharSequence title = mContext.getText( com.android.internal.R.string.low_internal_storage_view_title); CharSequence details = mContext.getText( com.android.internal.R.string.low_internal_storage_view_text); PendingIntent intent = PendingIntent.getActivity(mContext, 0, lowMemIntent, 0); 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(mContext, title, details, intent); mNotificationMgr.notify(LOW_MEMORY_NOTIFICATION_ID, notification); mContext.sendStickyBroadcast(mStorageLowIntent); } /** * Cancels low storage notification and sends OK intent. */ private final void cancelNotification() { if(localLOGV) Slog.i(TAG, "Canceling low memory notification"); NotificationManager mNotificationMgr = (NotificationManager)mContext.getSystemService( Context.NOTIFICATION_SERVICE); //cancel notification since memory has been freed mNotificationMgr.cancel(LOW_MEMORY_NOTIFICATION_ID); mContext.removeStickyBroadcast(mStorageLowIntent); mContext.sendBroadcast(mStorageOkIntent); } /** * Send a notification when storage is full. */ private final void sendFullNotification() { if(localLOGV) Slog.i(TAG, "Sending memory full notification"); mContext.sendStickyBroadcast(mStorageFullIntent); } /** * Cancels memory full notification and sends "not full" intent. */ private final void cancelFullNotification() { if(localLOGV) Slog.i(TAG, "Canceling memory full notification"); mContext.removeStickyBroadcast(mStorageFullIntent); mContext.sendBroadcast(mStorageNotFullIntent); } public void updateMemory() { int callingUid = getCallingUid(); if(callingUid != Process.SYSTEM_UID) { return; } // force an early check postCheckMemoryMsg(true, 0); } }