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.audio;
18 
19 import static android.car.media.CarAudioManager.AUDIO_MIRROR_CAN_ENABLE;
20 import static android.car.media.CarAudioManager.AUDIO_MIRROR_OUT_OF_OUTPUT_DEVICES;
21 import static android.car.media.CarAudioManager.INVALID_REQUEST_ID;
22 
23 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
24 
25 import android.annotation.Nullable;
26 import android.car.builtin.util.Slogf;
27 import android.car.media.CarAudioManager;
28 import android.car.media.IAudioZonesMirrorStatusCallback;
29 import android.media.AudioDeviceAttributes;
30 import android.os.Handler;
31 import android.os.HandlerThread;
32 import android.os.RemoteCallbackList;
33 import android.os.RemoteException;
34 import android.util.ArraySet;
35 import android.util.LongSparseArray;
36 import android.util.SparseLongArray;
37 import android.util.proto.ProtoOutputStream;
38 
39 import com.android.car.CarLog;
40 import com.android.car.CarServiceUtils;
41 import com.android.car.audio.CarAudioDumpProto.CarAudioMirrorRequestHandlerProto;
42 import com.android.car.audio.CarAudioDumpProto.CarAudioMirrorRequestHandlerProto.RequestIdToZones;
43 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
44 import com.android.car.internal.util.IndentingPrintWriter;
45 import com.android.internal.annotations.GuardedBy;
46 import com.android.internal.util.Preconditions;
47 
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.List;
51 import java.util.Objects;
52 
53 /**
54  * Managed the car audio mirror request
55  */
56 /* package */ final class CarAudioMirrorRequestHandler {
57     private static final String TAG = CarLog.TAG_AUDIO;
58 
59     private static final String REQUEST_HANDLER_THREAD_NAME = "CarAudioMirrorRequest";
60 
61     private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
62             REQUEST_HANDLER_THREAD_NAME);
63     private final Handler mHandler = new Handler(mHandlerThread.getLooper());
64 
65     private final Object mLock = new Object();
66 
67     // Lock not needed as the callback is only broadcast inside the handler's thread
68     // If this changes then the lock may be needed to prevent concurrent calls to
69     // mAudioZonesMirrorStatusCallbacks.beginBroadcast
70     private final RemoteCallbackList<IAudioZonesMirrorStatusCallback>
71             mAudioZonesMirrorStatusCallbacks = new RemoteCallbackList<>();
72     @GuardedBy("mLock")
73     private final List<CarAudioDeviceInfo> mMirrorDevices = new ArrayList<>();
74     @GuardedBy("mLock")
75     private final SparseLongArray mZonesToMirrorRequestId = new SparseLongArray();
76     @GuardedBy("mLock")
77     private final LongSparseArray<CarAudioDeviceInfo> mRequestIdToMirrorDevice =
78             new LongSparseArray<>();
79 
80     @GuardedBy("mLock")
81     private final LongSparseArray<int[]> mRequestIdToZones = new LongSparseArray<>();
82 
83     private final RequestIdGenerator mRequestIdGenerator = new RequestIdGenerator();
84 
registerAudioZonesMirrorStatusCallback( IAudioZonesMirrorStatusCallback callback)85     boolean registerAudioZonesMirrorStatusCallback(
86             IAudioZonesMirrorStatusCallback callback) {
87         Objects.requireNonNull(callback, "Audio zones mirror status callback can not be null");
88 
89         if (!isMirrorAudioEnabled()) {
90             Slogf.w(TAG, "Could not register audio mirror status callback, mirroring not enabled");
91             return false;
92         }
93 
94         return mAudioZonesMirrorStatusCallbacks.register(callback);
95     }
96 
unregisterAudioZonesMirrorStatusCallback(IAudioZonesMirrorStatusCallback callback)97     boolean unregisterAudioZonesMirrorStatusCallback(IAudioZonesMirrorStatusCallback callback) {
98         Objects.requireNonNull(callback, "Audio zones mirror status callback can not be null");
99 
100         return mAudioZonesMirrorStatusCallbacks.unregister(callback);
101     }
102 
isMirrorAudioEnabled()103     boolean isMirrorAudioEnabled() {
104         synchronized (mLock) {
105             return !mMirrorDevices.isEmpty();
106         }
107     }
108 
setMirrorDeviceInfos(List<CarAudioDeviceInfo> mirroringDevices)109     void setMirrorDeviceInfos(List<CarAudioDeviceInfo> mirroringDevices) {
110         Objects.requireNonNull(mirroringDevices, "Mirror devices can not be null");
111 
112         synchronized (mLock) {
113             mMirrorDevices.clear();
114             mMirrorDevices.addAll(mirroringDevices);
115         }
116     }
117 
getMirroringDeviceInfos()118     List<CarAudioDeviceInfo> getMirroringDeviceInfos() {
119         synchronized (mLock) {
120             return List.copyOf(mMirrorDevices);
121         }
122     }
123 
124     @Nullable
getAudioDevice(long requestId)125     AudioDeviceAttributes getAudioDevice(long requestId) {
126         Preconditions.checkArgument(requestId != INVALID_REQUEST_ID,
127                 "Request id for device can not be INVALID_REQUEST_ID");
128         synchronized (mLock) {
129             int index = mRequestIdToMirrorDevice.indexOfKey(requestId);
130             if (index < 0) {
131                 return null;
132             }
133             return mRequestIdToMirrorDevice.valueAt(index).getAudioDevice();
134         }
135     }
136 
enableMirrorForZones(long requestId, int[] audioZones)137     void enableMirrorForZones(long requestId, int[] audioZones) {
138         Objects.requireNonNull(audioZones, "Mirror audio zones can not be null");
139         Preconditions.checkArgument(requestId != INVALID_REQUEST_ID,
140                 "Request id can not be INVALID_REQUEST_ID");
141         synchronized (mLock) {
142             mRequestIdToZones.put(requestId, audioZones);
143             for (int index = 0; index < audioZones.length; index++) {
144                 mZonesToMirrorRequestId.put(audioZones[index], requestId);
145             }
146         }
147         mHandler.post(() ->
148                 handleInformCallbacks(audioZones, CarAudioManager.AUDIO_REQUEST_STATUS_APPROVED));
149     }
150 
handleInformCallbacks(int[] audioZones, int status)151     private void handleInformCallbacks(int[] audioZones, int status) {
152         int n = mAudioZonesMirrorStatusCallbacks.beginBroadcast();
153         for (int c = 0; c < n; c++) {
154             IAudioZonesMirrorStatusCallback callback =
155                     mAudioZonesMirrorStatusCallbacks.getBroadcastItem(c);
156             try {
157                 // Calling binder inside lock here since the call is one way and doest not block.
158                 // The lock is needed to prevent concurrent beginBroadcast
159                 callback.onAudioZonesMirrorStatusChanged(audioZones, status);
160             } catch (RemoteException e) {
161                 Slogf.e(TAG, e, "Could not inform mirror status callback index %d of total %d",
162                         c, n);
163             }
164 
165         }
166         mAudioZonesMirrorStatusCallbacks.finishBroadcast();
167     }
168 
169     @Nullable
getMirrorAudioZonesForRequest(long requestId)170     int[] getMirrorAudioZonesForRequest(long requestId) {
171         synchronized (mLock) {
172             return mRequestIdToZones.get(requestId, /* valueIfKeyNotFound= */ null);
173         }
174     }
175 
isMirrorEnabledForZone(int zoneId)176     boolean isMirrorEnabledForZone(int zoneId) {
177         synchronized (mLock) {
178             return mZonesToMirrorRequestId.get(zoneId, INVALID_REQUEST_ID) != INVALID_REQUEST_ID;
179         }
180     }
181 
rejectMirrorForZones(long requestId, int[] audioZones)182     void rejectMirrorForZones(long requestId, int[] audioZones) {
183         Objects.requireNonNull(audioZones, "Rejected audio zones can not be null");
184         Preconditions.checkArgument(audioZones.length > 1,
185                 "Rejected audio zones must be greater than one");
186 
187         synchronized (mLock) {
188             releaseRequestIdLocked(requestId);
189         }
190         mHandler.post(() ->
191                 handleInformCallbacks(audioZones, CarAudioManager.AUDIO_REQUEST_STATUS_REJECTED));
192     }
193 
updateRemoveMirrorConfigurationForZones(long requestId, int[] newConfig)194     void updateRemoveMirrorConfigurationForZones(long requestId, int[] newConfig) {
195         ArraySet<Integer> newConfigSet = CarServiceUtils.toIntArraySet(newConfig);
196         ArrayList<Integer> delta = new ArrayList<>();
197         synchronized (mLock) {
198             int[] prevConfig = mRequestIdToZones.get(requestId, new int[0]);
199             for (int index = 0; index < prevConfig.length; index++) {
200                 int zoneId = prevConfig[index];
201                 mZonesToMirrorRequestId.delete(zoneId);
202                 if (newConfigSet.contains(zoneId)) {
203                     continue;
204                 }
205                 delta.add(zoneId);
206             }
207             if (newConfig.length == 0) {
208                 mRequestIdToZones.remove(requestId);
209                 releaseRequestIdLocked(requestId);
210             } else {
211                 mRequestIdToZones.put(requestId, newConfig);
212             }
213             for (int index = 0; index < newConfig.length; index++) {
214                 int zoneId = newConfig[index];
215                 mZonesToMirrorRequestId.put(zoneId, requestId);
216             }
217         }
218         mHandler.post(() ->
219                 handleInformCallbacks(CarServiceUtils.toIntArray(delta),
220                         CarAudioManager.AUDIO_REQUEST_STATUS_STOPPED));
221     }
222 
223     /**
224      * Return the difference between the audio zones ids to remove and the current for the request
225      * id. This can be used to determine how a configuration should change when some zones
226      * are removed.
227      */
228     @Nullable
calculateAudioConfigurationAfterRemovingZonesFromRequestId(long requestId, int[] audioZoneIdsToRemove)229     int[] calculateAudioConfigurationAfterRemovingZonesFromRequestId(long requestId,
230             int[] audioZoneIdsToRemove) {
231         Objects.requireNonNull(audioZoneIdsToRemove, "Audio zone ids to remove must not be null");
232         Preconditions.checkArgument(audioZoneIdsToRemove.length > 0,
233                 "audio zones ids to remove must not empty");
234         ArraySet<Integer> zonesToRemove = CarServiceUtils.toIntArraySet(audioZoneIdsToRemove);
235         int[] oldConfig;
236 
237         synchronized (mLock) {
238             oldConfig = mRequestIdToZones.get(requestId, /* valueIfKeyNotFound= */ null);
239         }
240 
241         if (oldConfig == null) {
242             Slogf.w(TAG, "calculateAudioConfigurationAfterRemovingZonesFromRequestId Request "
243                     + "id %d is no longer valid");
244             return null;
245         }
246 
247         ArrayList<Integer> newConfig = new ArrayList<>();
248         for (int index = 0; index < oldConfig.length; index++) {
249             int zoneId = oldConfig[index];
250             if (zonesToRemove.contains(zoneId)) {
251                 continue;
252             }
253             newConfig.add(zoneId);
254         }
255 
256         return CarServiceUtils.toIntArray(newConfig);
257     }
258 
getUniqueRequestIdAndAssignMirrorDevice()259     long getUniqueRequestIdAndAssignMirrorDevice() {
260         long requestId = mRequestIdGenerator.generateUniqueRequestId();
261         synchronized (mLock) {
262             if (assignAvailableDeviceToRequestIdLocked(requestId)) {
263                 return requestId;
264             }
265             releaseRequestIdLocked(requestId);
266         }
267         return INVALID_REQUEST_ID;
268     }
269 
getRequestIdForAudioZone(int audioZoneId)270     long getRequestIdForAudioZone(int audioZoneId) {
271         synchronized (mLock) {
272             return mZonesToMirrorRequestId.get(audioZoneId, INVALID_REQUEST_ID);
273         }
274     }
275 
verifyValidRequestId(long requestId)276     void verifyValidRequestId(long requestId) {
277         synchronized (mLock) {
278             Preconditions.checkArgument(mRequestIdToZones.indexOfKey(requestId) >= 0,
279                     "Mirror request id " + requestId + " is not valid");
280         }
281     }
282 
canEnableAudioMirror()283     int canEnableAudioMirror() {
284         synchronized (mLock) {
285             return mRequestIdToMirrorDevice.size() < mMirrorDevices.size()
286                     ? AUDIO_MIRROR_CAN_ENABLE : AUDIO_MIRROR_OUT_OF_OUTPUT_DEVICES;
287         }
288     }
289 
290     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)291     void dump(IndentingPrintWriter writer) {
292         writer.printf("Is audio mirroring enabled? %s\n", isMirrorAudioEnabled() ? "Yes" : "No");
293         if (!isMirrorAudioEnabled()) {
294             return;
295         }
296         writer.increaseIndent();
297         int registeredCount = mAudioZonesMirrorStatusCallbacks.getRegisteredCallbackCount();
298         synchronized (mLock) {
299             writer.println("Mirroring device info:");
300             dumpMirrorDeviceInfosLocked(writer);
301             writer.printf("Registered callback count: %d\n", registeredCount);
302             dumpMirroringConfigurationsLocked(writer);
303             dumpZonesToIdMappingLocked(writer);
304             dumpMirrorDeviceMappingLocked(writer);
305         }
306         writer.decreaseIndent();
307     }
308 
309     @GuardedBy("mLock")
310     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpMirroringConfigurationsLocked(IndentingPrintWriter writer)311     private void dumpMirroringConfigurationsLocked(IndentingPrintWriter writer) {
312         writer.println("Mirroring configurations:");
313         writer.increaseIndent();
314         for (int index = 0; index < mRequestIdToZones.size(); index++) {
315             writer.printf("Audio zone request id %d: %s\n", mRequestIdToZones.keyAt(index),
316                     Arrays.toString(mRequestIdToZones.valueAt(index)));
317         }
318         writer.decreaseIndent();
319     }
320 
321     @GuardedBy("mLock")
322     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpZonesToIdMappingLocked(IndentingPrintWriter writer)323     private void dumpZonesToIdMappingLocked(IndentingPrintWriter writer) {
324         writer.println("Mirroring zone to id mapping:");
325         writer.increaseIndent();
326         for (int index = 0; index < mZonesToMirrorRequestId.size(); index++) {
327             writer.printf("Audio zone %d: request id %d\n",
328                     mZonesToMirrorRequestId.keyAt(index),
329                     mZonesToMirrorRequestId.valueAt(index));
330         }
331         writer.decreaseIndent();
332     }
333 
334     @GuardedBy("mLock")
335     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpMirrorDeviceMappingLocked(IndentingPrintWriter writer)336     private void dumpMirrorDeviceMappingLocked(IndentingPrintWriter writer) {
337         writer.println("Mirroring device to id mapping:");
338         writer.increaseIndent();
339         for (int index = 0; index < mRequestIdToMirrorDevice.size(); index++) {
340             writer.printf("Mirror device %s: request id %d\n",
341                     mRequestIdToMirrorDevice.valueAt(index),
342                     mRequestIdToMirrorDevice.keyAt(index));
343         }
344         writer.decreaseIndent();
345     }
346 
347     @GuardedBy("mLock")
348     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpMirrorDeviceInfosLocked(IndentingPrintWriter writer)349     private void dumpMirrorDeviceInfosLocked(IndentingPrintWriter writer) {
350         for (int index = 0; index < mMirrorDevices.size(); index++) {
351             writer.printf("Mirror device[%d]\n", index);
352             writer.increaseIndent();
353             mMirrorDevices.get(index).dump(writer);
354             writer.decreaseIndent();
355         }
356     }
357 
358     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)359     void dumpProto(ProtoOutputStream proto) {
360         long mirrorRequestHandlerToken = proto.start(CarAudioDumpProto
361                 .CAR_AUDIO_MIRROR_REQUEST_HANDLER);
362         proto.write(CarAudioMirrorRequestHandlerProto.IS_MIRROR_AUDIO_ENABLED,
363                 isMirrorAudioEnabled());
364         if (!isMirrorAudioEnabled()) {
365             proto.end(mirrorRequestHandlerToken);
366             return;
367         }
368 
369         int registeredCount = mAudioZonesMirrorStatusCallbacks.getRegisteredCallbackCount();
370         synchronized (mLock) {
371             dumpProtoMirrorDeviceInfosLocked(proto);
372             proto.write(CarAudioMirrorRequestHandlerProto.REGISTER_COUNT, registeredCount);
373             dumpProtoMirroringConfigurationsLocked(proto);
374             dumpProtoMirrorDeviceMappingLocked(proto);
375         }
376         proto.end(mirrorRequestHandlerToken);
377     }
378 
379     @GuardedBy("mLock")
380     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProtoMirrorDeviceInfosLocked(ProtoOutputStream proto)381     private void dumpProtoMirrorDeviceInfosLocked(ProtoOutputStream proto) {
382         for (int index = 0; index < mMirrorDevices.size(); index++) {
383             mMirrorDevices.get(index).dumpProto(CarAudioMirrorRequestHandlerProto
384                     .MIRROR_DEVICE_INFOS, proto);
385         }
386     }
387 
388     @GuardedBy("mLock")
389     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProtoMirroringConfigurationsLocked(ProtoOutputStream proto)390     private void dumpProtoMirroringConfigurationsLocked(ProtoOutputStream proto) {
391         for (int index = 0; index < mRequestIdToZones.size(); index++) {
392             long configurationToken = proto.start(
393                     CarAudioMirrorRequestHandlerProto.MIRRORING_CONFIGURATIONS);
394             proto.write(RequestIdToZones.REQUEST_ID, mRequestIdToZones.keyAt(index));
395             for (int zoneIndex = 0; zoneIndex < mRequestIdToZones.valueAt(index).length;
396                     zoneIndex++) {
397                 proto.write(RequestIdToZones.ZONE_IDS, mRequestIdToZones.valueAt(index)[zoneIndex]);
398             }
399             proto.end(configurationToken);
400         }
401     }
402 
403     @GuardedBy("mLock")
404     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProtoMirrorDeviceMappingLocked(ProtoOutputStream proto)405     private void dumpProtoMirrorDeviceMappingLocked(ProtoOutputStream proto) {
406         for (int index = 0; index < mRequestIdToMirrorDevice.size(); index++) {
407             long mirrorDeviceMappingToken = proto.start(
408                     CarAudioMirrorRequestHandlerProto.MIRROR_DEVICE_MAPPINGS);
409             mRequestIdToMirrorDevice.valueAt(index).dumpProto(CarAudioMirrorRequestHandlerProto
410                     .RequestIdToMirrorDevice.MIRROR_DEVICE, proto);
411             proto.write(CarAudioMirrorRequestHandlerProto.RequestIdToMirrorDevice.REQUEST_ID,
412                     mRequestIdToMirrorDevice.keyAt(index));
413             proto.end(mirrorDeviceMappingToken);
414         }
415     }
416 
417     @GuardedBy("mLock")
assignAvailableDeviceToRequestIdLocked(long requestId)418     private boolean assignAvailableDeviceToRequestIdLocked(long requestId) {
419         for (int index = 0; index < mMirrorDevices.size(); index++) {
420             CarAudioDeviceInfo info = mMirrorDevices.get(index);
421             if (mRequestIdToMirrorDevice.indexOfValue(info) >= 0) {
422                 continue;
423             }
424             mRequestIdToMirrorDevice.put(requestId, info);
425             return true;
426         }
427         return false;
428     }
429 
430     @GuardedBy("mLock")
releaseRequestIdLocked(long requestId)431     private void releaseRequestIdLocked(long requestId) {
432         mRequestIdGenerator.releaseRequestId(requestId);
433         synchronized (mLock) {
434             mRequestIdToMirrorDevice.remove(requestId);
435         }
436     }
437 }
438