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