Default setup for excessive I/O notification

- Set sample default values for the thresholds that trigger
the I/O notification;
- Centralize tracking of the service's configuration;
- Implement a default broadcast agent.

Test: bit CarServiceTest:com.android.car.CarStorageMonitoringTest
Change-Id: I9eb0259eedd34a27532cb621383b87075c6460a3
diff --git a/car_product/overlay/packages/services/Car/service/res/values/Config.xml b/car_product/overlay/packages/services/Car/service/res/values/Config.xml
index 0680f15..021143c 100644
--- a/car_product/overlay/packages/services/Car/service/res/values/Config.xml
+++ b/car_product/overlay/packages/services/Car/service/res/values/Config.xml
@@ -1,4 +1,9 @@
 <resources>
     <!-- default activity whitelist which are allowed while driving -->
     <string name="defauiltActivityWhitelist">com.android.systemui,com.android.car.dialer,com.android.car.hvac,com.android.car.media,com.android.car.radio,com.android.support.car.lenspicker,com.google.android.setupwizard</string>
+
+    <integer name="acceptableWrittenKBytesPerSample">115000</integer>
+    <integer name="acceptableFsyncCallsPerSample">150</integer>
+    <integer name="maxExcessiveIoSamplesInWindow">11</integer>
+    <string name="intentReceiverForUnacceptableIoMetrics">com.google.android.car.defaultstoragemonitoringcompanionapp/.ExcessiveIoIntentReceiver</string>
 </resources>
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index 21925a3..69cc748 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -107,7 +107,10 @@
     <integer name="ioStatsNumSamplesToStore">15</integer>
 
     <!-- The maximum number of KB (1024 bytes) that can be written to storage in one sample
-         before CarService deems I/O activity excessive. -->
+         before CarService deems I/O activity excessive. A simple way to set this value
+         is - given the total writable amount (storage size * P/E cycles) - to make
+         reasonable assumptions about the expected lifetime of the vehicle and the average
+         daily driving time, and use that to allocate a per-sample budget. -->
     <integer name="acceptableWrittenKBytesPerSample">0</integer>
     <!-- The maximum number of fsync() system calls that can be made in one sample before
          CarService deems I/O activity excessive. -->
@@ -121,6 +124,9 @@
     <!-- The name of an intent to be notified by CarService whenever it detects too many
          samples with excessive I/O activity. Value must either be an empty string, which
          means that no notification will take place, or be in the format of a flattened
-         ComponentName and reference a valid BroadcastReceiver. -->
+         ComponentName and reference a valid BroadcastReceiver. This broadcast receiver
+         must be registered in its containing app's AndroidManifest.xml and it is
+         strongly recommended that it be protected with the
+         android.car.permission.STORAGE_MONITORING permission. -->
     <string name="intentReceiverForUnacceptableIoMetrics"></string>
 </resources>
diff --git a/service/src/com/android/car/CarStorageMonitoringService.java b/service/src/com/android/car/CarStorageMonitoringService.java
index f262b6f..25f885a 100644
--- a/service/src/com/android/car/CarStorageMonitoringService.java
+++ b/service/src/com/android/car/CarStorageMonitoringService.java
@@ -80,9 +80,7 @@
     private final SlidingWindow<UidIoStatsDelta> mIoStatsSamples;
     private final RemoteCallbackList<IUidIoStatsListener> mListeners;
     private final Object mIoStatsSamplesLock = new Object();
-    private final long mAcceptableBytesWrittenPerSample;
-    private final int mAcceptableFsyncCallsPerSample;
-    private final int mThresholdSamplesCount;
+    private final Configuration mConfiguration;
 
     private final CarPermission mStorageMonitoringPermission;
 
