• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.telemetry.publisher;
18 
19 import static com.android.car.telemetry.CarTelemetryService.DEBUG;
20 
21 import static java.lang.Integer.toHexString;
22 
23 import android.annotation.NonNull;
24 import android.car.VehiclePropertyIds;
25 import android.car.builtin.util.Slogf;
26 import android.car.hardware.CarPropertyConfig;
27 import android.car.hardware.CarPropertyValue;
28 import android.car.hardware.property.CarPropertyEvent;
29 import android.car.hardware.property.ICarPropertyEventListener;
30 import android.car.telemetry.TelemetryProto;
31 import android.car.telemetry.TelemetryProto.Publisher.PublisherCase;
32 import android.hardware.automotive.vehicle.VehiclePropertyType;
33 import android.os.Handler;
34 import android.os.PersistableBundle;
35 import android.os.RemoteException;
36 import android.util.ArraySet;
37 import android.util.SparseArray;
38 
39 import com.android.car.CarLog;
40 import com.android.car.CarPropertyService;
41 import com.android.car.telemetry.databroker.DataSubscriber;
42 import com.android.car.telemetry.sessioncontroller.SessionAnnotation;
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.internal.util.Preconditions;
45 
46 import java.nio.charset.StandardCharsets;
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * Publisher for Vehicle Property changes, aka {@code CarPropertyService}.
52  *
53  * <p> When a subscriber is added, it registers a car property change listener for the
54  * property id of the subscriber and starts pushing the change events to the subscriber.
55  */
56 public class VehiclePropertyPublisher extends AbstractPublisher {
57 
58     private final CarPropertyService mCarPropertyService;
59     private final Handler mTelemetryHandler;
60     // The class only reads, no need to synchronize this object.
61     // Maps property_id to CarPropertyConfig.
62     private final SparseArray<CarPropertyConfig> mCarPropertyList;
63     private final ICarPropertyEventListener mCarPropertyEventListener =
64             new ICarPropertyEventListener.Stub() {
65                 @Override
66                 public void onEvent(List<CarPropertyEvent> events) throws RemoteException {
67                     if (DEBUG) {
68                         Slogf.d(CarLog.TAG_TELEMETRY,
69                                 "Received " + events.size() + " vehicle property events");
70                     }
71                     for (CarPropertyEvent event : events) {
72                         onVehicleEvent(event);
73                     }
74                 }
75             };
76 
77     // Maps property id to the PropertyData containing batch of its bundles and subscribers.
78     // Each property is batched separately.
79     private final SparseArray<PropertyData> mPropertyDataLookup = new SparseArray<>();
80     private long mBatchIntervalMillis = 100L;  // Batch every 100 milliseconds = 10Hz
81 
VehiclePropertyPublisher( @onNull CarPropertyService carPropertyService, @NonNull PublisherListener listener, @NonNull Handler handler)82     public VehiclePropertyPublisher(
83             @NonNull CarPropertyService carPropertyService,
84             @NonNull PublisherListener listener,
85             @NonNull Handler handler) {
86         super(listener);
87         mCarPropertyService = carPropertyService;
88         mTelemetryHandler = handler;
89         // Load car property list once, as the list doesn't change runtime.
90         List<CarPropertyConfig> propertyList = mCarPropertyService.getPropertyList()
91                 .getConfigs();
92         mCarPropertyList = new SparseArray<>(propertyList.size());
93         for (CarPropertyConfig property : propertyList) {
94             mCarPropertyList.append(property.getPropertyId(), property);
95         }
96     }
97 
98     @Override
addDataSubscriber(@onNull DataSubscriber subscriber)99     public void addDataSubscriber(@NonNull DataSubscriber subscriber) {
100         TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
101         Preconditions.checkArgument(
102                 publisherParam.getPublisherCase()
103                         == TelemetryProto.Publisher.PublisherCase.VEHICLE_PROPERTY,
104                 "Subscribers only with VehicleProperty publisher are supported by this class.");
105         int propertyId = publisherParam.getVehicleProperty().getVehiclePropertyId();
106         CarPropertyConfig config = mCarPropertyList.get(propertyId);
107         Preconditions.checkArgument(
108                 config != null,
109                 "Vehicle property " + VehiclePropertyIds.toString(propertyId) + " not found.");
110         Preconditions.checkArgument(
111                 config.getAccess() == CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ
112                         || config.getAccess()
113                         == CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ_WRITE,
114                 "No access. Cannot read " + VehiclePropertyIds.toString(propertyId) + ".");
115         PropertyData propertyData = mPropertyDataLookup.get(propertyId);
116         if (propertyData == null) {
117             propertyData = new PropertyData(config);
118             mPropertyDataLookup.put(propertyId, propertyData);
119         }
120         if (propertyData.subscribers.isEmpty()) {
121             mCarPropertyService.registerListener(
122                     propertyId,
123                     publisherParam.getVehicleProperty().getReadRate(),
124                     mCarPropertyEventListener);
125         }
126         propertyData.subscribers.add(subscriber);
127     }
128 
129     @Override
removeDataSubscriber(@onNull DataSubscriber subscriber)130     public void removeDataSubscriber(@NonNull DataSubscriber subscriber) {
131         TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
132         if (publisherParam.getPublisherCase() != PublisherCase.VEHICLE_PROPERTY) {
133             Slogf.w(CarLog.TAG_TELEMETRY,
134                     "Expected VEHICLE_PROPERTY publisher, but received "
135                             + publisherParam.getPublisherCase().name());
136             return;
137         }
138         int propertyId = publisherParam.getVehicleProperty().getVehiclePropertyId();
139         PropertyData propertyData = mPropertyDataLookup.get(propertyId);
140         if (propertyData == null) {
141             return;
142         }
143         propertyData.subscribers.remove(subscriber);
144         if (propertyData.subscribers.isEmpty()) {
145             mPropertyDataLookup.remove(propertyId);
146             // Doesn't throw exception as listener is not null. mCarPropertyService and
147             // local mCarPropertyToSubscribers will not get out of sync.
148             mCarPropertyService.unregisterListener(propertyId, mCarPropertyEventListener);
149         }
150     }
151 
152     @Override
removeAllDataSubscribers()153     public void removeAllDataSubscribers() {
154         for (int i = 0; i < mPropertyDataLookup.size(); i++) {
155             // Doesn't throw exception as listener is not null. mCarPropertyService and
156             // local mCarPropertyToSubscribers will not get out of sync.
157             mCarPropertyService.unregisterListener(
158                     mPropertyDataLookup.keyAt(i), mCarPropertyEventListener);
159         }
160         mPropertyDataLookup.clear();
161     }
162 
163     @Override
hasDataSubscriber(@onNull DataSubscriber subscriber)164     public boolean hasDataSubscriber(@NonNull DataSubscriber subscriber) {
165         TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
166         if (publisherParam.getPublisherCase() != PublisherCase.VEHICLE_PROPERTY) {
167             return false;
168         }
169         int propertyId = publisherParam.getVehicleProperty().getVehiclePropertyId();
170         return mPropertyDataLookup.contains(propertyId)
171                 && mPropertyDataLookup.get(propertyId).subscribers.contains(subscriber);
172     }
173 
174     @VisibleForTesting
setBatchIntervalMillis(long intervalMillis)175     public void setBatchIntervalMillis(long intervalMillis) {
176         mBatchIntervalMillis = intervalMillis;
177     }
178 
179     @Override
handleSessionStateChange(SessionAnnotation annotation)180     protected void handleSessionStateChange(SessionAnnotation annotation) {}
181 
182     /**
183      * Called when publisher receives new event. It's executed on a CarPropertyService's
184      * worker thread.
185      */
onVehicleEvent(@onNull CarPropertyEvent event)186     private void onVehicleEvent(@NonNull CarPropertyEvent event) {
187         // move the work from CarPropertyService's worker thread to the telemetry thread
188         mTelemetryHandler.post(() -> {
189             CarPropertyValue propValue = event.getCarPropertyValue();
190             int propertyId = propValue.getPropertyId();
191             PropertyData propertyData = mPropertyDataLookup.get(propertyId);
192             PersistableBundle bundle = parseCarPropertyValue(
193                     propValue, propertyData.config.getConfigArray());
194             propertyData.pendingData.add(bundle);
195             if (propertyData.pendingData.size() == 1) {
196                 mTelemetryHandler.postDelayed(
197                         () -> {
198                             pushPendingDataToSubscribers(propertyData);
199                         },
200                         mBatchIntervalMillis);
201             }
202         });
203     }
204 
205     /**
206      * Pushes bundle batch to subscribers and resets batch.
207      */
pushPendingDataToSubscribers(PropertyData propertyData)208     private void pushPendingDataToSubscribers(PropertyData propertyData) {
209         for (DataSubscriber subscriber : propertyData.subscribers) {
210             subscriber.push(propertyData.pendingData);
211         }
212         propertyData.pendingData = new ArrayList<>();
213     }
214 
215     /**
216      * Parses the car property value into a PersistableBundle.
217      */
parseCarPropertyValue( CarPropertyValue propValue, List<Integer> configArray)218     private PersistableBundle parseCarPropertyValue(
219             CarPropertyValue propValue, List<Integer> configArray) {
220         PersistableBundle bundle = new PersistableBundle();
221         bundle.putLong(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_TIMESTAMP, propValue.getTimestamp());
222         bundle.putInt(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_PROP_ID, propValue.getPropertyId());
223         bundle.putInt(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_AREA_ID, propValue.getAreaId());
224         bundle.putInt(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_STATUS, propValue.getStatus());
225         int type = propValue.getPropertyId() & VehiclePropertyType.MASK;
226         Object value = propValue.getValue();
227         if (VehiclePropertyType.BOOLEAN == type) {
228             bundle.putBoolean(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_BOOLEAN, (Boolean) value);
229         } else if (VehiclePropertyType.FLOAT == type) {
230             bundle.putDouble(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_FLOAT,
231                     ((Float) value).doubleValue());
232         } else if (VehiclePropertyType.INT32 == type) {
233             bundle.putInt(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_INT, (Integer) value);
234         } else if (VehiclePropertyType.INT64 == type) {
235             bundle.putLong(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_LONG, (Long) value);
236         } else if (VehiclePropertyType.FLOAT_VEC == type) {
237             Float[] floats = (Float[]) value;
238             double[] doubles = new double[floats.length];
239             for (int i = 0; i < floats.length; i++) {
240                 doubles[i] = floats[i].doubleValue();
241             }
242             bundle.putDoubleArray(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_FLOAT_ARRAY, doubles);
243         } else if (VehiclePropertyType.INT32_VEC == type) {
244             Integer[] integers = (Integer[]) value;
245             int[] ints = new int[integers.length];
246             for (int i = 0; i < integers.length; i++) {
247                 ints[i] = integers[i];
248             }
249             bundle.putIntArray(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_INT_ARRAY, ints);
250         } else if (VehiclePropertyType.INT64_VEC == type) {
251             Long[] oldLongs = (Long[]) value;
252             long[] longs = new long[oldLongs.length];
253             for (int i = 0; i < oldLongs.length; i++) {
254                 longs[i] = oldLongs[i];
255             }
256             bundle.putLongArray(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_LONG_ARRAY, longs);
257         } else if (VehiclePropertyType.STRING == type) {
258             bundle.putString(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_STRING, (String) value);
259         } else if (VehiclePropertyType.BYTES == type) {
260             bundle.putString(
261                     Constants.VEHICLE_PROPERTY_BUNDLE_KEY_BYTE_ARRAY,
262                     new String((byte[]) value, StandardCharsets.UTF_8));
263         } else if (VehiclePropertyType.MIXED == type) {
264             Object[] mixed = (Object[]) value;
265             int k = 0;
266             if (configArray.get(0) == 1) {  // Has single String
267                 bundle.putString(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_STRING, (String) mixed[k++]);
268             }
269             if (configArray.get(1) == 1) {  // Has single Boolean
270                 bundle.putBoolean(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_BOOLEAN,
271                         (Boolean) mixed[k++]);
272             }
273             if (configArray.get(2) == 1) {  // Has single Integer
274                 bundle.putInt(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_INT, (Integer) mixed[k++]);
275             }
276             if (configArray.get(3) != 0) {  // Integer[] length is non-zero
277                 int[] ints = new int[configArray.get(3)];
278                 for (int i = 0; i < configArray.get(3); i++) {
279                     ints[i] = (Integer) mixed[k++];
280                 }
281                 bundle.putIntArray(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_INT_ARRAY, ints);
282             }
283             if (configArray.get(4) == 1) {  // Has single Long
284                 bundle.putLong(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_LONG, (Long) mixed[k++]);
285             }
286             if (configArray.get(5) != 0) {  // Long[] length is non-zero
287                 long[] longs = new long[configArray.get(5)];
288                 for (int i = 0; i < configArray.get(5); i++) {
289                     longs[i] = (Long) mixed[k++];
290                 }
291                 bundle.putLongArray(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_LONG_ARRAY, longs);
292             }
293             if (configArray.get(6) == 1) {  // Has single Float
294                 bundle.putDouble(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_FLOAT,
295                         ((Float) mixed[k++]).doubleValue());
296             }
297             if (configArray.get(7) != 0) {  // Float[] length is non-zero
298                 double[] doubles = new double[configArray.get(7)];
299                 for (int i = 0; i < configArray.get(7); i++) {
300                     doubles[i] = ((Float) mixed[k++]).doubleValue();
301                 }
302                 bundle.putDoubleArray(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_FLOAT_ARRAY, doubles);
303             }
304             if (configArray.get(8) != 0) {  // Byte[] length is non-zero
305                 byte[] bytes = new byte[configArray.get(8)];
306                 for (int i = 0; i < configArray.get(8); i++) {
307                     bytes[i] = (Byte) mixed[k++];
308                 }
309                 bundle.putString(Constants.VEHICLE_PROPERTY_BUNDLE_KEY_BYTE_ARRAY,
310                         new String(bytes, StandardCharsets.UTF_8));
311             }
312         } else {
313             throw new IllegalArgumentException(
314                     "Unexpected property type: " + toHexString(type));
315         }
316         return bundle;
317     }
318 
319     /**
320      * Container class holding all the relevant information for a property.
321      */
322     private static final class PropertyData {
323         // The config containing info on how to parse the property value.
324         public final CarPropertyConfig config;
325         // Subscribers subscribed to the property this PropertyData is mapped to.
326         public final ArraySet<DataSubscriber> subscribers = new ArraySet<>();
327         // The list of bundles that are batched together and pushed to subscribers
328         public List<PersistableBundle> pendingData = new ArrayList<>();
329 
PropertyData(CarPropertyConfig propConfig)330         PropertyData(CarPropertyConfig propConfig) {
331             config = propConfig;
332         }
333     }
334 }
335