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