Introduce a Lifecycle for system services

Provide an abstract class for system services to extend from,
similar to the android.app.Service.

This will allow services to receive events in a uniform way,
and will allow services to be created and started in the
correct order regardless of whether or not a particular
service exists.

Similar to android.app.Service, services are meant to implement
Binder interfaces as inner classes. This prevents services from
having incestuous access to each other and makes them use the
public API.

Change-Id: Iaacfee8d5f080a28d7cc606761f4624673ed390f
diff --git a/services/java/com/android/server/storage/DeviceStorageMonitorService.java b/services/java/com/android/server/storage/DeviceStorageMonitorService.java
new file mode 100644
index 0000000..8805084
--- /dev/null
+++ b/services/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);
+        }
+    }
+}