| /* |
| * Copyright (C) 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.car.audio; |
| |
| import static android.car.media.CarAudioManager.INVALID_REQUEST_ID; |
| |
| import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; |
| |
| import android.annotation.Nullable; |
| import android.car.builtin.util.Slogf; |
| import android.car.media.CarAudioManager; |
| import android.car.media.IAudioZonesMirrorStatusCallback; |
| import android.media.AudioDeviceInfo; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.RemoteCallbackList; |
| import android.os.RemoteException; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.SparseLongArray; |
| |
| import com.android.car.CarLog; |
| import com.android.car.CarServiceUtils; |
| import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; |
| import com.android.car.internal.util.IndentingPrintWriter; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.util.Preconditions; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Objects; |
| |
| /** |
| * Managed the car audio mirror request |
| */ |
| /* package */ final class CarAudioMirrorRequestHandler { |
| private static final String TAG = CarLog.TAG_AUDIO; |
| |
| private static final String REQUEST_HANDLER_THREAD_NAME = "CarAudioMirrorRequest"; |
| |
| private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread( |
| REQUEST_HANDLER_THREAD_NAME); |
| private final Handler mHandler = new Handler(mHandlerThread.getLooper()); |
| |
| private final Object mLock = new Object(); |
| |
| // Lock not needed as the callback is only broadcast inside the handler's thread |
| // If this changes then the lock may be needed to prevent concurrent calls to |
| // mAudioZonesMirrorStatusCallbacks.beginBroadcast |
| private final RemoteCallbackList<IAudioZonesMirrorStatusCallback> |
| mAudioZonesMirrorStatusCallbacks = new RemoteCallbackList<>(); |
| @GuardedBy("mLock") |
| private final List<CarAudioDeviceInfo> mMirrorDevices = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private final SparseLongArray mZonesToMirrorRequestId = new SparseLongArray(); |
| |
| @GuardedBy("mLock") |
| private final ArrayMap<Long, int[]> mRequestIdToZones = new ArrayMap<>(); |
| |
| private final RequestIdGenerator mRequestIdGenerator = new RequestIdGenerator(); |
| |
| boolean registerAudioZonesMirrorStatusCallback( |
| IAudioZonesMirrorStatusCallback callback) { |
| Objects.requireNonNull(callback, "Audio zones mirror status callback can not be null"); |
| |
| if (!isMirrorAudioEnabled()) { |
| Slogf.w(TAG, "Could not register audio mirror status callback, mirroring not enabled"); |
| return false; |
| } |
| |
| return mAudioZonesMirrorStatusCallbacks.register(callback); |
| } |
| |
| boolean unregisterAudioZonesMirrorStatusCallback(IAudioZonesMirrorStatusCallback callback) { |
| Objects.requireNonNull(callback, "Audio zones mirror status callback can not be null"); |
| |
| return mAudioZonesMirrorStatusCallbacks.unregister(callback); |
| } |
| |
| boolean isMirrorAudioEnabled() { |
| synchronized (mLock) { |
| return !mMirrorDevices.isEmpty(); |
| } |
| } |
| |
| void setMirrorDeviceInfos(List<CarAudioDeviceInfo> mirroringDevices) { |
| Objects.requireNonNull(mirroringDevices, "Mirror devices can not be null"); |
| |
| synchronized (mLock) { |
| mMirrorDevices.clear(); |
| mMirrorDevices.addAll(mirroringDevices); |
| } |
| } |
| |
| List<CarAudioDeviceInfo> getMirroringDeviceInfos() { |
| synchronized (mLock) { |
| return List.copyOf(mMirrorDevices); |
| } |
| } |
| |
| @Nullable AudioDeviceInfo getAudioDeviceInfo() { |
| //TODO (b/265973263): Replace 0 index with a request id indexing scheme |
| synchronized (mLock) { |
| return mMirrorDevices.isEmpty() ? null : mMirrorDevices.get(0).getAudioDeviceInfo(); |
| } |
| } |
| |
| void enableMirrorForZones(long requestId, int[] audioZones) { |
| Objects.requireNonNull(audioZones, "Mirror audio zones can not be null"); |
| Preconditions.checkArgument(requestId != INVALID_REQUEST_ID, |
| "Request id can not be INVALID_REQUEST_ID"); |
| synchronized (mLock) { |
| mRequestIdToZones.put(requestId, audioZones); |
| for (int index = 0; index < audioZones.length; index++) { |
| mZonesToMirrorRequestId.put(audioZones[index], requestId); |
| } |
| } |
| mHandler.post(() -> |
| handleInformCallbacks(audioZones, CarAudioManager.AUDIO_REQUEST_STATUS_APPROVED)); |
| } |
| |
| private void handleInformCallbacks(int[] audioZones, int status) { |
| int n = mAudioZonesMirrorStatusCallbacks.beginBroadcast(); |
| for (int c = 0; c < n; c++) { |
| IAudioZonesMirrorStatusCallback callback = |
| mAudioZonesMirrorStatusCallbacks.getBroadcastItem(c); |
| try { |
| // Calling binder inside lock here since the call is one way and doest not block. |
| // The lock is needed to prevent concurrent beginBroadcast |
| callback.onAudioZonesMirrorStatusChanged(audioZones, status); |
| } catch (RemoteException e) { |
| Slogf.e(TAG, e, "Could not inform mirror status callback index %d of total %d", |
| c, n); |
| } |
| |
| } |
| mAudioZonesMirrorStatusCallbacks.finishBroadcast(); |
| } |
| |
| @Nullable |
| int[] getMirrorAudioZonesForRequest(long requestId) { |
| synchronized (mLock) { |
| return mRequestIdToZones.getOrDefault(requestId, /* valueIfNotFound= */ null); |
| } |
| } |
| |
| boolean isMirrorEnabledForZone(int zoneId) { |
| synchronized (mLock) { |
| return mZonesToMirrorRequestId.get(zoneId, INVALID_REQUEST_ID) != INVALID_REQUEST_ID; |
| } |
| } |
| |
| void rejectMirrorForZones(int[] audioZones) { |
| mHandler.post(() -> |
| handleInformCallbacks(audioZones, CarAudioManager.AUDIO_REQUEST_STATUS_REJECTED)); |
| } |
| |
| void updateRemoveMirrorConfigurationForZones(long requestId, int[] newConfig) { |
| ArraySet<Integer> newConfigSet = CarServiceUtils.toIntArraySet(newConfig); |
| ArrayList<Integer> delta = new ArrayList<>(); |
| synchronized (mLock) { |
| int[] prevConfig = mRequestIdToZones.getOrDefault(requestId, new int[0]); |
| for (int index = 0; index < prevConfig.length; index++) { |
| int zoneId = prevConfig[index]; |
| mZonesToMirrorRequestId.delete(zoneId); |
| if (newConfigSet.contains(zoneId)) { |
| continue; |
| } |
| delta.add(zoneId); |
| } |
| if (newConfig.length == 0) { |
| mRequestIdToZones.remove(requestId); |
| mRequestIdGenerator.releaseRequestId(requestId); |
| } else { |
| mRequestIdToZones.put(requestId, newConfig); |
| } |
| for (int index = 0; index < newConfig.length; index++) { |
| int zoneId = newConfig[index]; |
| mZonesToMirrorRequestId.put(zoneId, requestId); |
| } |
| } |
| mHandler.post(() -> |
| handleInformCallbacks(CarServiceUtils.toIntArray(delta), |
| CarAudioManager.AUDIO_REQUEST_STATUS_STOPPED)); |
| } |
| |
| /** |
| * Return the difference between the audio zones ids to remove and the current for the request |
| * id. This can be used to determine how a configuration should change when some zones |
| * are removed. |
| */ |
| @Nullable |
| int[] calculateAudioConfigurationAfterRemovingZonesFromRequestId(long requestId, |
| int[] audioZoneIdsToRemove) { |
| Objects.requireNonNull(audioZoneIdsToRemove, "Audio zone ids to remove must not be null"); |
| Preconditions.checkArgument(audioZoneIdsToRemove.length > 0, |
| "audio zones ids to remove must not empty"); |
| ArraySet<Integer> zonesToRemove = CarServiceUtils.toIntArraySet(audioZoneIdsToRemove); |
| int[] oldConfig; |
| |
| synchronized (mLock) { |
| oldConfig = mRequestIdToZones.getOrDefault(requestId, null); |
| } |
| |
| if (oldConfig == null) { |
| Slogf.w(TAG, "calculateAudioConfingurationAfterRemovingZonesFromRequestId Request " |
| + "id %d is no longer valid"); |
| return null; |
| } |
| |
| ArrayList<Integer> newConfig = new ArrayList<>(); |
| for (int index = 0; index < oldConfig.length; index++) { |
| int zoneId = oldConfig[index]; |
| if (zonesToRemove.contains(zoneId)) { |
| continue; |
| } |
| newConfig.add(zoneId); |
| } |
| |
| return CarServiceUtils.toIntArray(newConfig); |
| } |
| |
| long getUniqueRequestId() { |
| return mRequestIdGenerator.generateUniqueRequestId(); |
| } |
| |
| long getRequestIdForAudioZone(int audioZoneId) { |
| synchronized (mLock) { |
| return mZonesToMirrorRequestId.get(audioZoneId, INVALID_REQUEST_ID); |
| } |
| } |
| |
| public void verifyValidRequestId(long requestId) { |
| synchronized (mLock) { |
| Preconditions.checkArgument(mRequestIdToZones.containsKey(requestId), |
| "Mirror request id " + requestId + " is not valid"); |
| } |
| } |
| |
| @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) |
| void dump(IndentingPrintWriter writer) { |
| writer.printf("Is audio mirroring enabled? %s\n", isMirrorAudioEnabled() ? "Yes" : "No"); |
| if (!isMirrorAudioEnabled()) { |
| return; |
| } |
| writer.increaseIndent(); |
| int registeredCount = mAudioZonesMirrorStatusCallbacks.getRegisteredCallbackCount(); |
| synchronized (mLock) { |
| writer.println("Mirroring device info:"); |
| dumpMirrorDeviceInfosLocked(writer); |
| writer.printf("Registered callback count: %d\n", registeredCount); |
| writer.println("Mirroring configurations:"); |
| writer.increaseIndent(); |
| for (int index = 0; index < mRequestIdToZones.size(); index++) { |
| writer.printf("Audio zone request id %d: %s\n", mRequestIdToZones.keyAt(index), |
| Arrays.toString(mRequestIdToZones.valueAt(index))); |
| } |
| writer.decreaseIndent(); |
| writer.println("Mirroring zone to id mapping:"); |
| writer.increaseIndent(); |
| for (int index = 0; index < mZonesToMirrorRequestId.size(); index++) { |
| writer.printf("Audio zone %d: request id %d\n", |
| mZonesToMirrorRequestId.keyAt(index), |
| mZonesToMirrorRequestId.valueAt(index)); |
| } |
| writer.decreaseIndent(); |
| } |
| writer.decreaseIndent(); |
| } |
| |
| @GuardedBy("mLock") |
| @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) |
| private void dumpMirrorDeviceInfosLocked(IndentingPrintWriter writer) { |
| for (int index = 0; index < mMirrorDevices.size(); index++) { |
| writer.printf("Mirror device[%d]\n", index); |
| writer.increaseIndent(); |
| mMirrorDevices.get(index).dump(writer); |
| writer.decreaseIndent(); |
| } |
| } |
| } |