1 /* 2 * Copyright (C) 2023 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.internal.property; 18 19 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 20 import static com.android.car.internal.property.CarPropertyHelper.propertyIdsToString; 21 import static com.android.car.internal.util.ArrayUtils.convertToIntArray; 22 23 import android.annotation.Nullable; 24 import android.car.VehiclePropertyIds; 25 import android.util.ArrayMap; 26 import android.util.ArraySet; 27 import android.util.Log; 28 import android.util.Slog; 29 30 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 31 import com.android.car.internal.util.IndentingPrintWriter; 32 import com.android.car.internal.util.PairSparseArray; 33 34 import java.util.ArrayList; 35 import java.util.List; 36 import java.util.Objects; 37 import java.util.Set; 38 import java.util.TreeSet; 39 40 /** 41 * This class manages [{propertyId, areaId} -> RateInfoForClients] map and maintains two states: 42 * a current state and a staged state. The staged state represents the proposed changes. After 43 * the changes are applied to the lower layer, caller either uses {@link #commit} to replace 44 * the curren state with the staged state, or uses {@link #dropCommit} to drop the staged state. 45 * 46 * A common pattern is 47 * 48 * ``` 49 * synchronized (mLock) { 50 * mSubscriptionManager.stageNewOptions(...); 51 * // Optionally stage some other options. 52 * mSubscriptionManager.stageNewOptions(...); 53 * // Optionally stage unregistration. 54 * mSubscriptionManager.stageUnregister(...); 55 * 56 * mSubscriptionManager.diffBetweenCurrentAndStage(...); 57 * try { 58 * // Apply the diff. 59 * } catch (Exception e) { 60 * mSubscriptionManager.dropCommit(); 61 * throw e; 62 * } 63 * mSubscriptionManager.commit(); 64 * } 65 * ``` 66 * 67 * This class is not thread-safe. 68 * 69 * @param <ClientType> A class representing a client. 70 * 71 * @hide 72 */ 73 public final class SubscriptionManager<ClientType> { 74 private static final String TAG = SubscriptionManager.class.getSimpleName(); 75 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 76 77 private static final class RateInfo { 78 public final float updateRateHz; 79 public final boolean enableVariableUpdateRate; 80 public final float resolution; 81 RateInfo(float updateRateHz, boolean enableVariableUpdateRate, float resolution)82 RateInfo(float updateRateHz, boolean enableVariableUpdateRate, float resolution) { 83 this.updateRateHz = updateRateHz; 84 this.enableVariableUpdateRate = enableVariableUpdateRate; 85 this.resolution = resolution; 86 } 87 88 @Override toString()89 public String toString() { 90 return String.format( 91 "RateInfo{updateRateHz: %f, enableVur: %b, resolution: %f}", updateRateHz, 92 enableVariableUpdateRate, resolution); 93 } 94 95 @Override equals(Object other)96 public boolean equals(Object other) { 97 if (this == other) { 98 return true; 99 } 100 if (!(other instanceof RateInfo)) { 101 return false; 102 } 103 RateInfo that = (RateInfo) other; 104 return updateRateHz == that.updateRateHz 105 && enableVariableUpdateRate == that.enableVariableUpdateRate 106 && resolution == that.resolution; 107 } 108 109 @Override hashCode()110 public int hashCode() { 111 return Objects.hash(updateRateHz, enableVariableUpdateRate, resolution); 112 } 113 } 114 115 /** 116 * This class provides an abstraction for all the clients and their subscribed rate for a 117 * specific {propertyId, areaId} pair. 118 */ 119 private static final class RateInfoForClients<ClientType> { 120 private final ArrayMap<ClientType, RateInfo> mRateInfoByClient; 121 // An ordered set for all update rates to provide efficient add, remove and get max update 122 // rate. 123 private final TreeSet<Float> mUpdateRatesHz; 124 private final ArrayMap<Float, Integer> mClientCountByUpdateRateHz; 125 private final TreeSet<Float> mResolutions; 126 private final ArrayMap<Float, Integer> mClientCountByResolution; 127 // How many clients has enabled variable update rate. We can only enable variable update 128 // rate in the underlying layer if all clients enable VUR. 129 private int mEnableVariableUpdateRateCount; 130 RateInfoForClients()131 RateInfoForClients() { 132 mRateInfoByClient = new ArrayMap<>(); 133 mUpdateRatesHz = new TreeSet<>(); 134 mClientCountByUpdateRateHz = new ArrayMap<>(); 135 mResolutions = new TreeSet<>(); 136 mClientCountByResolution = new ArrayMap<>(); 137 } 138 RateInfoForClients(RateInfoForClients other)139 RateInfoForClients(RateInfoForClients other) { 140 mRateInfoByClient = new ArrayMap<>(other.mRateInfoByClient); 141 mUpdateRatesHz = new TreeSet<>(other.mUpdateRatesHz); 142 mClientCountByUpdateRateHz = new ArrayMap<>(other.mClientCountByUpdateRateHz); 143 mResolutions = new TreeSet<>(other.mResolutions); 144 mClientCountByResolution = new ArrayMap<>(other.mClientCountByResolution); 145 mEnableVariableUpdateRateCount = other.mEnableVariableUpdateRateCount; 146 } 147 148 /** 149 * Gets the max update rate for this {propertyId, areaId}. 150 */ getMaxUpdateRateHz()151 private float getMaxUpdateRateHz() { 152 return mUpdateRatesHz.last(); 153 } 154 155 /** 156 * Gets the min required resolution for this {propertyId, areaId}. 157 */ getMinRequiredResolution()158 private float getMinRequiredResolution() { 159 return mResolutions.first(); 160 } 161 isVariableUpdateRateEnabledForAllClients()162 private boolean isVariableUpdateRateEnabledForAllClients() { 163 return mEnableVariableUpdateRateCount == mRateInfoByClient.size(); 164 } 165 166 /** 167 * Gets the combined rate info for all clients. 168 * 169 * We use the max update rate, min required resolution, and only enable VUR if all clients 170 * enable. 171 */ getCombinedRateInfo()172 RateInfo getCombinedRateInfo() { 173 return new RateInfo(getMaxUpdateRateHz(), isVariableUpdateRateEnabledForAllClients(), 174 getMinRequiredResolution()); 175 } 176 getClients()177 Set<ClientType> getClients() { 178 return mRateInfoByClient.keySet(); 179 } 180 getUpdateRateHz(ClientType client)181 float getUpdateRateHz(ClientType client) { 182 return mRateInfoByClient.get(client).updateRateHz; 183 } 184 isVariableUpdateRateEnabled(ClientType client)185 boolean isVariableUpdateRateEnabled(ClientType client) { 186 return mRateInfoByClient.get(client).enableVariableUpdateRate; 187 } 188 getResolution(ClientType client)189 float getResolution(ClientType client) { 190 return mRateInfoByClient.get(client).resolution; 191 } 192 193 /** 194 * Adds a new client for this {propertyId, areaId}. 195 */ add(ClientType client, float updateRateHz, boolean enableVariableUpdateRate, float resolution)196 void add(ClientType client, float updateRateHz, boolean enableVariableUpdateRate, 197 float resolution) { 198 // Clear the existing updateRateHz for the client if exists. 199 remove(client); 200 // Store the new rate info. 201 mRateInfoByClient.put(client, 202 new RateInfo(updateRateHz, enableVariableUpdateRate, resolution)); 203 204 if (enableVariableUpdateRate) { 205 mEnableVariableUpdateRateCount++; 206 } 207 208 if (!mClientCountByUpdateRateHz.containsKey(updateRateHz)) { 209 mUpdateRatesHz.add(updateRateHz); 210 mClientCountByUpdateRateHz.put(updateRateHz, 1); 211 } else { 212 mClientCountByUpdateRateHz.put(updateRateHz, 213 mClientCountByUpdateRateHz.get(updateRateHz) + 1); 214 } 215 216 if (!mClientCountByResolution.containsKey(resolution)) { 217 mResolutions.add(resolution); 218 mClientCountByResolution.put(resolution, 1); 219 } else { 220 mClientCountByResolution.put(resolution, 221 mClientCountByResolution.get(resolution) + 1); 222 } 223 } 224 remove(ClientType client)225 void remove(ClientType client) { 226 if (!mRateInfoByClient.containsKey(client)) { 227 return; 228 } 229 RateInfo rateInfo = mRateInfoByClient.get(client); 230 if (rateInfo.enableVariableUpdateRate) { 231 mEnableVariableUpdateRateCount--; 232 } 233 float updateRateHz = rateInfo.updateRateHz; 234 if (mClientCountByUpdateRateHz.containsKey(updateRateHz)) { 235 int newCount = mClientCountByUpdateRateHz.get(updateRateHz) - 1; 236 if (newCount == 0) { 237 mClientCountByUpdateRateHz.remove(updateRateHz); 238 mUpdateRatesHz.remove(updateRateHz); 239 } else { 240 mClientCountByUpdateRateHz.put(updateRateHz, newCount); 241 } 242 } 243 float resolution = rateInfo.resolution; 244 if (mClientCountByResolution.containsKey(resolution)) { 245 int newCount = mClientCountByResolution.get(resolution) - 1; 246 if (newCount == 0) { 247 mClientCountByResolution.remove(resolution); 248 mResolutions.remove(resolution); 249 } else { 250 mClientCountByResolution.put(resolution, newCount); 251 } 252 } 253 254 mRateInfoByClient.remove(client); 255 } 256 isEmpty()257 boolean isEmpty() { 258 return mRateInfoByClient.isEmpty(); 259 } 260 } 261 262 PairSparseArray<RateInfoForClients<ClientType>> mCurrentRateInfoByClientByPropIdAreaId = 263 new PairSparseArray<>(); 264 PairSparseArray<RateInfoForClients<ClientType>> mStagedRateInfoByClientByPropIdAreaId = 265 new PairSparseArray<>(); 266 ArraySet<int[]> mStagedAffectedPropIdAreaIds = new ArraySet<>(); 267 268 /** 269 * Prepares new subscriptions. 270 * 271 * This apply the new subscribe options in the staging area without actually committing them. 272 * Client should call {@link #diffBetweenCurrentAndStage} to get the difference between current 273 * and the staging state. Apply them to the lower layer, and either commit the change after 274 * the operation succeeds or drop the change after the operation failed. 275 */ stageNewOptions(ClientType client, List<CarSubscription> options)276 public void stageNewOptions(ClientType client, List<CarSubscription> options) { 277 if (DBG) { 278 Slog.d(TAG, "stageNewOptions: options: " + options); 279 } 280 281 cloneCurrentToStageIfClean(); 282 283 for (int i = 0; i < options.size(); i++) { 284 CarSubscription option = options.get(i); 285 int propertyId = option.propertyId; 286 for (int areaId : option.areaIds) { 287 mStagedAffectedPropIdAreaIds.add(new int[]{propertyId, areaId}); 288 if (mStagedRateInfoByClientByPropIdAreaId.get(propertyId, areaId) == null) { 289 mStagedRateInfoByClientByPropIdAreaId.put(propertyId, areaId, 290 new RateInfoForClients<>()); 291 } 292 mStagedRateInfoByClientByPropIdAreaId.get(propertyId, areaId).add( 293 client, option.updateRateHz, option.enableVariableUpdateRate, 294 option.resolution); 295 } 296 } 297 } 298 299 /** 300 * Prepares unregistration for list of property IDs. 301 * 302 * This apply the unregistration in the staging area without actually committing them. 303 */ stageUnregister(ClientType client, ArraySet<Integer> propertyIdsToUnregister)304 public void stageUnregister(ClientType client, ArraySet<Integer> propertyIdsToUnregister) { 305 if (DBG) { 306 Slog.d(TAG, "stageUnregister: propertyIdsToUnregister: " + propertyIdsToString( 307 propertyIdsToUnregister)); 308 } 309 310 cloneCurrentToStageIfClean(); 311 312 for (int i = 0; i < propertyIdsToUnregister.size(); i++) { 313 int propertyId = propertyIdsToUnregister.valueAt(i); 314 ArraySet<Integer> areaIds = 315 mStagedRateInfoByClientByPropIdAreaId.getSecondKeysForFirstKey(propertyId); 316 for (int j = 0; j < areaIds.size(); j++) { 317 int areaId = areaIds.valueAt(j); 318 mStagedAffectedPropIdAreaIds.add(new int[]{propertyId, areaId}); 319 RateInfoForClients<ClientType> rateInfoForClients = 320 mStagedRateInfoByClientByPropIdAreaId.get(propertyId, areaId); 321 if (rateInfoForClients == null) { 322 Slog.e(TAG, "The property: " + VehiclePropertyIds.toString(propertyId) 323 + ", area ID: " + areaId + " was not registered, do nothing"); 324 continue; 325 } 326 rateInfoForClients.remove(client); 327 if (rateInfoForClients.isEmpty()) { 328 mStagedRateInfoByClientByPropIdAreaId.remove(propertyId, areaId); 329 } 330 } 331 } 332 } 333 334 /** 335 * Commit the staged changes. 336 * 337 * This will replace the current state with the staged state. This should be called after the 338 * changes are applied successfully to the lower layer. 339 */ commit()340 public void commit() { 341 if (mStagedAffectedPropIdAreaIds.isEmpty()) { 342 if (DBG) { 343 Slog.d(TAG, "No changes has been staged, nothing to commit"); 344 } 345 return; 346 } 347 // Drop the current state. 348 mCurrentRateInfoByClientByPropIdAreaId = mStagedRateInfoByClientByPropIdAreaId; 349 mStagedAffectedPropIdAreaIds.clear(); 350 } 351 352 /** 353 * Drop the staged changes. 354 * 355 * This should be called after the changes failed to apply to the lower layer. 356 */ dropCommit()357 public void dropCommit() { 358 if (mStagedAffectedPropIdAreaIds.isEmpty()) { 359 if (DBG) { 360 Slog.d(TAG, "No changes has been staged, nothing to drop"); 361 } 362 return; 363 } 364 // Drop the staged state. 365 mStagedRateInfoByClientByPropIdAreaId = mCurrentRateInfoByClientByPropIdAreaId; 366 mStagedAffectedPropIdAreaIds.clear(); 367 } 368 getCurrentSubscribedPropIds()369 public ArraySet<Integer> getCurrentSubscribedPropIds() { 370 return new ArraySet<Integer>(mCurrentRateInfoByClientByPropIdAreaId.getFirstKeys()); 371 } 372 373 /** 374 * Clear both the current state and staged state. 375 */ clear()376 public void clear() { 377 mStagedRateInfoByClientByPropIdAreaId.clear(); 378 mCurrentRateInfoByClientByPropIdAreaId.clear(); 379 mStagedAffectedPropIdAreaIds.clear(); 380 } 381 382 /** 383 * Gets all the subscription clients for the given propertyID, area ID pair. 384 * 385 * This uses the current state. 386 */ getClients(int propertyId, int areaId)387 public @Nullable Set<ClientType> getClients(int propertyId, int areaId) { 388 if (!mCurrentRateInfoByClientByPropIdAreaId.contains(propertyId, areaId)) { 389 return null; 390 } 391 return mCurrentRateInfoByClientByPropIdAreaId.get(propertyId, areaId).getClients(); 392 } 393 394 /** 395 * Dumps the state. 396 */ 397 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)398 public void dump(IndentingPrintWriter writer) { 399 writer.println("Current subscription states:"); 400 dumpStates(writer, mCurrentRateInfoByClientByPropIdAreaId); 401 writer.println("Staged subscription states:"); 402 dumpStates(writer, mStagedRateInfoByClientByPropIdAreaId); 403 } 404 405 /** 406 * Calculates the difference between the staged state and current state. 407 * 408 * @param outDiffSubscriptions The output subscriptions that has changed. This includes 409 * both new subscriptions and updated subscriptions with a new update rate. 410 * @param outPropertyIdsToUnsubscribe The output property IDs that need to be unsubscribed. 411 */ diffBetweenCurrentAndStage(List<CarSubscription> outDiffSubscriptions, List<Integer> outPropertyIdsToUnsubscribe)412 public void diffBetweenCurrentAndStage(List<CarSubscription> outDiffSubscriptions, 413 List<Integer> outPropertyIdsToUnsubscribe) { 414 if (mStagedAffectedPropIdAreaIds.isEmpty()) { 415 if (DBG) { 416 Slog.d(TAG, "No changes has been staged, no diff"); 417 } 418 return; 419 } 420 ArraySet<Integer> possiblePropIdsToUnsubscribe = new ArraySet<>(); 421 PairSparseArray<RateInfo> diffRateInfoByPropIdAreaId = new PairSparseArray<>(); 422 for (int i = 0; i < mStagedAffectedPropIdAreaIds.size(); i++) { 423 int[] propIdAreaId = mStagedAffectedPropIdAreaIds.valueAt(i); 424 int propertyId = propIdAreaId[0]; 425 int areaId = propIdAreaId[1]; 426 427 if (!mStagedRateInfoByClientByPropIdAreaId.contains(propertyId, areaId)) { 428 // The [PropertyId, areaId] is no longer subscribed. 429 if (DBG) { 430 Slog.d(TAG, String.format("The property: %s, areaId: %d is no longer " 431 + "subscribed", VehiclePropertyIds.toString(propertyId), areaId)); 432 } 433 possiblePropIdsToUnsubscribe.add(propertyId); 434 continue; 435 } 436 437 RateInfo newCombinedRateInfo = mStagedRateInfoByClientByPropIdAreaId 438 .get(propertyId, areaId).getCombinedRateInfo(); 439 440 if (!mCurrentRateInfoByClientByPropIdAreaId.contains(propertyId, areaId) 441 || !(mCurrentRateInfoByClientByPropIdAreaId 442 .get(propertyId, areaId).getCombinedRateInfo() 443 .equals(newCombinedRateInfo))) { 444 if (DBG) { 445 Slog.d(TAG, String.format( 446 "New combined subscription rate info for property: %s, areaId: %d, %s", 447 VehiclePropertyIds.toString(propertyId), areaId, newCombinedRateInfo)); 448 } 449 diffRateInfoByPropIdAreaId.put(propertyId, areaId, newCombinedRateInfo); 450 continue; 451 } 452 } 453 outDiffSubscriptions.addAll(getCarSubscription(diffRateInfoByPropIdAreaId)); 454 for (int i = 0; i < possiblePropIdsToUnsubscribe.size(); i++) { 455 int possiblePropIdToUnsubscribe = possiblePropIdsToUnsubscribe.valueAt(i); 456 if (mStagedRateInfoByClientByPropIdAreaId.getSecondKeysForFirstKey( 457 possiblePropIdToUnsubscribe).isEmpty()) { 458 // We should only unsubscribe the property if all area IDs are unsubscribed. 459 if (DBG) { 460 Slog.d(TAG, String.format( 461 "All areas for the property: %s are no longer subscribed, " 462 + "unsubscribe it", VehiclePropertyIds.toString( 463 possiblePropIdToUnsubscribe))); 464 } 465 outPropertyIdsToUnsubscribe.add(possiblePropIdToUnsubscribe); 466 } 467 } 468 } 469 470 /** 471 * Generates the {@code CarSubscription} instances. 472 * 473 * Converts [[propId, areaId] -> updateRateHz] map to 474 * [propId -> [updateRateHz -> list of areaIds]] and then generates subscribe option for each 475 * updateRateHz for each propId. 476 * 477 * @param diffRateInfoByPropIdAreaId A [[propId, areaId] -> updateRateHz] map. 478 */ getCarSubscription( PairSparseArray<RateInfo> diffRateInfoByPropIdAreaId)479 private static List<CarSubscription> getCarSubscription( 480 PairSparseArray<RateInfo> diffRateInfoByPropIdAreaId) { 481 List<CarSubscription> carSubscriptions = new ArrayList<>(); 482 ArraySet<Integer> propertyIds = diffRateInfoByPropIdAreaId.getFirstKeys(); 483 for (int propertyIdIndex = 0; propertyIdIndex < propertyIds.size(); propertyIdIndex++) { 484 int propertyId = propertyIds.valueAt(propertyIdIndex); 485 ArraySet<Integer> areaIds = diffRateInfoByPropIdAreaId.getSecondKeysForFirstKey( 486 propertyId); 487 488 // Group the areaIds by RateInfo. 489 ArrayMap<RateInfo, List<Integer>> areaIdsByRateInfo = new ArrayMap<>(); 490 for (int i = 0; i < areaIds.size(); i++) { 491 int areaId = areaIds.valueAt(i); 492 RateInfo rateInfo = diffRateInfoByPropIdAreaId.get(propertyId, areaId); 493 if (!areaIdsByRateInfo.containsKey(rateInfo)) { 494 areaIdsByRateInfo.put(rateInfo, new ArrayList<>()); 495 } 496 areaIdsByRateInfo.get(rateInfo).add(areaId); 497 } 498 499 // Convert each update rate to a new CarSubscription. 500 for (int i = 0; i < areaIdsByRateInfo.size(); i++) { 501 CarSubscription option = new CarSubscription(); 502 option.propertyId = propertyId; 503 option.areaIds = convertToIntArray(areaIdsByRateInfo.valueAt(i)); 504 option.updateRateHz = areaIdsByRateInfo.keyAt(i).updateRateHz; 505 option.enableVariableUpdateRate = 506 areaIdsByRateInfo.keyAt(i).enableVariableUpdateRate; 507 option.resolution = areaIdsByRateInfo.keyAt(i).resolution; 508 carSubscriptions.add(option); 509 } 510 } 511 512 return carSubscriptions; 513 } 514 cloneCurrentToStageIfClean()515 private void cloneCurrentToStageIfClean() { 516 if (!mStagedAffectedPropIdAreaIds.isEmpty()) { 517 // The current state is not clean, we already cloned once. We allow staging multiple 518 // commits before final commit/drop. 519 return; 520 } 521 522 mStagedRateInfoByClientByPropIdAreaId = new PairSparseArray<>(); 523 for (int i = 0; i < mCurrentRateInfoByClientByPropIdAreaId.size(); i++) { 524 int[] keyPair = mCurrentRateInfoByClientByPropIdAreaId.keyPairAt(i); 525 mStagedRateInfoByClientByPropIdAreaId.put(keyPair[0], keyPair[1], 526 new RateInfoForClients<>( 527 mCurrentRateInfoByClientByPropIdAreaId.valueAt(i))); 528 } 529 } 530 dumpStates(IndentingPrintWriter writer, PairSparseArray<RateInfoForClients<ClientType>> states)531 private static <ClientType> void dumpStates(IndentingPrintWriter writer, 532 PairSparseArray<RateInfoForClients<ClientType>> states) { 533 for (int i = 0; i < states.size(); i++) { 534 int[] propIdAreaId = states.keyPairAt(i); 535 RateInfoForClients<ClientType> rateInfoForClients = states.valueAt(i); 536 int propertyId = propIdAreaId[0]; 537 int areaId = propIdAreaId[1]; 538 Set<ClientType> clients = states.get(propertyId, areaId).getClients(); 539 writer.println("property: " + VehiclePropertyIds.toString(propertyId) 540 + ", area ID: " + areaId + " is registered by " + clients.size() 541 + " client(s)."); 542 writer.increaseIndent(); 543 for (ClientType client : clients) { 544 writer.println("Client " + client + ": Subscribed at " 545 + rateInfoForClients.getUpdateRateHz(client) + " hz" 546 + ", enableVur: " 547 + rateInfoForClients.isVariableUpdateRateEnabled(client) 548 + ", resolution: " + rateInfoForClients.getResolution(client)); 549 } 550 writer.decreaseIndent(); 551 } 552 } 553 } 554