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