Add listener registration for I/O activity deltas to CarStorageMonitoringManager

Test: bit CarServiceTest:com.android.car.CarStorageMonitoringTest
Bug: 32512551
Bug: 65846699
Change-Id: I74f5d1ad3fe03329a34aef0bc32feb2149077c60
diff --git a/car-lib/api/system-current.txt b/car-lib/api/system-current.txt
index bf0cad0..88267ca 100644
--- a/car-lib/api/system-current.txt
+++ b/car-lib/api/system-current.txt
@@ -1017,12 +1017,18 @@
     method public int getPreEolIndicatorStatus() throws android.car.CarNotConnectedException;
     method public android.car.storagemonitoring.WearEstimate getWearEstimate() throws android.car.CarNotConnectedException;
     method public java.util.List<android.car.storagemonitoring.WearEstimateChange> getWearEstimateHistory() throws android.car.CarNotConnectedException;
+    method public void registerListener(android.car.storagemonitoring.CarStorageMonitoringManager.UidIoStatsListener) throws android.car.CarNotConnectedException;
+    method public void unregisterListener(android.car.storagemonitoring.CarStorageMonitoringManager.UidIoStatsListener) throws android.car.CarNotConnectedException;
     field public static final int PRE_EOL_INFO_NORMAL = 1; // 0x1
     field public static final int PRE_EOL_INFO_UNKNOWN = 0; // 0x0
     field public static final int PRE_EOL_INFO_URGENT = 3; // 0x3
     field public static final int PRE_EOL_INFO_WARNING = 2; // 0x2
   }
 
+  public static abstract interface CarStorageMonitoringManager.UidIoStatsListener {
+    method public abstract void onSnapshot(android.car.storagemonitoring.UidIoStatsDelta);
+  }
+
   public final class UidIoRecord {
     ctor public UidIoRecord(int, long, long, long, long, long, long, long, long, long, long);
     field public final long background_fsync;
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index 6101797..31b8c4e 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -675,7 +675,7 @@
             case BLUETOOTH_SERVICE:
                 manager = new CarBluetoothManager(binder, mContext);
             case STORAGE_MONITORING_SERVICE:
-                manager = new CarStorageMonitoringManager(binder);
+                manager = new CarStorageMonitoringManager(binder, mEventHandler);
         }
         return manager;
     }
diff --git a/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java b/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
index cc32acc..057bdc2 100644
--- a/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
+++ b/car-lib/src/android/car/storagemonitoring/CarStorageMonitoringManager.java
@@ -20,10 +20,16 @@
 import android.car.Car;
 import android.car.CarManagerBase;
 import android.car.CarNotConnectedException;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.util.Log;
+import com.android.car.internal.SingleMessageHandler;
+import java.lang.ref.WeakReference;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import static android.car.CarApiUtil.checkCarNotConnectedExceptionFromCarService;
 
