blob: 3086e89974fa7f5b844e16ba3a7a4831edadbd6b [file] [log] [blame]
/*
* Copyright (C) 2021 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.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.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;
import android.content.Context;
import android.graphics.Insets;
import android.graphics.Rect;
import android.hardware.automotive.vehicle.VehiclePropertyStatus;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import com.android.car.R;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.util.IntArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.List;
/**
* Translates HAL input events to higher-level semantic information.
*/
public final class ClusterHalService extends HalServiceBase {
private static final String TAG = ClusterHalService.class.getSimpleName();
// The value of config_clusterHomeServiceOnMode that are currently supported:
// 0: FULL mode. ClusterHomeService is enabled only when all CORE_PROPERTIES are available.
// 1: LIGHT mode. No properties need to be available. In this mode, all service methods that
// rely on core properties will throw IllegalStateException, regardless of whether the
// property is actually available or not.
private static final int CONFIG_CLUSTER_HOME_SERVICE_FULL_MODE = 0;
private static final int CONFIG_CLUSTER_HOME_SERVICE_LIGHT_MODE = 1;
public static final int DISPLAY_OFF = 0;
public static final int DISPLAY_ON = 1;
public static final int DONT_CARE = -1;
/**
* Interface to receive incoming Cluster HAL events.
*/
public interface ClusterHalEventCallback {
/**
* Called when CLUSTER_SWITCH_UI message is received.
*
* @param uiType uiType ClusterOS wants to switch to
*/
void onSwitchUi(int uiType);
/**
* Called when CLUSTER_DISPLAY_STATE message is received.
*
* @param onOff 0 - off, 1 - on
* @param bounds the area to render the cluster Activity in pixel
* @param insets Insets of the cluster display
*/
void onDisplayState(int onOff, Rect bounds, Insets insets);
};
private static final int[] SUPPORTED_PROPERTIES = new int[]{
CLUSTER_SWITCH_UI,
CLUSTER_DISPLAY_STATE,
CLUSTER_REPORT_STATE,
CLUSTER_REQUEST_DISPLAY,
CLUSTER_NAVIGATION_STATE,
CLUSTER_HEARTBEAT,
};
private static final int[] CORE_PROPERTIES = new int[]{
CLUSTER_SWITCH_UI,
CLUSTER_REPORT_STATE,
CLUSTER_DISPLAY_STATE,
CLUSTER_REQUEST_DISPLAY,
};
private static final int[] SUBSCRIBABLE_PROPERTIES = new int[]{
CLUSTER_SWITCH_UI,
CLUSTER_DISPLAY_STATE,
};
private final Object mLock = new Object();
@GuardedBy("mLock")
private ClusterHalEventCallback mCallback;
private final VehicleHal mHal;
// The value of config_clusterHomeServiceOnMode
private final int mServiceMode;
// Whether all CORE_PROPERTIES are available.
private volatile boolean mIsCoreSupported;
private volatile boolean mIsNavigationStateSupported;
private volatile boolean mIsHeartbeatSupported;
private final HalPropValueBuilder mPropValueBuilder;
public ClusterHalService(Context context, VehicleHal hal) {
mHal = hal;
mPropValueBuilder = hal.getHalPropValueBuilder();
mServiceMode = context.getResources().getInteger(R.integer.config_clusterHomeServiceMode);
}
/**
* {@inheritDoc}
*
* <p>Note that {@link #takeProperties} must be called before this method, so that available
* properties are correctly initialized.</p>
*/
@Override
public void init() {
Slogf.d(TAG, "initClusterHalService");
// Do not subscribe if the config is not FULL mode, or any core property is not available.
if (!isFullModeEnabled()) return;
for (int property : SUBSCRIBABLE_PROPERTIES) {
mHal.subscribeProperty(this, property);
}
}
@Override
public void release() {
Slogf.d(TAG, "releaseClusterHalService");
synchronized (mLock) {
mCallback = null;
}
}
/**
* Sets the event callback to receive Cluster HAL events.
*/
public void setCallback(ClusterHalEventCallback callback) {
synchronized (mLock) {
mCallback = callback;
}
}
@NonNull
@Override
public int[] getAllSupportedProperties() {
return SUPPORTED_PROPERTIES;
}
@Override
public void takeProperties(@NonNull Collection<HalPropConfig> properties) {
IntArray supportedProperties = new IntArray(properties.size());
for (HalPropConfig property : properties) {
supportedProperties.add(property.getPropId());
}
mIsCoreSupported = true;
for (int coreProperty : CORE_PROPERTIES) {
if (supportedProperties.indexOf(coreProperty) < 0) {
mIsCoreSupported = false;
break;
}
}
mIsNavigationStateSupported = supportedProperties.indexOf(CLUSTER_NAVIGATION_STATE) >= 0;
mIsHeartbeatSupported = supportedProperties.indexOf(CLUSTER_HEARTBEAT) >= 0;
Slogf.d(TAG, "takeProperties: coreSupported=%s, navigationStateSupported=%s, "
+ "heartbeatSupported=%s",
mIsCoreSupported, mIsNavigationStateSupported, mIsHeartbeatSupported);
}
@VisibleForTesting
boolean isFullModeEnabled() {
// In FULL mode, all core properties need to be available.
return mIsCoreSupported && (mServiceMode == CONFIG_CLUSTER_HOME_SERVICE_FULL_MODE);
}
public boolean isLightMode() {
return mServiceMode == CONFIG_CLUSTER_HOME_SERVICE_LIGHT_MODE;
}
public boolean isServiceEnabled() {
return isFullModeEnabled() || isLightMode();
}
public boolean isNavigationStateSupported() {
return mIsNavigationStateSupported;
}
public boolean isHeartbeatSupported() {
return mIsHeartbeatSupported;
}
@Override
public void onHalEvents(List<HalPropValue> values) {
Slogf.d(TAG, "handleHalEvents(): %s", values);
ClusterHalEventCallback callback;
synchronized (mLock) {
callback = mCallback;
}
if (callback == null || !isFullModeEnabled()) {
return;
}
for (HalPropValue value : values) {
switch (value.getPropId()) {
case CLUSTER_SWITCH_UI:
if (value.getInt32ValuesSize() < 1) {
Slogf.e(TAG, "received invalid CLUSTER_SWITCH_UI property from HAL, "
+ "expect at least 1 int value.");
break;
}
int uiType = value.getInt32Value(0);
callback.onSwitchUi(uiType);
break;
case CLUSTER_DISPLAY_STATE:
if (value.getInt32ValuesSize() < 9) {
Slogf.e(TAG, "received invalid CLUSTER_DISPLAY_STATE property from HAL, "
+ "expect at least 9 int value.");
break;
}
int onOff = value.getInt32Value(0);
Rect bounds = null;
if (hasNoDontCare(value, /* start= */ 1, /* length= */ 4, "bounds")) {
bounds =
new Rect(
value.getInt32Value(1), value.getInt32Value(2),
value.getInt32Value(3), value.getInt32Value(4));
}
Insets insets = null;
if (hasNoDontCare(value, /* start= */ 5, /* length= */ 4, "insets")) {
insets =
Insets.of(
value.getInt32Value(5), value.getInt32Value(6),
value.getInt32Value(7), value.getInt32Value(8));
}
callback.onDisplayState(onOff, bounds, insets);
break;
default:
Slogf.w(TAG, "received unsupported event from HAL: %s", value);
}
}
}
private static boolean hasNoDontCare(HalPropValue value, int start, int length,
String fieldName) {
int count = 0;
for (int i = start; i < start + length; ++i) {
if (value.getInt32Value(i) == DONT_CARE) {
++count;
}
}
if (count == 0) {
return true;
}
if (count != length) {
Slogf.w(TAG, "Don't care should be set in the whole %s.", fieldName);
}
return false;
}
/**
* Reports the current display state and ClusterUI state.
*
* @param onOff 0 - off, 1 - on
* @param bounds the area to render the cluster Activity in pixel
* @param insets Insets of the cluster display
* @param uiTypeMain uiType that ClusterHome tries to show in main area
* @param uiTypeSub uiType that ClusterHome tries to show in sub area
* @param uiAvailability the byte array to represent the availability of ClusterUI.
*/
public void reportState(int onOff, Rect bounds, Insets insets,
int uiTypeMain, int uiTypeSub, byte[] uiAvailability) {
if (!isFullModeEnabled()) {
throw new IllegalStateException(
"reportState: one or more core property is not supported on this device, "
+ "or the service is not in FULL mode");
}
int[] intValues = new int[]{
onOff,
bounds.left,
bounds.top,
bounds.right,
bounds.bottom,
insets.left,
insets.top,
insets.right,
insets.bottom,
uiTypeMain,
uiTypeSub
};
HalPropValue request = mPropValueBuilder.build(CLUSTER_REPORT_STATE,
/* areaId= */ 0, SystemClock.elapsedRealtime(), VehiclePropertyStatus.AVAILABLE,
/* int32Values= */ intValues, /* floatValues= */ EMPTY_FLOAT_ARRAY,
/* int64Values= */ EMPTY_LONG_ARRAY, /* stringValue= */ "",
/* byteValues= */ uiAvailability);
send(request);
}
/**
* Requests to turn the cluster display on to show some ClusterUI.
*
* @param uiType uiType that ClusterHome tries to show in main area
*/
public void requestDisplay(int uiType) {
if (!isFullModeEnabled()) {
throw new IllegalStateException("requestDisplay: one or more core property is "
+ "not supported on this device, or the service is not in FULL mode");
}
HalPropValue request = mPropValueBuilder.build(CLUSTER_REQUEST_DISPLAY,
/* areaId= */ 0, SystemClock.elapsedRealtime(), VehiclePropertyStatus.AVAILABLE,
/* value= */ uiType);
send(request);
}
/**
* Informs the current navigation state.
*
* @param navigateState the serialized message of {@code NavigationStateProto}
*/
public void sendNavigationState(byte[] navigateState) {
if (!isNavigationStateSupported()) {
return;
}
HalPropValue request = mPropValueBuilder.build(CLUSTER_NAVIGATION_STATE,
/* areaId= */ 0, SystemClock.elapsedRealtime(), VehiclePropertyStatus.AVAILABLE,
/* value= */ navigateState);
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);
} catch (ServiceSpecificException | IllegalArgumentException e) {
Slogf.e(TAG, "Failed to send request: " + request, e);
}
}
@Override
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
public void dump(PrintWriter writer) {
writer.println("*Cluster HAL*");
writer.println("mServiceMode: " + mServiceMode);
writer.println("mIsCoreSupported: " + mIsCoreSupported);
writer.println("mIsNavigationStateSupported: " + mIsNavigationStateSupported);
writer.println("mIsHeartbeatSupported: " + mIsHeartbeatSupported);
}
}