@@ -96,6 +94,10 @@
     public CarStorageMonitoringService(Context context, SystemInterface systemInterface) {
         mContext = context;
         Resources resources = mContext.getResources();
+        mConfiguration = new Configuration(resources);
+
+        Log.d(TAG, "service configuration: " + mConfiguration);
+
         mUidIoStatsProvider = systemInterface.getUidIoStatsProvider();
         mUptimeTrackerFile = new File(systemInterface.getFilesDir(), UPTIME_TRACKER_FILENAME);
         mWearInfoFile = new File(systemInterface.getFilesDir(), WEAR_INFO_FILENAME);
@@ -105,20 +107,10 @@
         mStorageMonitoringPermission =
                 new CarPermission(mContext, Car.PERMISSION_STORAGE_MONITORING);
         mWearEstimateChanges = Collections.emptyList();
-        mIoStatsSamples = new SlidingWindow<>(
-            resources.getInteger(R.integer.ioStatsNumSamplesToStore));
+        mIoStatsSamples = new SlidingWindow<>(mConfiguration.ioStatsNumSamplesToStore);
+        mListeners = new RemoteCallbackList<>();
         systemInterface.scheduleActionForBootCompleted(this::doInitServiceIfNeeded,
             Duration.ofSeconds(10));
-        mListeners = new RemoteCallbackList<>();
-        mAcceptableBytesWrittenPerSample = 1024 * resources.getInteger(
-                R.integer.acceptableWrittenKBytesPerSample);
-        mAcceptableFsyncCallsPerSample = resources.getInteger(
-               R.integer.acceptableFsyncCallsPerSample);
-        mThresholdSamplesCount = resources.getInteger(R.integer.maxExcessiveIoSamplesInWindow);
-    }
-
-    private static long getUptimeSnapshotIntervalMs() {
-        return Duration.ofHours(R.integer.uptimeHoursIntervalBetweenUptimeDataWrite).toMillis();
     }
 
     private Optional<WearInformation> loadWearInformation() {
@@ -187,13 +179,12 @@
         Log.d(TAG, "CarStorageMonitoringService init()");
 
         mUptimeTracker = new UptimeTracker(mUptimeTrackerFile,
-            getUptimeSnapshotIntervalMs(),
+            mConfiguration.uptimeIntervalBetweenUptimeDataWriteMs,
             mSystemInterface);
     }
 
     private void launchWearChangeActivity() {
-        final String activityPath = mContext.getResources().getString(
-            R.string.activityHandlerForFlashWearChanges);
+        final String activityPath = mConfiguration.activityHandlerForFlashWearChanges;
         if (activityPath.isEmpty()) return;
         try {
             final ComponentName activityComponent =
@@ -254,8 +245,7 @@
     private void sendExcessiveIoBroadcast() {
         Log.w(TAG, "sending excessive I/O notification");
 
-        final String receiverPath = mContext.getResources().getString(
-            R.string.intentReceiverForUnacceptableIoMetrics);
+        final String receiverPath = mConfiguration.intentReceiverForUnacceptableIoMetrics;
         if (receiverPath.isEmpty()) return;
 
         final ComponentName receiverComponent;
@@ -277,9 +267,12 @@
         synchronized (mIoStatsSamplesLock) {
             return mIoStatsSamples.count((UidIoStatsDelta delta) -> {
                 Metrics total = delta.getTotals();
-                return (total.bytesWrittenToStorage > mAcceptableBytesWrittenPerSample) ||
-                    (total.fsyncCalls > mAcceptableFsyncCallsPerSample);
-            }) > mThresholdSamplesCount;
+                final boolean tooManyBytesWritten =
+                    (total.bytesWrittenToStorage > mConfiguration.acceptableBytesWrittenPerSample);
+                final boolean tooManyFsyncCalls =
+                    (total.fsyncCalls > mConfiguration.acceptableFsyncCallsPerSample);
+                return tooManyBytesWritten || tooManyFsyncCalls;
+            }) > mConfiguration.maxExcessiveIoSamplesInWindow;
         }
     }
 
@@ -301,8 +294,6 @@
 
         Log.d(TAG, "initializing CarStorageMonitoringService");
 
-        final Resources resources = mContext.getResources();
-
         mWearInformation = loadWearInformation();
 
         // TODO(egranata): can this be done lazily?
@@ -312,8 +303,8 @@
             storeWearHistory(wearHistory);
         }
         Log.d(TAG, "wear history being tracked is " + wearHistory);
-        mWearEstimateChanges = wearHistory.toWearEstimateChanges(resources.getInteger(
-                        R.integer.acceptableHoursPerOnePercentFlashWear));
+        mWearEstimateChanges = wearHistory.toWearEstimateChanges(
+                mConfiguration.acceptableHoursPerOnePercentFlashWear);
 
         mOnShutdownReboot.addAction((Context ctx, Intent intent) -> release());
 
@@ -335,14 +326,12 @@
                 return stats;
             }).collect(Collectors.toList());
 
-        final long newStatsDelayMs =
-                1000L * resources.getInteger(R.integer.ioStatsRefreshRateSeconds);
-
         mIoStatsTracker = new IoStatsTracker(mBootIoStats,
-                newStatsDelayMs,
+                mConfiguration.ioStatsRefreshRateMs,
                 mSystemInterface.getSystemStateInterface());
 
-        mSystemInterface.scheduleAction(this::collectNewIoMetrics, newStatsDelayMs);
+        mSystemInterface.scheduleAction(this::collectNewIoMetrics,
+                mConfiguration.ioStatsRefreshRateMs);
 
         Log.i(TAG, "CarStorageMonitoringService is up");
 
@@ -460,4 +449,60 @@
 
         mListeners.unregister(listener);
     }