@@ -34,7 +40,32 @@
  */
 @SystemApi
 public final class CarStorageMonitoringManager implements CarManagerBase {
+    private static final String TAG = CarStorageMonitoringManager.class.getSimpleName();
+    private static final int MSG_IO_STATS_EVENT = 0;
+
     private final ICarStorageMonitoring mService;
+    private ListenerToService mListenerToService;
+    private final SingleMessageHandler<UidIoStatsDelta> mMessageHandler;
+    private final Set<UidIoStatsListener> mListeners = new HashSet<>();
+
+    public interface UidIoStatsListener {
+        void onSnapshot(UidIoStatsDelta snapshot);
+    }
+    private static final class ListenerToService extends IUidIoStatsListener.Stub {
+        private final WeakReference<CarStorageMonitoringManager> mManager;
+
+        ListenerToService(CarStorageMonitoringManager manager) {
+            mManager = new WeakReference<>(manager);
+        }
+
+        @Override
+        public void onSnapshot(UidIoStatsDelta snapshot) {
+            CarStorageMonitoringManager manager = mManager.get();
+            if (manager != null) {
+                manager.mMessageHandler.sendEvents(Collections.singletonList(snapshot));
+            }
+        }
+    }
 
     public static final int PRE_EOL_INFO_UNKNOWN = 0;
     public static final int PRE_EOL_INFO_NORMAL = 1;
@@ -44,8 +75,16 @@
     /**
      * @hide
      */
-    public CarStorageMonitoringManager(IBinder service) {
+    public CarStorageMonitoringManager(IBinder service, Handler handler) {
         mService = ICarStorageMonitoring.Stub.asInterface(service);
+        mMessageHandler = new SingleMessageHandler<UidIoStatsDelta>(handler, MSG_IO_STATS_EVENT) {
+            @Override
+            protected void handleEvent(UidIoStatsDelta event) {
+                for (UidIoStatsListener listener : mListeners) {
+                    listener.onSnapshot(event);
+                }
+            }
+        };
     }
 
     /**
@@ -184,4 +223,49 @@
         }
         return Collections.emptyList();
     }
+
+    /**
+     * This method registers a new listener to receive I/O stats deltas.
+     *
+     * The system periodically gathers I/O activity metrics and computes a delta of such
+     * activity. Registered listeners will receive those deltas as they are available.
+     *
+     * The timing of availability of the deltas is configurable by the OEM.
+     */
+    @RequiresPermission(value=Car.PERMISSION_STORAGE_MONITORING)
+    public void registerListener(UidIoStatsListener listener) throws CarNotConnectedException {
+        try {
+            if (mListeners.isEmpty()) {
+                if (mListenerToService == null) {
+                    mListenerToService = new ListenerToService(this);
+                }
+                mService.registerListener(mListenerToService);
+            }
+            mListeners.add(listener);
+        } catch (IllegalStateException e) {
+            checkCarNotConnectedExceptionFromCarService(e);
+        } catch (RemoteException e) {
+            throw new CarNotConnectedException();
+        }
+    }
+
+    /**
+     * This method removes a registered listener of I/O stats deltas.
+     */
+    @RequiresPermission(value=Car.PERMISSION_STORAGE_MONITORING)
+    public void unregisterListener(UidIoStatsListener listener) throws CarNotConnectedException {
+        try {
+            if (!mListeners.remove(listener)) {
+                return;
+            }
+            if (mListeners.isEmpty()) {
+                mService.unregisterListener(mListenerToService);
+                mListenerToService = null;
+            }
+        } catch (IllegalStateException e) {
+            checkCarNotConnectedExceptionFromCarService(e);
+        } catch (RemoteException e) {
+            throw new CarNotConnectedException();
+        }
+    }
 }
diff --git a/car-lib/src/android/car/storagemonitoring/ICarStorageMonitoring.aidl b/car-lib/src/android/car/storagemonitoring/ICarStorageMonitoring.aidl
index d8183d6..ab10800 100644
--- a/car-lib/src/android/car/storagemonitoring/ICarStorageMonitoring.aidl
+++ b/car-lib/src/android/car/storagemonitoring/ICarStorageMonitoring.aidl
@@ -16,6 +16,7 @@
 
 package android.car.storagemonitoring;
 
+import android.car.storagemonitoring.IUidIoStatsListener;
 import android.car.storagemonitoring.UidIoStats;
 import android.car.storagemonitoring.UidIoStatsDelta;
 import android.car.storagemonitoring.WearEstimate;
@@ -52,4 +53,15 @@
    * Return the I/O stats deltas currently known to the service.
    */
   List<UidIoStatsDelta> getIoStatsDeltas() = 6;
+
+  /**
+   * Register a new listener to receive new I/O activity deltas as they are generated.
+   */
+  void registerListener(IUidIoStatsListener listener) = 7;
+
+  /**
+   * Remove a listener registration, terminating delivery of I/O activity deltas to it.
+   */
+  void unregisterListener(IUidIoStatsListener listener) = 8;
+
 }
