Add ClusterHomeManager#sendHeartbeat
Bug: 285415531
Test: atest ClusterHomeServiceUnitTest ClusterHalServiceTest
Change-Id: I60ee9b8a489dfe1510787c0b9b24a7acd833dd6c
diff --git a/car-lib/src/android/car/cluster/ClusterHomeManager.java b/car-lib/src/android/car/cluster/ClusterHomeManager.java
index b1f309f..111da5a 100644
--- a/car-lib/src/android/car/cluster/ClusterHomeManager.java
+++ b/car-lib/src/android/car/cluster/ClusterHomeManager.java
@@ -367,6 +367,21 @@
}
}
+ /**
+ * Sends a heartbeat to ClusterOS.
+ * @param epochTimeNs the current time
+ * @param appMetadata the application specific metadata which will be delivered with
+ * the heartbeat.
+ */
+ @RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
+ public void sendHeartbeat(long epochTimeNs, @Nullable byte[] appMetadata) {
+ try {
+ mService.sendHeartbeat(epochTimeNs, appMetadata);
+ } catch (RemoteException e) {
+ handleRemoteExceptionFromCarService(e);
+ }
+ }
+
@Override
protected void onCarDisconnected() {
mStateListeners.clear();
diff --git a/car-lib/src/android/car/cluster/IClusterHomeService.aidl b/car-lib/src/android/car/cluster/IClusterHomeService.aidl
index c5c589b..582eb27 100644
--- a/car-lib/src/android/car/cluster/IClusterHomeService.aidl
+++ b/car-lib/src/android/car/cluster/IClusterHomeService.aidl
@@ -61,4 +61,6 @@
void registerClusterNavigationStateListener(in IClusterNavigationStateListener listener) = 9;
/** Unregisters a cluster navigation state listener. */
void unregisterClusterNavigationStateListener(in IClusterNavigationStateListener listener) = 10;
+ /** Sends a heartbeat. */
+ void sendHeartbeat(long epochTimeNs, in byte[] appMetadata) = 11;
}
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index 331f397..c9794cd 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -437,7 +437,7 @@
t, ClusterHomeService.class,
() -> new ClusterHomeService(serviceContext, mHal.getClusterHal(),
mClusterNavigationService, mCarOccupantZoneService,
- mFixedActivityService), allServices);
+ mFixedActivityService, mCarActivityService), allServices);
} else {
Slogf.w(TAG, "Can't init ClusterHomeService, since Old cluster service is running");
mClusterHomeService = null;
diff --git a/service/src/com/android/car/cluster/ClusterHomeService.java b/service/src/com/android/car/cluster/ClusterHomeService.java
index d4abe37..a9ef031 100644
--- a/service/src/com/android/car/cluster/ClusterHomeService.java
+++ b/service/src/com/android/car/cluster/ClusterHomeService.java
@@ -23,11 +23,13 @@
import static com.android.car.hal.ClusterHalService.DISPLAY_ON;
import static com.android.car.hal.ClusterHalService.DONT_CARE;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
+import static com.android.car.internal.common.CommonConstants.EMPTY_BYTE_ARRAY;
import android.app.ActivityOptions;
import android.car.Car;
import android.car.CarOccupantZoneManager;
import android.car.ICarOccupantZoneCallback;
+import android.car.builtin.app.TaskInfoHelper;
import android.car.builtin.os.UserManagerHelper;
import android.car.builtin.util.Slogf;
import android.car.cluster.ClusterHomeManager;
@@ -56,6 +58,7 @@
import com.android.car.CarOccupantZoneService;
import com.android.car.CarServiceBase;
import com.android.car.R;
+import com.android.car.am.CarActivityService;
import com.android.car.am.FixedActivityService;
import com.android.car.hal.ClusterHalService;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
@@ -76,6 +79,7 @@
private final ClusterNavigationService mClusterNavigationService;
private final CarOccupantZoneService mOccupantZoneService;
private final FixedActivityService mFixedActivityService;
+ private final CarActivityService mCarActivityService;
private final ComponentName mClusterHomeActivity;
private boolean mServiceEnabled;
@@ -88,6 +92,7 @@
private int mUiType = ClusterHomeManager.UI_TYPE_CLUSTER_HOME;
private Intent mLastIntent;
private int mLastIntentUserId = UserManagerHelper.USER_SYSTEM;
+ private volatile boolean mClusterActivityVisible;
private final RemoteCallbackList<IClusterStateListener> mClientListeners =
new RemoteCallbackList<>();
@@ -98,12 +103,14 @@
public ClusterHomeService(Context context, ClusterHalService clusterHalService,
ClusterNavigationService navigationService,
CarOccupantZoneService occupantZoneService,
- FixedActivityService fixedActivityService) {
+ FixedActivityService fixedActivityService,
+ CarActivityService carActivityService) {
mContext = context;
mClusterHalService = clusterHalService;
mClusterNavigationService = navigationService;
mOccupantZoneService = occupantZoneService;
mFixedActivityService = fixedActivityService;
+ mCarActivityService = carActivityService;
mClusterHomeActivity = ComponentName.unflattenFromString(
mContext.getString(R.string.config_clusterHomeActivity));
mLastIntent = new Intent(ACTION_MAIN).setComponent(mClusterHomeActivity);
@@ -134,6 +141,10 @@
mClusterNavigationService.setClusterServiceCallback(this);
mOccupantZoneService.registerCallback(mOccupantZoneCallback);
+
+ if (mClusterHalService.isHeartbeatSupported()) {
+ mCarActivityService.registerActivityLaunchListener(mActivityLaunchListener);
+ }
initClusterDisplay();
}
@@ -183,6 +194,9 @@
@Override
public void release() {
Slogf.d(TAG, "releaseClusterHomeService");
+ if (mClusterHalService.isHeartbeatSupported()) {
+ mCarActivityService.unregisterActivityLaunchListener(mActivityLaunchListener);
+ }
mOccupantZoneService.unregisterCallback(mOccupantZoneCallback);
mClusterHalService.setCallback(null);
mClusterNavigationService.setClusterServiceCallback(null);
@@ -190,6 +204,18 @@
mClientNavigationListeners.kill();
}
+ private final CarActivityService.ActivityLaunchListener mActivityLaunchListener =
+ (topTask) -> {
+ if (TaskInfoHelper.getDisplayId(topTask) != mClusterDisplayId) return;
+ if (!mLastIntent.getComponent().equals(topTask.topActivity)) {
+ mClusterActivityVisible = false;
+ return;
+ }
+
+ // TODO: b/285415531 - Install TPL and TPL decides the visibility.
+ mClusterActivityVisible = true;
+ };
+
@Override
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
public void dump(IndentingPrintWriter writer) {
@@ -383,6 +409,15 @@
if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
return createClusterState();
}
+
+ @Override
+ public void sendHeartbeat(long epochTimeNs, byte[] appMetadata) {
+ enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
+ if (appMetadata == null) {
+ appMetadata = EMPTY_BYTE_ARRAY;
+ }
+ mClusterHalService.sendHeartbeat(epochTimeNs, mClusterActivityVisible ? 1 : 0, appMetadata);
+ }
// IClusterHomeService ends
private void enforcePermission(String permissionName) {
diff --git a/service/src/com/android/car/hal/ClusterHalService.java b/service/src/com/android/car/hal/ClusterHalService.java
index 17a9f84..3086e89 100644
--- a/service/src/com/android/car/hal/ClusterHalService.java
+++ b/service/src/com/android/car/hal/ClusterHalService.java
@@ -17,14 +17,16 @@
package com.android.car.hal;
import static android.car.VehiclePropertyIds.CLUSTER_DISPLAY_STATE;
+import static android.car.VehiclePropertyIds.CLUSTER_HEARTBEAT;
import static android.car.VehiclePropertyIds.CLUSTER_NAVIGATION_STATE;
import static android.car.VehiclePropertyIds.CLUSTER_REPORT_STATE;
import static android.car.VehiclePropertyIds.CLUSTER_REQUEST_DISPLAY;
import static android.car.VehiclePropertyIds.CLUSTER_SWITCH_UI;
-import static com.android.car.internal.common.CommonConstants.EMPTY_FLOAT_ARRAY;
-import static com.android.car.internal.common.CommonConstants.EMPTY_LONG_ARRAY;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
+import static com.android.car.internal.common.CommonConstants.EMPTY_FLOAT_ARRAY;
+import static com.android.car.internal.common.CommonConstants.EMPTY_INT_ARRAY;
+import static com.android.car.internal.common.CommonConstants.EMPTY_LONG_ARRAY;
import android.annotation.NonNull;
import android.car.builtin.util.Slogf;
@@ -90,6 +92,7 @@
CLUSTER_REPORT_STATE,
CLUSTER_REQUEST_DISPLAY,
CLUSTER_NAVIGATION_STATE,
+ CLUSTER_HEARTBEAT,
};
private static final int[] CORE_PROPERTIES = new int[]{
@@ -116,6 +119,7 @@
// Whether all CORE_PROPERTIES are available.
private volatile boolean mIsCoreSupported;
private volatile boolean mIsNavigationStateSupported;
+ private volatile boolean mIsHeartbeatSupported;
private final HalPropValueBuilder mPropValueBuilder;
@@ -179,8 +183,10 @@
}
}
mIsNavigationStateSupported = supportedProperties.indexOf(CLUSTER_NAVIGATION_STATE) >= 0;
- Slogf.d(TAG, "takeProperties: coreSupported=%s, navigationStateSupported=%s",
- mIsCoreSupported, mIsNavigationStateSupported);
+ mIsHeartbeatSupported = supportedProperties.indexOf(CLUSTER_HEARTBEAT) >= 0;
+ Slogf.d(TAG, "takeProperties: coreSupported=%s, navigationStateSupported=%s, "
+ + "heartbeatSupported=%s",
+ mIsCoreSupported, mIsNavigationStateSupported, mIsHeartbeatSupported);
}
@VisibleForTesting
@@ -201,6 +207,10 @@
return mIsNavigationStateSupported;
}
+ public boolean isHeartbeatSupported() {
+ return mIsHeartbeatSupported;
+ }
+
@Override
public void onHalEvents(List<HalPropValue> values) {
Slogf.d(TAG, "handleHalEvents(): %s", values);
@@ -339,6 +349,26 @@
send(request);
}
+ /**
+ * Sends a heartbeat to ClusterOS
+ * @param epochTimeNs the current time
+ * @param visibility 0 means invisible and 1 means visible.
+ * @param appMetadata the application specific metadata which will be delivered with
+ * the heartbeat.
+ */
+ public void sendHeartbeat(long epochTimeNs, long visibility, byte[] appMetadata) {
+ long[] longValues = new long[]{
+ epochTimeNs,
+ visibility
+ };
+ HalPropValue request = mPropValueBuilder.build(CLUSTER_HEARTBEAT,
+ /* areaId= */ 0, SystemClock.elapsedRealtime(), VehiclePropertyStatus.AVAILABLE,
+ /* int32Values= */ EMPTY_INT_ARRAY, /* floatValues= */ EMPTY_FLOAT_ARRAY,
+ /* int64Values= */ longValues, /* stringValue= */ "",
+ /* byteValues= */ appMetadata);
+ send(request);
+ }
+
private void send(HalPropValue request) {
try {
mHal.set(request);
@@ -354,5 +384,6 @@
writer.println("mServiceMode: " + mServiceMode);
writer.println("mIsCoreSupported: " + mIsCoreSupported);
writer.println("mIsNavigationStateSupported: " + mIsNavigationStateSupported);
+ writer.println("mIsHeartbeatSupported: " + mIsHeartbeatSupported);
}
}
diff --git a/tests/carservice_unit_test/res/raw/car_hidden_apis.txt b/tests/carservice_unit_test/res/raw/car_hidden_apis.txt
index 14e13a8..42561a4 100644
--- a/tests/carservice_unit_test/res/raw/car_hidden_apis.txt
+++ b/tests/carservice_unit_test/res/raw/car_hidden_apis.txt
@@ -230,6 +230,7 @@
android.car.cluster ClusterHomeManager void registerClusterStateListener(Executor executor, ClusterStateListener callback)
android.car.cluster ClusterHomeManager void reportState(int uiTypeMain, int uiTypeSub, byte[] uiAvailability)
android.car.cluster ClusterHomeManager void requestDisplay(int uiType)
+android.car.cluster ClusterHomeManager void sendHeartbeat(long epochTimeNs, byte[] appMetadata)
android.car.cluster ClusterHomeManager void stopFixedActivityMode()
android.car.cluster ClusterHomeManager void unregisterClusterNavigationStateListener(ClusterNavigationStateListener callback)
android.car.cluster ClusterHomeManager void unregisterClusterStateListener(ClusterStateListener callback)
diff --git a/tests/carservice_unit_test/src/com/android/car/cluster/ClusterHomeServiceUnitTest.java b/tests/carservice_unit_test/src/com/android/car/cluster/ClusterHomeServiceUnitTest.java
index 7c0f6f5..9240396 100644
--- a/tests/carservice_unit_test/src/com/android/car/cluster/ClusterHomeServiceUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/cluster/ClusterHomeServiceUnitTest.java
@@ -21,6 +21,8 @@
import static android.car.cluster.ClusterHomeManager.UI_TYPE_CLUSTER_NONE;
import static android.car.navigation.CarNavigationInstrumentCluster.CLUSTER_TYPE_IMAGE_CODES_ONLY;
+import static com.android.car.internal.common.CommonConstants.EMPTY_BYTE_ARRAY;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -30,6 +32,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.car.CarOccupantZoneManager;
import android.car.ICarOccupantZoneCallback;
@@ -51,6 +54,8 @@
import android.view.Display;
import com.android.car.CarOccupantZoneService;
+import com.android.car.am.CarActivityService;
+import com.android.car.am.CarActivityService.ActivityLaunchListener;
import com.android.car.am.FixedActivityService;
import com.android.car.cluster.ClusterNavigationService.ContextOwner;
import com.android.car.hal.ClusterHalService;
@@ -88,6 +93,8 @@
@Mock
private FixedActivityService mFixedActivityService;
@Mock
+ private CarActivityService mCarActivityService;
+ @Mock
private DisplayManager mDisplayManager;
@Mock
private Display mClusterDisplay;
@@ -96,6 +103,7 @@
private int mClusterStateChanges;
private byte[] mNavigationState;
private ICarOccupantZoneCallback mOccupantZoneCallback;
+ private ActivityLaunchListener mActivityLaunchListener;
private IClusterStateListener mClusterStateListener;
private IClusterNavigationStateListener mClusterNavigationStateListener;
@@ -134,15 +142,23 @@
when(mClusterHalService.isServiceEnabled()).thenReturn(true);
when(mClusterHalService.isLightMode()).thenReturn(false);
when(mClusterHalService.isNavigationStateSupported()).thenReturn(true);
+ when(mClusterHalService.isHeartbeatSupported()).thenReturn(true);
when(mDisplayManager.getDisplay(CLUSTER_DISPLAY_ID)).thenReturn(mClusterDisplay);
doAnswer(invocation -> {
Point size = (Point) invocation.getArgument(0);
size.set(CLUSTER_WIDTH, CLUSTER_HEIGHT);
return null;
}).when(mClusterDisplay).getRealSize(any(Point.class));
+ doAnswer(invocation -> {
+ mActivityLaunchListener = invocation.getArgument(0);
+ assertThat(mActivityLaunchListener).isNotNull();
+ return null;
+ }).when(mCarActivityService).registerActivityLaunchListener(
+ any(ActivityLaunchListener.class));
mClusterHomeService = new ClusterHomeService(mContext, mClusterHalService,
- mNavigationService, mOccupantZoneService, mFixedActivityService);
+ mNavigationService, mOccupantZoneService, mFixedActivityService,
+ mCarActivityService);
mClusterHomeService.init();
}
@@ -350,4 +366,31 @@
verify(mFixedActivityService).stopFixedActivityMode(eq(CLUSTER_DISPLAY_ID));
}
+
+ @Test
+ public void sendHeartbeat_SimpleCase() {
+ byte[] appMetadata = null;
+ long epochTimeNs = 123456789;
+
+ mClusterHomeService.sendHeartbeat(epochTimeNs, appMetadata);
+
+ verify(mClusterHalService).sendHeartbeat(
+ eq(epochTimeNs), eq(/* visibility= */ 0L), eq(EMPTY_BYTE_ARRAY));
+ }
+
+ @Test
+ public void sendHeartbeat_ReflectsClusterAppVisibility() {
+ byte[] appMetadata = new byte[] {(byte) 3, (byte) 2, (byte) 0, (byte) 1};
+ long epochTimeNs = 123456789;
+ ActivityManager.RunningTaskInfo clusterTask = new ActivityManager.RunningTaskInfo();
+ clusterTask.displayId = CLUSTER_DISPLAY_ID;
+ clusterTask.topActivity = mClusterHomeActivity;
+ clusterTask.isVisible = true;
+ mActivityLaunchListener.onActivityLaunch(clusterTask);
+
+ mClusterHomeService.sendHeartbeat(epochTimeNs, appMetadata);
+
+ verify(mClusterHalService).sendHeartbeat(
+ eq(epochTimeNs), eq(/* visibility= */ 1L), eq(appMetadata));
+ }
}
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/ClusterHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/ClusterHalServiceTest.java
index 7edf86f..47c7863 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/ClusterHalServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/ClusterHalServiceTest.java
@@ -17,6 +17,7 @@
package com.android.car.hal;
import static android.car.VehiclePropertyIds.CLUSTER_DISPLAY_STATE;
+import static android.car.VehiclePropertyIds.CLUSTER_HEARTBEAT;
import static android.car.VehiclePropertyIds.CLUSTER_NAVIGATION_STATE;
import static android.car.VehiclePropertyIds.CLUSTER_REPORT_STATE;
import static android.car.VehiclePropertyIds.CLUSTER_REQUEST_DISPLAY;
@@ -528,4 +529,21 @@
verify(mVehicleHal, times(0)).set(mPropCaptor.capture());
}
+
+ @Test
+ public void testSendHeartbeat() {
+ long epochTimeNs = 123456789;
+ long visibility = 1;
+ byte[] appMetadata = new byte[]{3, 2, 0, 1};
+ mClusterHalService.sendHeartbeat(epochTimeNs, visibility, appMetadata);
+
+ verify(mVehicleHal).set(mPropCaptor.capture());
+ HalPropValue prop = mPropCaptor.getValue();
+ assertThat(prop.getPropId()).isEqualTo(CLUSTER_HEARTBEAT);
+ assertThat(prop.getInt64ContainerArray()).asList()
+ .containsExactly((long) epochTimeNs, (long) visibility);
+ assertThat(prop.getByteArray()).asList()
+ .containsExactly((byte) 3, (byte) 2, (byte) 0, (byte) 1);
+ }
+
}