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