diff --git a/car-lib/src/android/car/storagemonitoring/IUidIoStatsListener.aidl b/car-lib/src/android/car/storagemonitoring/IUidIoStatsListener.aidl
new file mode 100644
index 0000000..acdc55d
--- /dev/null
+++ b/car-lib/src/android/car/storagemonitoring/IUidIoStatsListener.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2017 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 android.car.storagemonitoring;
+
+import android.car.storagemonitoring.UidIoStatsDelta;
+
+/** @hide */
+oneway interface IUidIoStatsListener {
+    /**
+     * Called each time a new uid_io activity snapshot is generated by the service.
+     *
+     * The interval at which new snapshots are generated is an OEM-configurable property.
+     */
+    void onSnapshot(in UidIoStatsDelta snapshot) = 0;
+}
diff --git a/car-lib/src/com/android/car/internal/SingleMessageHandler.java b/car-lib/src/com/android/car/internal/SingleMessageHandler.java
index f659f17..f61309c 100644
--- a/car-lib/src/com/android/car/internal/SingleMessageHandler.java
+++ b/car-lib/src/com/android/car/internal/SingleMessageHandler.java
@@ -36,6 +36,10 @@
         mHandler = new Handler(looper, this);
     }
 
+    public SingleMessageHandler(Handler handler, int handledMessage) {
+        this(handler.getLooper(), handledMessage);
+    }
+
     protected abstract void handleEvent(EventType event);
 
     @Override
diff --git a/service/src/com/android/car/CarStorageMonitoringService.java b/service/src/com/android/car/CarStorageMonitoringService.java
index 819c123..1fc6384 100644
--- a/service/src/com/android/car/CarStorageMonitoringService.java
+++ b/service/src/com/android/car/CarStorageMonitoringService.java
@@ -17,6 +17,7 @@
 package com.android.car;
 
 import android.car.Car;
+import android.car.storagemonitoring.IUidIoStatsListener;
 import android.car.storagemonitoring.ICarStorageMonitoring;
 import android.car.storagemonitoring.UidIoRecord;
 import android.car.storagemonitoring.UidIoStats;
@@ -28,6 +29,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
 import android.util.JsonWriter;
 import android.util.Log;
 import android.util.SparseArray;
@@ -50,6 +53,7 @@
 import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 import org.json.JSONException;
 
 public class CarStorageMonitoringService extends ICarStorageMonitoring.Stub
@@ -69,6 +73,7 @@
     private final SystemInterface mSystemInterface;
     private final UidIoStatsProvider mUidIoStatsProvider;
     private final SlidingWindow<UidIoStatsDelta> mIoStatsSamples;
+    private final RemoteCallbackList<IUidIoStatsListener> mListeners;
     private final Object mIoStatsSamplesLock = new Object();
 
     private final CarPermission mStorageMonitoringPermission;
@@ -96,6 +101,7 @@
             resources.getInteger(R.integer.ioStatsNumSamplesToStore));
         systemInterface.scheduleActionForBootCompleted(this::doInitServiceIfNeeded,
             Duration.ofSeconds(10));
+        mListeners = new RemoteCallbackList<>();
     }
 
     private static long getUptimeSnapshotIntervalMs() {
@@ -205,12 +211,15 @@
     }
 
     private void collectNewIoMetrics() {
+        UidIoStatsDelta uidIoStatsDelta;
+
         mIoStatsTracker.update(loadNewIoStats());
         synchronized (mIoStatsSamplesLock) {
-            mIoStatsSamples.add(new UidIoStatsDelta(
-                    SparseArrayStream.valueStream(mIoStatsTracker.getCurrentSample())
-                        .collect(Collectors.toList()),
-                    mSystemInterface.getUptime()));
+            uidIoStatsDelta = new UidIoStatsDelta(
+                SparseArrayStream.valueStream(mIoStatsTracker.getCurrentSample())
+                    .collect(Collectors.toList()),
+                mSystemInterface.getUptime());
+            mIoStatsSamples.add(uidIoStatsDelta);
         }
 
         if (DBG) {
@@ -222,6 +231,21 @@
                     uidIoStats -> Log.d(TAG, "updated I/O stat data: " + uidIoStats));
             }
         }
+
+        dispatchNewIoEvent(uidIoStatsDelta);
+    }
+
+    private void dispatchNewIoEvent(UidIoStatsDelta delta) {
+        final int listenersCount = mListeners.beginBroadcast();
+        IntStream.range(0, listenersCount).forEach(
+            i -> {
+                try {
+                    mListeners.getBroadcastItem(i).onSnapshot(delta);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "failed to dispatch snapshot", e);
+                }
+            });
+        mListeners.finishBroadcast();
     }
 
     private synchronized void doInitServiceIfNeeded() {
@@ -372,4 +396,20 @@
             return mIoStatsSamples.stream().collect(Collectors.toList());
         }
     }