+
+    private static final class Configuration {
+        final long acceptableBytesWrittenPerSample;
+        final int acceptableFsyncCallsPerSample;
+        final int acceptableHoursPerOnePercentFlashWear;
+        final String activityHandlerForFlashWearChanges;
+        final String intentReceiverForUnacceptableIoMetrics;
+        final int ioStatsNumSamplesToStore;
+        final int ioStatsRefreshRateMs;
+        final int maxExcessiveIoSamplesInWindow;
+        final long uptimeIntervalBetweenUptimeDataWriteMs;
+
+        Configuration(Resources resources) throws Resources.NotFoundException {
+            ioStatsNumSamplesToStore = resources.getInteger(R.integer.ioStatsNumSamplesToStore);
+            acceptableBytesWrittenPerSample =
+                    1024 * resources.getInteger(R.integer.acceptableWrittenKBytesPerSample);
+            acceptableFsyncCallsPerSample =
+                    resources.getInteger(R.integer.acceptableFsyncCallsPerSample);
+            maxExcessiveIoSamplesInWindow =
+                    resources.getInteger(R.integer.maxExcessiveIoSamplesInWindow);
+            uptimeIntervalBetweenUptimeDataWriteMs =
+                        60 * 60 * 1000 *
+                        resources.getInteger(R.integer.uptimeHoursIntervalBetweenUptimeDataWrite);
+            acceptableHoursPerOnePercentFlashWear =
+                    resources.getInteger(R.integer.acceptableHoursPerOnePercentFlashWear);
+            ioStatsRefreshRateMs =
+                    1000 * resources.getInteger(R.integer.ioStatsRefreshRateSeconds);
+            activityHandlerForFlashWearChanges =
+                    resources.getString(R.string.activityHandlerForFlashWearChanges);
+            intentReceiverForUnacceptableIoMetrics =
+                    resources.getString(R.string.intentReceiverForUnacceptableIoMetrics);
+        }
+
+        @Override
+        public String toString() {
+            return String.format(
+                "acceptableBytesWrittenPerSample = %d, " +
+                "acceptableFsyncCallsPerSample = %d, " +
+                "acceptableHoursPerOnePercentFlashWear = %d, " +
+                "activityHandlerForFlashWearChanges = %s, " +
+                "intentReceiverForUnacceptableIoMetrics = %s, " +
+                "ioStatsNumSamplesToStore = %d, " +
+                "ioStatsRefreshRateMs = %d, " +
+                "maxExcessiveIoSamplesInWindow = %d, " +
+                "uptimeIntervalBetweenUptimeDataWriteMs = %d",
+                acceptableBytesWrittenPerSample,
+                acceptableFsyncCallsPerSample,
+                acceptableHoursPerOnePercentFlashWear,
+                activityHandlerForFlashWearChanges,
+                intentReceiverForUnacceptableIoMetrics,
+                ioStatsNumSamplesToStore,
+                ioStatsRefreshRateMs,
+                maxExcessiveIoSamplesInWindow,
+                uptimeIntervalBetweenUptimeDataWriteMs);
+        }
+    }
 }
diff --git a/tests/DefaultStorageMonitoringCompanionApp/AndroidManifest.xml b/tests/DefaultStorageMonitoringCompanionApp/AndroidManifest.xml
index 45380ce..a676e18 100644
--- a/tests/DefaultStorageMonitoringCompanionApp/AndroidManifest.xml
+++ b/tests/DefaultStorageMonitoringCompanionApp/AndroidManifest.xml
@@ -31,6 +31,16 @@
         android:exported="true"
         android:permission="android.car.permission.STORAGE_MONITORING">
     </activity>
+
+    <receiver
+        android:name=".ExcessiveIoIntentReceiver"
+        android:exported="true"
+        android:permission="android.car.permission.STORAGE_MONITORING">
+      <intent-filter>
+        <action android:name="android.car.storagemonitoring.EXCESSIVE_IO"/>
+      </intent-filter>
+    </receiver>
+
   </application>
 
 </manifest>
