1 /*
2  * Copyright (C) 2018 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.google.android.car.kitchensink.property;
18 
19 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
20 
21 import static java.lang.Integer.toHexString;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.car.Car;
26 import android.car.VehiclePropertyIds;
27 import android.car.VehiclePropertyType;
28 import android.car.hardware.CarPropertyConfig;
29 import android.car.hardware.CarPropertyValue;
30 import android.car.hardware.property.CarPropertyManager;
31 import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback;
32 import android.car.hardware.property.CarPropertyManager.GetPropertyCallback;
33 import android.car.hardware.property.CarPropertyManager.GetPropertyRequest;
34 import android.car.hardware.property.CarPropertyManager.GetPropertyResult;
35 import android.car.hardware.property.Subscription;
36 import android.content.Context;
37 import android.os.Bundle;
38 import android.os.Handler;
39 import android.util.Log;
40 import android.util.SparseArray;
41 import android.util.SparseBooleanArray;
42 import android.util.SparseIntArray;
43 import android.util.SparseLongArray;
44 import android.view.LayoutInflater;
45 import android.view.View;
46 import android.view.ViewGroup;
47 import android.widget.AdapterView;
48 import android.widget.AdapterView.OnItemSelectedListener;
49 import android.widget.ArrayAdapter;
50 import android.widget.Button;
51 import android.widget.EditText;
52 import android.widget.ScrollView;
53 import android.widget.Spinner;
54 import android.widget.TextView;
55 import android.widget.Toast;
56 import android.widget.ToggleButton;
57 
58 import androidx.fragment.app.Fragment;
59 
60 import com.google.android.car.kitchensink.KitchenSinkHelper;
61 import com.google.android.car.kitchensink.R;
62 
63 import java.util.ArrayList;
64 import java.util.Arrays;
65 import java.util.List;
66 import java.util.stream.Collectors;
67 
68 public class PropertyTestFragment extends Fragment implements OnItemSelectedListener {
69     private static final String TAG = "PropertyTestFragment";
70     private static final int KS_PERMISSIONS_REQUEST = 1;
71 
72     // The dangerous permissions that need to be granted at run-time.
73     private static final String[] REQUIRED_DANGEROUS_PERMISSIONS = new String[]{
74         Car.PERMISSION_ENERGY,
75         Car.PERMISSION_SPEED
76     };
77     private static final Float[] SUBSCRIPTION_RATES_HZ = new Float[]{
78         0.0f,
79         1.0f,
80         2.0f,
81         5.0f,
82         10.0f,
83         100.0f
84     };
85     private static final Float[] RESOLUTIONS = new Float[]{
86         0.0f,
87         0.1f,
88         1.0f,
89         10.0f
90     };
91 
92     private Context mContext;
93     private KitchenSinkHelper mKitchenSinkHelper;
94     private CarPropertyManager mMgr;
95     private List<PropertyInfo> mPropInfo = null;
96     private Spinner mSubscriptionRateHz;
97     private Spinner mResolution;
98     private Spinner mVariableUpdateRate;
99     private ToggleButton mSubscribeButton;
100     private Spinner mAreaId;
101     private TextView mEventLog;
102     private Spinner mPropertyId;
103     private ScrollView mScrollView;
104     private EditText mSetValue;
105     private PropertyListEventListener mListener;
106     private final SparseIntArray mPropertySubscriptionRateHzSelection = new SparseIntArray();
107     private final SparseIntArray mPropertyResolutionSelection = new SparseIntArray();
108     private final SparseIntArray mPropertyVariableUpdateRateSelection = new SparseIntArray();
109     private final SparseBooleanArray mPropertyIsSubscribedSelection = new SparseBooleanArray();
110     private GetPropertyCallback mGetPropertyCallback = new GetPropertyCallback() {
111         @Override
112         public void onSuccess(@NonNull GetPropertyResult<?> getPropertyResult) {
113             int propId = getPropertyResult.getPropertyId();
114             long timestamp = getPropertyResult.getTimestampNanos();
115             setTextOnSuccess(propId, timestamp, getPropertyResult.getValue(),
116                     CarPropertyValue.STATUS_AVAILABLE);
117         }
118 
119         @Override
120         public void onFailure(@NonNull CarPropertyManager.PropertyAsyncError propertyAsyncError) {
121             Log.e(TAG, "Failed to get async VHAL property");
122             Toast.makeText(mContext, "Failed to get async VHAL property with error code: "
123                     + propertyAsyncError.getErrorCode() + " and vendor error code: "
124                     + propertyAsyncError.getVendorErrorCode(), Toast.LENGTH_SHORT).show();
125         }
126     };
127 
128     private CarPropertyManager.SetPropertyCallback mSetPropertyCallback =
129             new CarPropertyManager.SetPropertyCallback() {
130                 @Override
131                 public void onSuccess(
132                         @NonNull CarPropertyManager.SetPropertyResult setPropertyResult) {
133                     Toast.makeText(mContext, "Success", Toast.LENGTH_SHORT).show();
134                 }
135 
136                 @Override
137                 public void onFailure(
138                         @NonNull CarPropertyManager.PropertyAsyncError propertyAsyncError) {
139                     Log.e(TAG, "Failed to get async VHAL property");
140                     Toast.makeText(mContext, "Failed to set async VHAL property with error code: "
141                             + propertyAsyncError.getErrorCode() + " and vendor error code: "
142                             + propertyAsyncError.getVendorErrorCode(), Toast.LENGTH_SHORT).show();
143                 }
144     };
145 
146     @Override
onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)147     public void onRequestPermissionsResult(int requestCode, String[] permissions,
148             int[] grantResults) {
149         for (int i = 0; i < grantResults.length; i++) {
150             if (grantResults[i] != PERMISSION_GRANTED) {
151                 Log.w(TAG, "Permission: " + permissions[i] + " is not granted, "
152                         + "some properties might not be listed");
153             }
154         }
155         Runnable r = () -> {
156             mMgr = mKitchenSinkHelper.getPropertyManager();
157             populateConfigList();
158 
159             // Configure dropdown menu for propertyId spinner
160             ArrayAdapter<PropertyInfo> adapter =
161                     new ArrayAdapter<PropertyInfo>(mContext, android.R.layout.simple_spinner_item,
162                             mPropInfo);
163             adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
164             mPropertyId.setAdapter(adapter);
165             mPropertyId.setOnItemSelectedListener(this);
166         };
167         mKitchenSinkHelper.requestRefreshManager(r, new Handler(getContext().getMainLooper()));
168     }
169 
170     @Nullable
171     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)172     public View onCreateView(LayoutInflater inflater,
173             @Nullable ViewGroup container,
174             @Nullable Bundle savedInstanceState) {
175         View view = inflater.inflate(R.layout.property, container, false);
176         // Get resource IDs
177         mSubscriptionRateHz = view.findViewById(R.id.sSubscriptionRate);
178         mResolution = view.findViewById(R.id.sResolution);
179         mVariableUpdateRate = view.findViewById(R.id.sVariableUpdateRate);
180         mAreaId = view.findViewById(R.id.sAreaId);
181         mEventLog = view.findViewById(R.id.tvEventLog);
182         mPropertyId = view.findViewById(R.id.sPropertyId);
183         mScrollView = view.findViewById(R.id.svEventLog);
184         mSetValue = view.findViewById(R.id.etSetPropertyValue);
185         mContext = getActivity();
186         mListener = new PropertyListEventListener(mEventLog);
187         if (!(mContext instanceof KitchenSinkHelper)) {
188             throw new IllegalStateException(
189                     "context does not implement " + KitchenSinkHelper.class.getSimpleName());
190         }
191         mKitchenSinkHelper = (KitchenSinkHelper) mContext;
192 
193         mSubscribeButton = view.findViewById(R.id.tbSubscribeButton);
194         mSubscribeButton.setEnabled(false);
195 
196         // Configure listeners for buttons
197         Button b = view.findViewById(R.id.bGetProperty);
198         b.setOnClickListener(v -> {
199             try {
200                 PropertyInfo info = (PropertyInfo) mPropertyId.getSelectedItem();
201                 int propId = info.mConfig.getPropertyId();
202                 int areaId = Integer.decode(mAreaId.getSelectedItem().toString());
203                 CarPropertyValue value = mMgr.getProperty(propId, areaId);
204                 setTextOnSuccess(propId, value.getTimestamp(), value.getValue(), value.getStatus());
205             } catch (Exception e) {
206                 Log.e(TAG, "Failed to get VHAL property", e);
207                 Toast.makeText(mContext, "Failed to get VHAL property: " + e.getMessage(),
208                         Toast.LENGTH_SHORT).show();
209             }
210         });
211 
212         b = view.findViewById(R.id.getPropertyAsync);
213         b.setOnClickListener(v -> {
214             try {
215                 PropertyInfo info = (PropertyInfo) mPropertyId.getSelectedItem();
216                 int propId = info.mConfig.getPropertyId();
217                 int areaId = Integer.decode(mAreaId.getSelectedItem().toString());
218                 GetPropertyRequest getPropertyRequest = mMgr.generateGetPropertyRequest(propId,
219                         areaId);
220                 mMgr.getPropertiesAsync(List.of(getPropertyRequest),
221                         /* cancellationSignal= */ null, /* callbackExecutor= */ null,
222                         mGetPropertyCallback);
223             } catch (Exception e) {
224                 Log.e(TAG, "Failed to get async VHAL property", e);
225                 Toast.makeText(mContext, "Failed to get async VHAL property: "
226                                 + e.getMessage(), Toast.LENGTH_SHORT).show();
227             }
228         });
229 
230         b = view.findViewById(R.id.bSetProperty);
231         b.setOnClickListener(v -> {
232             try {
233                 PropertyInfo info = (PropertyInfo) mPropertyId.getSelectedItem();
234                 int propId = info.mConfig.getPropertyId();
235                 int areaId = Integer.decode(mAreaId.getSelectedItem().toString());
236                 String valueString = mSetValue.getText().toString();
237 
238                 switch (propId & VehiclePropertyType.MASK) {
239                     case VehiclePropertyType.BOOLEAN:
240                         Boolean boolVal = Boolean.parseBoolean(valueString);
241                         mMgr.setBooleanProperty(propId, areaId, boolVal);
242                         break;
243                     case VehiclePropertyType.FLOAT:
244                         Float floatVal = Float.parseFloat(valueString);
245                         mMgr.setFloatProperty(propId, areaId, floatVal);
246                         break;
247                     case VehiclePropertyType.INT32:
248                         Integer intVal = Integer.parseInt(valueString);
249                         mMgr.setIntProperty(propId, areaId, intVal);
250                         break;
251                     default:
252                         Toast.makeText(mContext, "PropertyType=0x" + toHexString(propId
253                                         & VehiclePropertyType.MASK) + " is not handled!",
254                                 Toast.LENGTH_LONG).show();
255                         break;
256                 }
257             } catch (Exception e) {
258                 Log.e(TAG, "Failed to set VHAL property", e);
259                 Toast.makeText(mContext, "Failed to set VHAL property: " + e.getMessage(),
260                         Toast.LENGTH_LONG).show();
261             }
262         });
263 
264         b = view.findViewById(R.id.SetPropertyAsync);
265         b.setOnClickListener(v -> {
266             try {
267                 PropertyInfo info = (PropertyInfo) mPropertyId.getSelectedItem();
268                 int propId = info.mConfig.getPropertyId();
269                 int areaId = Integer.decode(mAreaId.getSelectedItem().toString());
270                 String valueString = mSetValue.getText().toString();
271 
272                 switch (propId & VehiclePropertyType.MASK) {
273                     case VehiclePropertyType.BOOLEAN:
274                         Boolean boolVal = Boolean.parseBoolean(valueString);
275                         callSetPropertiesAsync(propId, areaId, boolVal);
276                         break;
277                     case VehiclePropertyType.FLOAT:
278                         Float floatVal = Float.parseFloat(valueString);
279                         callSetPropertiesAsync(propId, areaId, floatVal);
280                         break;
281                     case VehiclePropertyType.INT32:
282                         Integer intVal = Integer.parseInt(valueString);
283                         callSetPropertiesAsync(propId, areaId, intVal);
284                         break;
285                     default:
286                         Toast.makeText(mContext, "PropertyType=0x" + toHexString(propId
287                                         & VehiclePropertyType.MASK) + " is not handled!",
288                                 Toast.LENGTH_LONG).show();
289                         break;
290                 }
291             } catch (Exception e) {
292                 Log.e(TAG, "Failed to set VHAL property", e);
293                 Toast.makeText(mContext, "Failed to set async VHAL property: "
294                         + e.getMessage(), Toast.LENGTH_LONG).show();
295             }
296         });
297 
298         b = view.findViewById(R.id.bClearLog);
299         b.setOnClickListener(v -> {
300             mEventLog.setText("");
301         });
302 
303         requestPermissions(REQUIRED_DANGEROUS_PERMISSIONS, KS_PERMISSIONS_REQUEST);
304 
305         return view;
306     }
307 
populateConfigList()308     private void populateConfigList() {
309         try {
310             mPropInfo = mMgr.getPropertyList()
311                     .stream()
312                     .map(PropertyInfo::new)
313                     .sorted()
314                     .collect(Collectors.toList());
315         } catch (Exception e) {
316             Log.e(TAG, "Unhandled exception in populateConfigList: ", e);
317         }
318     }
319 
setEnabledSubscriptionScrollViews(boolean setEnabled)320     private void setEnabledSubscriptionScrollViews(boolean setEnabled) {
321         mSubscriptionRateHz.setEnabled(setEnabled);
322         mResolution.setEnabled(setEnabled);
323         mVariableUpdateRate.setEnabled(setEnabled);
324     }
325 
326     // Spinner callbacks
onItemSelected(AdapterView<?> parent, View view, int pos, long id)327     public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
328         PropertyInfo info = (PropertyInfo) parent.getItemAtPosition(pos);
329         int propertyId = info.mPropId;
330         int[] areaIds = info.mConfig.getAreaIds();
331         List<String> areaIdsString = new ArrayList<String>();
332         if (areaIds.length == 0) {
333             areaIdsString.add("0x0");
334         } else {
335             for (int areaId : areaIds) {
336                 areaIdsString.add("0x" + toHexString(areaId));
337             }
338         }
339 
340         // Configure dropdown menu for propertyId spinner
341         ArrayAdapter<String> areaIdAdapter = new ArrayAdapter<String>(mContext,
342                 android.R.layout.simple_spinner_item, areaIdsString);
343         areaIdAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
344         mAreaId.setAdapter(areaIdAdapter);
345 
346         int changeMode = info.mConfig.getChangeMode();
347         List<String> subscriptionRateHzStrings = new ArrayList<String>();
348         subscriptionRateHzStrings.add("0 Hz");
349         List<String> resolutionStrings = new ArrayList<String>();
350         resolutionStrings.add("0");
351         List<String> vurStrings = new ArrayList<String>();
352         vurStrings.add("DISABLED");
353 
354         if (changeMode == CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_STATIC) {
355             setEnabledSubscriptionScrollViews(false);
356             mSubscribeButton.setEnabled(false);
357         } else if (changeMode == CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_ONCHANGE) {
358             setEnabledSubscriptionScrollViews(false);
359             mSubscribeButton.setEnabled(true);
360         } else if (changeMode == CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS) {
361             setEnabledSubscriptionScrollViews(true);
362             mSubscribeButton.setEnabled(true);
363 
364             float maxSubRate = info.mConfig.getMaxSampleRate();
365             subscriptionRateHzStrings.add("1 Hz");
366             if (maxSubRate >= 2.0) {
367                 subscriptionRateHzStrings.add("2 Hz");
368             }
369             if (maxSubRate >= 5.0) {
370                 subscriptionRateHzStrings.add("5 Hz");
371             }
372             if (maxSubRate >= 10.0) {
373                 subscriptionRateHzStrings.add("10 Hz");
374             }
375             if (maxSubRate >= 100.0) {
376                 subscriptionRateHzStrings.add("100 Hz");
377             }
378 
379             resolutionStrings.add("0.1");
380             resolutionStrings.add("1");
381             resolutionStrings.add("10");
382 
383             vurStrings.add("ENABLED");
384         }
385 
386         if (mPropertySubscriptionRateHzSelection.get(propertyId, -1) == -1) {
387             mPropertySubscriptionRateHzSelection.put(propertyId, 0);
388             mPropertyResolutionSelection.put(propertyId, 0);
389             mPropertyVariableUpdateRateSelection.put(propertyId, 0);
390             mPropertyIsSubscribedSelection.put(propertyId, false);
391         }
392 
393         ArrayAdapter<String> subscriptionRateHzAdapter = new ArrayAdapter<String>(mContext,
394                 android.R.layout.simple_spinner_item, subscriptionRateHzStrings);
395         subscriptionRateHzAdapter.setDropDownViewResource(
396                 android.R.layout.simple_spinner_dropdown_item);
397         mSubscriptionRateHz.setAdapter(subscriptionRateHzAdapter);
398         mSubscriptionRateHz.setSelection(mPropertySubscriptionRateHzSelection.get(propertyId));
399         mSubscriptionRateHz.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
400             @Override
401             public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long id) {
402                 mPropertySubscriptionRateHzSelection.put(info.mConfig.getPropertyId(), pos);
403             }
404 
405             @Override
406             public void onNothingSelected(AdapterView<?> adapterView) {
407                 // do nothing.
408             }
409         });
410 
411         ArrayAdapter<String> resolutionAdapter = new ArrayAdapter<String>(mContext,
412                 android.R.layout.simple_spinner_item, resolutionStrings);
413         resolutionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
414         mResolution.setAdapter(resolutionAdapter);
415         mResolution.setSelection(mPropertyResolutionSelection.get(propertyId));
416         mResolution.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
417             @Override
418             public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long id) {
419                 mPropertyResolutionSelection.put(info.mConfig.getPropertyId(), pos);
420             }
421 
422             @Override
423             public void onNothingSelected(AdapterView<?> adapterView) {
424                 // do nothing.
425             }
426         });
427 
428         ArrayAdapter<String> variableUpdateRateAdapter = new ArrayAdapter<String>(mContext,
429                 android.R.layout.simple_spinner_item, vurStrings);
430         variableUpdateRateAdapter.setDropDownViewResource(
431                 android.R.layout.simple_spinner_dropdown_item);
432         mVariableUpdateRate.setAdapter(variableUpdateRateAdapter);
433         mVariableUpdateRate.setSelection(mPropertyVariableUpdateRateSelection.get(propertyId));
434         mVariableUpdateRate.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
435             @Override
436             public void onItemSelected(AdapterView<?> adapterView, View view, int pos, long id) {
437                 mPropertyVariableUpdateRateSelection.put(info.mConfig.getPropertyId(), pos);
438             }
439 
440             @Override
441             public void onNothingSelected(AdapterView<?> adapterView) {
442                 // do nothing.
443             }
444         });
445 
446         mSubscribeButton.setChecked(mPropertyIsSubscribedSelection.get(propertyId));
447         if (mSubscribeButton.isChecked()) {
448             setEnabledSubscriptionScrollViews(false);
449         }
450         mSubscribeButton.setOnClickListener(v -> {
451             Float subscriptionRateHz = SUBSCRIPTION_RATES_HZ[
452                     mPropertySubscriptionRateHzSelection.get(propertyId)];
453             if (mSubscribeButton.isChecked()
454                     && (changeMode != CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_CONTINUOUS
455                             || subscriptionRateHz != 0.0)) {
456                 mListener.addPropertySelectedSubscriptionRateHz(propertyId, subscriptionRateHz);
457                 mListener.updatePropertyStartTime(propertyId);
458                 mListener.resetEventCountForProperty(propertyId);
459 
460                 Float resolution = RESOLUTIONS[mPropertyResolutionSelection.get(propertyId)];
461                 boolean variableUpdateRate =
462                         mPropertyVariableUpdateRateSelection.get(propertyId) != 0;
463 
464                 try {
465                     mMgr.subscribePropertyEvents(List.of(
466                             new Subscription.Builder(propertyId)
467                                     .setUpdateRateHz(subscriptionRateHz)
468                                     .setResolution(resolution)
469                                     .setVariableUpdateRateEnabled(variableUpdateRate)
470                                     .build()),
471                             /* callbackExecutor= */ null, mListener);
472                     mPropertyIsSubscribedSelection.put(propertyId, true);
473                     setEnabledSubscriptionScrollViews(false);
474                 } catch (Exception e) {
475                     Log.e(TAG, "Unhandled exception: ", e);
476                 }
477             } else {
478                 try {
479                     mMgr.unsubscribePropertyEvents(propertyId, mListener);
480                     mPropertyIsSubscribedSelection.put(propertyId, false);
481                     setEnabledSubscriptionScrollViews(true);
482                 } catch (Exception e) {
483                     Log.e(TAG, "Unhandled exception: ", e);
484                 }
485             }
486         });
487     }
488 
onNothingSelected(AdapterView<?> parent)489     public void onNothingSelected(AdapterView<?> parent) {
490         // Another interface callback
491     }
492 
scrollEventLogsToBottom()493     public void scrollEventLogsToBottom() {
494         mScrollView.post(new Runnable() {
495             public void run() {
496                 mScrollView.fullScroll(View.FOCUS_DOWN);
497                 //mListenerScrollView.smoothScrollTo(0, mTextStatus.getBottom());
498             }
499         });
500     }
501 
setTextOnSuccess(int propId, long timestamp, Object value, int status)502     private void setTextOnSuccess(int propId, long timestamp, Object value, int status) {
503         mEventLog.append("getProperty: ");
504         if (propId == VehiclePropertyIds.WHEEL_TICK) {
505             Object[] ticks = (Object[]) value;
506             mEventLog.append("ElapsedRealtimeNanos=" + timestamp
507                     + " [0]=" + (Long) ticks[0]
508                     + " [1]=" + (Long) ticks[1] + " [2]=" + (Long) ticks[2]
509                     + " [3]=" + (Long) ticks[3] + " [4]=" + (Long) ticks[4]);
510         } else {
511             String valueString = value.getClass().isArray()
512                     ? Arrays.toString((Object[]) value)
513                     : value.toString();
514             mEventLog.append("ElapsedRealtimeNanos=" + timestamp
515                     + " status=" + status
516                     + " value=" + valueString
517                     + " read=" + mMgr.getReadPermission(propId)
518                     + " write=" + mMgr.getWritePermission(propId));
519         }
520         mEventLog.append("\n");
521         scrollEventLogsToBottom();
522     }
523 
callSetPropertiesAsync(int propId, int areaId, T request)524     private <T> void callSetPropertiesAsync(int propId, int areaId, T request) {
525         mMgr.setPropertiesAsync(
526                 List.of(mMgr.generateSetPropertyRequest(propId, areaId, request)),
527                 /* cancellationSignal= */ null,
528                 /* callbackExecutor= */ null, mSetPropertyCallback);
529     }
530 
531     private class PropertyListEventListener implements CarPropertyEventCallback {
532         private final TextView mTvLogEvent;
533         private final SparseArray<Float> mPropSubscriptionRateHz = new SparseArray<>();
534         private final SparseLongArray mStartTime = new SparseLongArray();
535         private final SparseIntArray mNumEvents = new SparseIntArray();
536 
PropertyListEventListener(TextView logEvent)537         PropertyListEventListener(TextView logEvent) {
538             mTvLogEvent = logEvent;
539         }
540 
addPropertySelectedSubscriptionRateHz(Integer propId, Float subscriptionRateHz)541         void addPropertySelectedSubscriptionRateHz(Integer propId, Float subscriptionRateHz) {
542             mPropSubscriptionRateHz.put(propId, subscriptionRateHz);
543         }
544 
updatePropertyStartTime(Integer propId)545         void updatePropertyStartTime(Integer propId) {
546             mStartTime.put(propId, System.currentTimeMillis());
547         }
548 
resetEventCountForProperty(Integer propId)549         void resetEventCountForProperty(Integer propId) {
550             mNumEvents.put(propId, 0);
551         }
552 
553         @Override
onChangeEvent(CarPropertyValue value)554         public void onChangeEvent(CarPropertyValue value) {
555             int propId = value.getPropertyId();
556             int areaId = value.getAreaId();
557 
558             mNumEvents.put(propId, mNumEvents.get(propId) + 1);
559 
560             String valueString = value.getValue().getClass().isArray()
561                     ? Arrays.toString((Object[]) value.getValue())
562                     : value.getValue().toString();
563 
564             mTvLogEvent.append(String.format("Event %1$s: elapsedRealtimeNanos=%2$s propId=0x%3$s "
565                     + "areaId=0x%4$s name=%5$s status=%6$s value=%7$s", mNumEvents.get(propId),
566                     value.getTimestamp(), toHexString(propId), toHexString(areaId),
567                     VehiclePropertyIds.toString(propId), value.getStatus(), valueString));
568             if (mPropSubscriptionRateHz.contains(propId)) {
569                 mTvLogEvent.append(
570                         String.format(" selected subscription rate (Hz)=%1$s "
571                                 + "actual subscription rate (Hz)=%2$s\n",
572                                 mPropSubscriptionRateHz.get(propId),
573                                 mNumEvents.get(propId) * 1000.0f / (System.currentTimeMillis()
574                                         - mStartTime.get(propId))));
575             } else {
576                 mTvLogEvent.append("\n");
577             }
578             scrollEventLogsToBottom();
579         }
580 
581         @Override
onErrorEvent(int propId, int areaId)582         public void onErrorEvent(int propId, int areaId) {
583             mTvLogEvent.append("Received error event propId=0x"
584                     + VehiclePropertyIds.toString(propId) + ", areaId=0x" + toHexString(areaId));
585             scrollEventLogsToBottom();
586         }
587     }
588 }
589