+
+    @Override
+    public void registerListener(IUidIoStatsListener listener) {
+        mStorageMonitoringPermission.assertGranted();
+        doInitServiceIfNeeded();
+
+        mListeners.register(listener);
+    }
+
+    @Override
+    public void unregisterListener(IUidIoStatsListener listener) {
+        mStorageMonitoringPermission.assertGranted();
+        // no need to initialize service if unregistering
+
+        mListeners.unregister(listener);
+    }
 }
diff --git a/tests/carservice_test/src/com/android/car/CarStorageMonitoringTest.java b/tests/carservice_test/src/com/android/car/CarStorageMonitoringTest.java
index 10c110e..285acbc 100644
--- a/tests/carservice_test/src/com/android/car/CarStorageMonitoringTest.java
+++ b/tests/carservice_test/src/com/android/car/CarStorageMonitoringTest.java
@@ -25,6 +25,7 @@
 import android.car.storagemonitoring.UidIoRecord;
 import android.car.storagemonitoring.WearEstimate;
 import android.car.storagemonitoring.WearEstimateChange;
+import android.os.SystemClock;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.util.JsonWriter;
 import android.util.Log;
@@ -436,6 +437,92 @@
         assertTrue(deltaRecord0.representsSameMetrics(newerRecord0.delta(newRecord0)));
     }
 
+    public void testEventDelivery() throws Exception {
+        final Duration eventDeliveryDeadline = Duration.ofSeconds(5);
+
+        UidIoRecord record = new UidIoRecord(0,
+            0,
+            100,
+            0,
+            75,
+            1,
+            0,
+            0,
+            0,
+            0,
+            0);
+
+        Listener listener1 = new Listener("listener1");
+        Listener listener2 = new Listener("listener2");
+
+        mCarStorageMonitoringManager.registerListener(listener1);
+        mCarStorageMonitoringManager.registerListener(listener2);
+
+        mMockStorageMonitoringInterface.addIoStatsRecord(record);
+        mMockTimeInterface.setUptime(500).tick();
+
+        assertTrue(listener1.waitForEvent(eventDeliveryDeadline));
+        assertTrue(listener2.waitForEvent(eventDeliveryDeadline));
+
+        UidIoStatsDelta event1 = listener1.reset();
+        UidIoStatsDelta event2 = listener2.reset();
+
+        assertEquals(event1, event2);
+        event1.getStats().forEach(stats -> assertTrue(stats.representsSameMetrics(record)));
+
+        mCarStorageMonitoringManager.unregisterListener(listener1);
+
+        mMockTimeInterface.setUptime(600).tick();
+        assertFalse(listener1.waitForEvent(eventDeliveryDeadline));
+        assertTrue(listener2.waitForEvent(eventDeliveryDeadline));
+    }
+
+    static final class Listener implements CarStorageMonitoringManager.UidIoStatsListener {
+        private final String mName;
+        private final Object mSync = new Object();
+
+        private UidIoStatsDelta mLastEvent = null;
+
+        Listener(String name) {
+            mName = name;
+        }
+
+        UidIoStatsDelta reset() {
+            synchronized (mSync) {
+                UidIoStatsDelta lastEvent = mLastEvent;
+                mLastEvent = null;
+                return lastEvent;
+            }
+        }
+
+        boolean waitForEvent(Duration duration) {
+            long start = SystemClock.elapsedRealtime();
+            long end = start + duration.toMillis();
+            synchronized (mSync) {
+                while (mLastEvent == null && SystemClock.elapsedRealtime() < end) {
+                    try {
+                        mSync.wait(10L);
+                    } catch (InterruptedException e) {
+                        // ignore
+                    }
+                }
+            }
+
+            return (mLastEvent != null);
+        }
+
+        @Override
+        public void onSnapshot(UidIoStatsDelta event) {
+            synchronized (mSync) {
+                Log.d(TAG, "listener " + mName + " received event " + event);
+                // We're going to hold a reference to this object
+                mLastEvent = event;
+                mSync.notify();
+            }
+        }
+
+    }
+
     static final class MockStorageMonitoringInterface implements StorageMonitoringInterface,
         WearInformationProvider {
         private WearInformation mWearInformation = null;