diff --git a/tests/DefaultStorageMonitoringCompanionApp/src/com/google/android/car/defaultstoragemonitoringcompanionapp/ExcessiveIoIntentReceiver.java b/tests/DefaultStorageMonitoringCompanionApp/src/com/google/android/car/defaultstoragemonitoringcompanionapp/ExcessiveIoIntentReceiver.java
new file mode 100644
index 0000000..2190bbd
--- /dev/null
+++ b/tests/DefaultStorageMonitoringCompanionApp/src/com/google/android/car/defaultstoragemonitoringcompanionapp/ExcessiveIoIntentReceiver.java
@@ -0,0 +1,20 @@
+package com.google.android.car.defaultstoragemonitoringcompanionapp;
+
+import android.car.storagemonitoring.CarStorageMonitoringManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class ExcessiveIoIntentReceiver extends BroadcastReceiver {
+    private final static String TAG = ExcessiveIoIntentReceiver.class.getSimpleName();
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (CarStorageMonitoringManager.INTENT_EXCESSIVE_IO.equals(intent.getAction())) {
+            Log.d(TAG, "excessive I/O activity detected.");
+        } else {
+            Log.w(TAG, "unexpected intent received: " + intent);
+        }
+    }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml b/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml
index b310c3d..cf98695 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/storagewear.xml
@@ -45,6 +45,12 @@
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:text="Write 1K"/>
+        <Button
+            android:id="@+id/perform_fsync"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="Call fsync()"/>
     </LinearLayout>
     <TextView
         android:id="@+id/free_disk_space"
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagelifetime/StorageLifetimeFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagelifetime/StorageLifetimeFragment.java
index a229904..69d4af4 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagelifetime/StorageLifetimeFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/storagelifetime/StorageLifetimeFragment.java
@@ -26,6 +26,7 @@
 import android.os.Bundle;
 import android.os.StatFs;
 import android.support.v4.app.Fragment;
+import android.system.ErrnoException;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -35,6 +36,9 @@
 import android.widget.TextView;
 import com.google.android.car.kitchensink.KitchenSinkActivity;
 import com.google.android.car.kitchensink.R;
+import java.io.FileDescriptor;
+import java.nio.ByteBuffer;
+import libcore.io.Libcore;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
@@ -44,6 +48,9 @@
 import java.security.SecureRandom;
 import java.util.List;
 
+import static android.system.OsConstants.O_APPEND;
+import static android.system.OsConstants.O_RDWR;
+
 public class StorageLifetimeFragment extends Fragment {
     private static final String FILE_NAME = "storage.bin";
     private static final String TAG = "CAR.STORAGELIFETIME.KS";
@@ -114,14 +121,19 @@
         }
     }
 
+    private Path getFilePath() throws IOException {
+        Path filePath = new File(mActivity.getFilesDir(), FILE_NAME).toPath();
+        if (Files.notExists(filePath)) {
+            Files.createFile(filePath);
+        }
+        return filePath;
+    }
+
     private void writeBytesToFile(int size) {
         try {
+            final Path filePath = getFilePath();
             byte[] data = new byte[size];
             SecureRandom.getInstanceStrong().nextBytes(data);
-            Path filePath = new File(mActivity.getFilesDir(), FILE_NAME).toPath();
-            if (Files.notExists(filePath)) {
-                Files.createFile(filePath);
-            }
             Files.write(filePath,
                 data,
                 StandardOpenOption.APPEND);
@@ -130,6 +142,24 @@
         }
     }
 
+    private void fsyncFile() {
+        try {
+            final Path filePath = getFilePath();
+            FileDescriptor fd = Libcore.os.open(filePath.toString(), O_APPEND | O_RDWR, 0);
+            if (!fd.valid()) {
+                Log.w(TAG, "file descriptor is invalid");
+                return;
+            }
+            // fill byteBuffer with arbitrary data in order to make an fsync() meaningful
+            ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[] {101, 110, 114, 105, 99, 111});
+            Libcore.os.write(fd, byteBuffer);
+            Libcore.os.fsync(fd);
+            Libcore.os.close(fd);
+        } catch (ErrnoException | IOException e) {
+            Log.w(TAG, "could not fsync data", e);
+        }
+    }
+
     @Nullable
     @Override
     public View onCreateView(
@@ -149,6 +179,9 @@
         view.findViewById(R.id.write_one_megabyte).setOnClickListener(
             v -> writeBytesToFile(MEGABYTE));
 
+        view.findViewById(R.id.perform_fsync).setOnClickListener(
+            v -> fsyncFile());
+
         return view;
     }
 
diff --git a/tests/carservice_test/AndroidManifest.xml b/tests/carservice_test/AndroidManifest.xml
index 21d8a75..d290e52 100644
--- a/tests/carservice_test/AndroidManifest.xml
+++ b/tests/carservice_test/AndroidManifest.xml
@@ -48,7 +48,7 @@
         <activity android:name="com.android.car.SystemActivityMonitoringServiceTest$BlockingActivity"
                   android:taskAffinity="com.android.car.carservicetest.block"/>
 
-        <receiver android:name=".CarStorageMonitoringBroadcastReceiver"
+        <receiver android:name="com.android.car.CarStorageMonitoringBroadcastReceiver"
             android:exported="true"
             android:permission="android.car.permission.STORAGE_MONITORING">
             <intent-filter>