1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.hal; 18 19 import static android.car.VehiclePropertyIds.CLUSTER_DISPLAY_STATE; 20 import static android.car.VehiclePropertyIds.CLUSTER_HEARTBEAT; 21 import static android.car.VehiclePropertyIds.CLUSTER_NAVIGATION_STATE; 22 import static android.car.VehiclePropertyIds.CLUSTER_REPORT_STATE; 23 import static android.car.VehiclePropertyIds.CLUSTER_REQUEST_DISPLAY; 24 import static android.car.VehiclePropertyIds.CLUSTER_SWITCH_UI; 25 26 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 27 import static com.android.car.internal.common.CommonConstants.EMPTY_FLOAT_ARRAY; 28 import static com.android.car.internal.common.CommonConstants.EMPTY_INT_ARRAY; 29 import static com.android.car.internal.common.CommonConstants.EMPTY_LONG_ARRAY; 30 31 import android.annotation.NonNull; 32 import android.car.builtin.util.Slogf; 33 import android.content.Context; 34 import android.graphics.Insets; 35 import android.graphics.Rect; 36 import android.hardware.automotive.vehicle.VehiclePropertyStatus; 37 import android.os.ServiceSpecificException; 38 import android.os.SystemClock; 39 40 import com.android.car.R; 41 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 42 import com.android.car.internal.util.IntArray; 43 import com.android.internal.annotations.GuardedBy; 44 import com.android.internal.annotations.VisibleForTesting; 45 46 import java.io.PrintWriter; 47 import java.util.Collection; 48 import java.util.List; 49 50 /** 51 * Translates HAL input events to higher-level semantic information. 52 */ 53 public final class ClusterHalService extends HalServiceBase { 54 private static final String TAG = ClusterHalService.class.getSimpleName(); 55 56 // The value of config_clusterHomeServiceOnMode that are currently supported: 57 // 0: FULL mode. ClusterHomeService is enabled only when all CORE_PROPERTIES are available. 58 // 1: LIGHT mode. No properties need to be available. In this mode, all service methods that 59 // rely on core properties will throw IllegalStateException, regardless of whether the 60 // property is actually available or not. 61 private static final int CONFIG_CLUSTER_HOME_SERVICE_FULL_MODE = 0; 62 private static final int CONFIG_CLUSTER_HOME_SERVICE_LIGHT_MODE = 1; 63 64 public static final int DISPLAY_OFF = 0; 65 public static final int DISPLAY_ON = 1; 66 public static final int DONT_CARE = -1; 67 68 /** 69 * Interface to receive incoming Cluster HAL events. 70 */ 71 public interface ClusterHalEventCallback { 72 /** 73 * Called when CLUSTER_SWITCH_UI message is received. 74 * 75 * @param uiType uiType ClusterOS wants to switch to 76 */ onSwitchUi(int uiType)77 void onSwitchUi(int uiType); 78 79 /** 80 * Called when CLUSTER_DISPLAY_STATE message is received. 81 * 82 * @param onOff 0 - off, 1 - on 83 * @param bounds the area to render the cluster Activity in pixel 84 * @param insets Insets of the cluster display 85 */ onDisplayState(int onOff, Rect bounds, Insets insets)86 void onDisplayState(int onOff, Rect bounds, Insets insets); 87 }; 88 89 private static final int[] SUPPORTED_PROPERTIES = new int[]{ 90 CLUSTER_SWITCH_UI, 91 CLUSTER_DISPLAY_STATE, 92 CLUSTER_REPORT_STATE, 93 CLUSTER_REQUEST_DISPLAY, 94 CLUSTER_NAVIGATION_STATE, 95 CLUSTER_HEARTBEAT, 96 }; 97 98 private static final int[] CORE_PROPERTIES = new int[]{ 99 CLUSTER_SWITCH_UI, 100 CLUSTER_REPORT_STATE, 101 CLUSTER_DISPLAY_STATE, 102 CLUSTER_REQUEST_DISPLAY, 103 }; 104 105 private static final int[] SUBSCRIBABLE_PROPERTIES = new int[]{ 106 CLUSTER_SWITCH_UI, 107 CLUSTER_DISPLAY_STATE, 108 }; 109 110 private final Object mLock = new Object(); 111 112 @GuardedBy("mLock") 113 private ClusterHalEventCallback mCallback; 114 115 private final VehicleHal mHal; 116 117 // The value of config_clusterHomeServiceOnMode 118 private final int mServiceMode; 119 // Whether all CORE_PROPERTIES are available. 120 private volatile boolean mIsCoreSupported; 121 private volatile boolean mIsNavigationStateSupported; 122 private volatile boolean mIsHeartbeatSupported; 123 124 private final HalPropValueBuilder mPropValueBuilder; 125 ClusterHalService(Context context, VehicleHal hal)126 public ClusterHalService(Context context, VehicleHal hal) { 127 mHal = hal; 128 mPropValueBuilder = hal.getHalPropValueBuilder(); 129 mServiceMode = context.getResources().getInteger(R.integer.config_clusterHomeServiceMode); 130 } 131 132 /** 133 * {@inheritDoc} 134 * 135 * <p>Note that {@link #takeProperties} must be called before this method, so that available 136 * properties are correctly initialized.</p> 137 */ 138 @Override init()139 public void init() { 140 Slogf.d(TAG, "initClusterHalService"); 141 // Do not subscribe if the config is not FULL mode, or any core property is not available. 142 if (!isFullModeEnabled()) return; 143 144 for (int property : SUBSCRIBABLE_PROPERTIES) { 145 mHal.subscribePropertySafe(this, property); 146 } 147 } 148 149 @Override release()150 public void release() { 151 Slogf.d(TAG, "releaseClusterHalService"); 152 synchronized (mLock) { 153 mCallback = null; 154 } 155 } 156 157 /** 158 * Sets the event callback to receive Cluster HAL events. 159 */ setCallback(ClusterHalEventCallback callback)160 public void setCallback(ClusterHalEventCallback callback) { 161 synchronized (mLock) { 162 mCallback = callback; 163 } 164 } 165 166 @NonNull 167 @Override getAllSupportedProperties()168 public int[] getAllSupportedProperties() { 169 return SUPPORTED_PROPERTIES; 170 } 171 172 @Override takeProperties(@onNull Collection<HalPropConfig> properties)173 public void takeProperties(@NonNull Collection<HalPropConfig> properties) { 174 IntArray supportedProperties = new IntArray(properties.size()); 175 for (HalPropConfig property : properties) { 176 supportedProperties.add(property.getPropId()); 177 } 178 mIsCoreSupported = true; 179 for (int coreProperty : CORE_PROPERTIES) { 180 if (supportedProperties.indexOf(coreProperty) < 0) { 181 mIsCoreSupported = false; 182 break; 183 } 184 } 185 mIsNavigationStateSupported = supportedProperties.indexOf(CLUSTER_NAVIGATION_STATE) >= 0; 186 mIsHeartbeatSupported = supportedProperties.indexOf(CLUSTER_HEARTBEAT) >= 0; 187 Slogf.d(TAG, "takeProperties: coreSupported=%s, navigationStateSupported=%s, " 188 + "heartbeatSupported=%s", 189 mIsCoreSupported, mIsNavigationStateSupported, mIsHeartbeatSupported); 190 } 191 192 @VisibleForTesting isFullModeEnabled()193 boolean isFullModeEnabled() { 194 // In FULL mode, all core properties need to be available. 195 return mIsCoreSupported && (mServiceMode == CONFIG_CLUSTER_HOME_SERVICE_FULL_MODE); 196 } 197 isLightMode()198 public boolean isLightMode() { 199 return mServiceMode == CONFIG_CLUSTER_HOME_SERVICE_LIGHT_MODE; 200 } 201 isServiceEnabled()202 public boolean isServiceEnabled() { 203 return isFullModeEnabled() || isLightMode(); 204 } 205 isNavigationStateSupported()206 public boolean isNavigationStateSupported() { 207 return mIsNavigationStateSupported; 208 } 209 isHeartbeatSupported()210 public boolean isHeartbeatSupported() { 211 return mIsHeartbeatSupported; 212 } 213 214 @Override onHalEvents(List<HalPropValue> values)215 public void onHalEvents(List<HalPropValue> values) { 216 Slogf.d(TAG, "handleHalEvents(): %s", values); 217 ClusterHalEventCallback callback; 218 synchronized (mLock) { 219 callback = mCallback; 220 } 221 if (callback == null || !isFullModeEnabled()) { 222 return; 223 } 224 225 for (HalPropValue value : values) { 226 switch (value.getPropId()) { 227 case CLUSTER_SWITCH_UI: 228 if (value.getInt32ValuesSize() < 1) { 229 Slogf.e(TAG, "received invalid CLUSTER_SWITCH_UI property from HAL, " 230 + "expect at least 1 int value."); 231 break; 232 } 233 int uiType = value.getInt32Value(0); 234 callback.onSwitchUi(uiType); 235 break; 236 case CLUSTER_DISPLAY_STATE: 237 if (value.getInt32ValuesSize() < 9) { 238 Slogf.e(TAG, "received invalid CLUSTER_DISPLAY_STATE property from HAL, " 239 + "expect at least 9 int value."); 240 break; 241 } 242 int onOff = value.getInt32Value(0); 243 Rect bounds = null; 244 if (hasNoDontCare(value, /* start= */ 1, /* length= */ 4, "bounds")) { 245 bounds = 246 new Rect( 247 value.getInt32Value(1), value.getInt32Value(2), 248 value.getInt32Value(3), value.getInt32Value(4)); 249 } 250 Insets insets = null; 251 if (hasNoDontCare(value, /* start= */ 5, /* length= */ 4, "insets")) { 252 insets = 253 Insets.of( 254 value.getInt32Value(5), value.getInt32Value(6), 255 value.getInt32Value(7), value.getInt32Value(8)); 256 } 257 callback.onDisplayState(onOff, bounds, insets); 258 break; 259 default: 260 Slogf.w(TAG, "received unsupported event from HAL: %s", value); 261 } 262 } 263 } 264 hasNoDontCare(HalPropValue value, int start, int length, String fieldName)265 private static boolean hasNoDontCare(HalPropValue value, int start, int length, 266 String fieldName) { 267 int count = 0; 268 for (int i = start; i < start + length; ++i) { 269 if (value.getInt32Value(i) == DONT_CARE) { 270 ++count; 271 } 272 } 273 if (count == 0) { 274 return true; 275 } 276 if (count != length) { 277 Slogf.w(TAG, "Don't care should be set in the whole %s.", fieldName); 278 } 279 return false; 280 } 281 282 /** 283 * Reports the current display state and ClusterUI state. 284 * 285 * @param onOff 0 - off, 1 - on 286 * @param bounds the area to render the cluster Activity in pixel 287 * @param insets Insets of the cluster display 288 * @param uiTypeMain uiType that ClusterHome tries to show in main area 289 * @param uiTypeSub uiType that ClusterHome tries to show in sub area 290 * @param uiAvailability the byte array to represent the availability of ClusterUI. 291 */ reportState(int onOff, Rect bounds, Insets insets, int uiTypeMain, int uiTypeSub, byte[] uiAvailability)292 public void reportState(int onOff, Rect bounds, Insets insets, 293 int uiTypeMain, int uiTypeSub, byte[] uiAvailability) { 294 if (!isFullModeEnabled()) { 295 throw new IllegalStateException( 296 "reportState: one or more core property is not supported on this device, " 297 + "or the service is not in FULL mode"); 298 } 299 int[] intValues = new int[]{ 300 onOff, 301 bounds.left, 302 bounds.top, 303 bounds.right, 304 bounds.bottom, 305 insets.left, 306 insets.top, 307 insets.right, 308 insets.bottom, 309 uiTypeMain, 310 uiTypeSub 311 }; 312 HalPropValue request = mPropValueBuilder.build(CLUSTER_REPORT_STATE, 313 /* areaId= */ 0, SystemClock.elapsedRealtime(), VehiclePropertyStatus.AVAILABLE, 314 /* int32Values= */ intValues, /* floatValues= */ EMPTY_FLOAT_ARRAY, 315 /* int64Values= */ EMPTY_LONG_ARRAY, /* stringValue= */ "", 316 /* byteValues= */ uiAvailability); 317 send(request); 318 } 319 320 /** 321 * Requests to turn the cluster display on to show some ClusterUI. 322 * 323 * @param uiType uiType that ClusterHome tries to show in main area 324 */ requestDisplay(int uiType)325 public void requestDisplay(int uiType) { 326 if (!isFullModeEnabled()) { 327 throw new IllegalStateException("requestDisplay: one or more core property is " 328 + "not supported on this device, or the service is not in FULL mode"); 329 } 330 HalPropValue request = mPropValueBuilder.build(CLUSTER_REQUEST_DISPLAY, 331 /* areaId= */ 0, SystemClock.elapsedRealtime(), VehiclePropertyStatus.AVAILABLE, 332 /* value= */ uiType); 333 send(request); 334 } 335 336 337 /** 338 * Informs the current navigation state. 339 * 340 * @param navigateState the serialized message of {@code NavigationStateProto} 341 */ sendNavigationState(byte[] navigateState)342 public void sendNavigationState(byte[] navigateState) { 343 if (!isNavigationStateSupported()) { 344 return; 345 } 346 HalPropValue request = mPropValueBuilder.build(CLUSTER_NAVIGATION_STATE, 347 /* areaId= */ 0, SystemClock.elapsedRealtime(), VehiclePropertyStatus.AVAILABLE, 348 /* values= */ navigateState); 349 send(request); 350 } 351 352 /** 353 * Sends a heartbeat to ClusterOS 354 * @param epochTimeNs the current time 355 * @param visibility 0 means invisible and 1 means visible. 356 * @param appMetadata the application specific metadata which will be delivered with 357 * the heartbeat. 358 */ sendHeartbeat(long epochTimeNs, long visibility, byte[] appMetadata)359 public void sendHeartbeat(long epochTimeNs, long visibility, byte[] appMetadata) { 360 long[] longValues = new long[]{ 361 epochTimeNs, 362 visibility 363 }; 364 HalPropValue request = mPropValueBuilder.build(CLUSTER_HEARTBEAT, 365 /* areaId= */ 0, SystemClock.elapsedRealtime(), VehiclePropertyStatus.AVAILABLE, 366 /* int32Values= */ EMPTY_INT_ARRAY, /* floatValues= */ EMPTY_FLOAT_ARRAY, 367 /* int64Values= */ longValues, /* stringValue= */ "", 368 /* byteValues= */ appMetadata); 369 send(request); 370 } 371 send(HalPropValue request)372 private void send(HalPropValue request) { 373 try { 374 mHal.set(request); 375 } catch (ServiceSpecificException | IllegalArgumentException e) { 376 Slogf.e(TAG, "Failed to send request: " + request, e); 377 } 378 } 379 380 @Override 381 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(PrintWriter writer)382 public void dump(PrintWriter writer) { 383 writer.println("*Cluster HAL*"); 384 writer.println("mServiceMode: " + mServiceMode); 385 writer.println("mIsCoreSupported: " + mIsCoreSupported); 386 writer.println("mIsNavigationStateSupported: " + mIsNavigationStateSupported); 387 writer.println("mIsHeartbeatSupported: " + mIsHeartbeatSupported); 388 } 389 } 390