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;