1 /* 2 * Copyright (C) 2024 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.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; 20 import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION; 21 22 import static com.android.car.audio.CarActivationVolumeConfig.ActivationVolumeInvocationType; 23 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 24 25 import android.annotation.NonNull; 26 import android.car.builtin.util.Slogf; 27 import android.car.media.CarAudioManager; 28 import android.media.AudioAttributes; 29 import android.os.Binder; 30 import android.telephony.TelephonyCallback; 31 import android.telephony.TelephonyManager; 32 import android.util.Pair; 33 import android.util.SparseArray; 34 import android.util.SparseIntArray; 35 36 import com.android.car.CarLog; 37 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 38 import com.android.internal.annotations.GuardedBy; 39 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Objects; 43 import java.util.concurrent.Executor; 44 import java.util.concurrent.Executors; 45 46 public final class CarAudioPlaybackMonitor { 47 48 private static final int INVALID_UID = -1; 49 50 private final Object mLock = new Object(); 51 private final CarAudioService mCarAudioService; 52 private final SparseArray<CarAudioZone> mCarAudioZones; 53 private final TelephonyManager mTelephonyManager; 54 private final Executor mExecutor; 55 private final CarCallStateListener mCallStateCallback; 56 @GuardedBy("mLock") 57 private final SparseArray<SparseIntArray> mZoneIdGroupIdToUidMap; 58 CarAudioPlaybackMonitor(@onNull CarAudioService carAudioService, @NonNull SparseArray<CarAudioZone> carAudioZones, @NonNull TelephonyManager telephonyManager)59 CarAudioPlaybackMonitor(@NonNull CarAudioService carAudioService, 60 @NonNull SparseArray<CarAudioZone> carAudioZones, 61 @NonNull TelephonyManager telephonyManager) { 62 mCarAudioService = Objects.requireNonNull(carAudioService, 63 "Car audio service can not be null"); 64 mCarAudioZones = Objects.requireNonNull(carAudioZones, 65 "Car audio zones can not be null"); 66 mZoneIdGroupIdToUidMap = new SparseArray<>(); 67 mTelephonyManager = Objects.requireNonNull(telephonyManager, 68 "Telephony manager can not be null"); 69 mExecutor = Executors.newSingleThreadExecutor(); 70 mCallStateCallback = new CarCallStateListener(); 71 mTelephonyManager.registerTelephonyCallback(mExecutor, mCallStateCallback); 72 } 73 74 /** 75 * Reset car audio playback monitor 76 * 77 * <p>Once reset, car audio playback monitor cannot be reused since the listener to 78 * {@link TelephonyManager} has been unregistered. 79 */ reset()80 void reset() { 81 mTelephonyManager.unregisterTelephonyCallback(mCallStateCallback); 82 } 83 resetActivationTypesForZone(int zoneId)84 void resetActivationTypesForZone(int zoneId) { 85 synchronized (mLock) { 86 mZoneIdGroupIdToUidMap.remove(zoneId); 87 } 88 } 89 90 /** 91 * Informs {@link CarAudioService} that newly active playbacks are received and min/max 92 * activation volume should be applied if needed. 93 * 94 * @param newActivePlaybackAttributesWithUid List of pairs of {@link AudioAttributes} and 95 * client Uid of the newly active 96 * {@link android.media.AudioPlaybackConfiguration}s 97 * @param zoneId Zone Id of thr newly active playbacks 98 */ onActiveAudioPlaybackAttributesAdded( List<Pair<AudioAttributes, Integer>> newActivePlaybackAttributesWithUid, int zoneId)99 public void onActiveAudioPlaybackAttributesAdded( 100 List<Pair<AudioAttributes, Integer>> newActivePlaybackAttributesWithUid, int zoneId) { 101 if (newActivePlaybackAttributesWithUid == null 102 || newActivePlaybackAttributesWithUid.isEmpty()) { 103 return; 104 } 105 CarAudioZoneConfig currentZoneConfig = mCarAudioZones.get(zoneId) 106 .getCurrentCarAudioZoneConfig(); 107 List<ActivationInfo> newActivationVolumeGroupsWithType = 108 new ArrayList<>(); 109 for (int index = 0; index < newActivePlaybackAttributesWithUid.size(); index++) { 110 Pair<AudioAttributes, Integer> newActivePlaybackAttributesWithUidEntry = 111 newActivePlaybackAttributesWithUid.get(index); 112 ActivationInfo activationVolumeGroupWithInvocationType = 113 getActivationInfo( 114 newActivePlaybackAttributesWithUidEntry, currentZoneConfig); 115 if (activationVolumeGroupWithInvocationType == null) { 116 continue; 117 } 118 newActivationVolumeGroupsWithType.add(activationVolumeGroupWithInvocationType); 119 } 120 if (newActivationVolumeGroupsWithType.isEmpty()) { 121 return; 122 } 123 mCarAudioService.handleActivationVolumeWithActivationInfos( 124 newActivationVolumeGroupsWithType, zoneId, currentZoneConfig.getZoneConfigId()); 125 } 126 getActivationInfo( Pair<AudioAttributes, Integer> attributesIntegerPair, CarAudioZoneConfig currentZoneConfig)127 private ActivationInfo getActivationInfo( 128 Pair<AudioAttributes, Integer> attributesIntegerPair, 129 CarAudioZoneConfig currentZoneConfig) { 130 int zoneId = currentZoneConfig.getZoneId(); 131 CarVolumeGroup volumeGroup = currentZoneConfig.getVolumeGroupForAudioAttributes( 132 attributesIntegerPair.first); 133 if (volumeGroup == null) { 134 Slogf.w(CarLog.TAG_AUDIO, "Audio attributes %s is not found in zone %d config %d", 135 attributesIntegerPair.first, zoneId, currentZoneConfig.getZoneConfigId()); 136 return null; 137 } 138 int groupId = volumeGroup.getId(); 139 int uid = attributesIntegerPair.second; 140 int activationVolumeInvocationType; 141 synchronized (mLock) { 142 // For a playback with a uid that does not exist for a given zone id and volume group 143 // id, it is the playback after boot or zone configuration change for activation volume. 144 if (!mZoneIdGroupIdToUidMap.contains(zoneId)) { 145 mZoneIdGroupIdToUidMap.put(zoneId, new SparseIntArray()); 146 activationVolumeInvocationType = 147 CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_BOOT; 148 } else if (mZoneIdGroupIdToUidMap.get(zoneId).get(groupId, INVALID_UID) 149 == INVALID_UID) { 150 activationVolumeInvocationType = 151 CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_BOOT; 152 } else { 153 // For a playback with a uid existing for the given zone id and volume group id, 154 // whether it changes the source from the previous playback is determines by 155 // whether it has the same uid. 156 int prevUid = mZoneIdGroupIdToUidMap.get(zoneId).get(groupId); 157 activationVolumeInvocationType = uid == prevUid 158 ? CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_PLAYBACK_CHANGED 159 : CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_SOURCE_CHANGED; 160 } 161 mZoneIdGroupIdToUidMap.get(zoneId).put(groupId, uid); 162 } 163 return new ActivationInfo(volumeGroup.getId(), 164 activationVolumeInvocationType); 165 } 166 167 static final class ActivationInfo { 168 final int mGroupId; 169 @ActivationVolumeInvocationType 170 final int mInvocationType; 171 ActivationInfo(int groupId, @ActivationVolumeInvocationType int type)172 ActivationInfo(int groupId, @ActivationVolumeInvocationType int type) { 173 mGroupId = groupId; 174 mInvocationType = type; 175 } 176 177 @Override equals(Object o)178 public boolean equals(Object o) { 179 if (this == o) { 180 return true; 181 } 182 if (!(o instanceof ActivationInfo)) { 183 return false; 184 } 185 ActivationInfo other = (ActivationInfo) o; 186 return other.mGroupId == mGroupId && other.mInvocationType == mInvocationType; 187 } 188 189 @Override hashCode()190 public int hashCode() { 191 return Objects.hash(mGroupId, mInvocationType); 192 } 193 194 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) 195 @Override toString()196 public String toString() { 197 return "ActivationInfo { volume group id: " + mGroupId + ", invocation type = " 198 + mInvocationType + "}"; 199 } 200 } 201 202 private class CarCallStateListener extends TelephonyCallback 203 implements TelephonyCallback.CallStateListener { 204 @Override onCallStateChanged(int state)205 public void onCallStateChanged(int state) { 206 int usage; 207 switch (state) { 208 case TelephonyManager.CALL_STATE_RINGING: 209 usage = USAGE_NOTIFICATION_RINGTONE; 210 break; 211 case TelephonyManager.CALL_STATE_OFFHOOK: 212 usage = USAGE_VOICE_COMMUNICATION; 213 break; 214 default: 215 return; 216 } 217 AudioAttributes audioAttributes = CarAudioContext.getAudioAttributeFromUsage(usage); 218 // TODO(331680279): get actual Uid from telephony or define a fake Uid for telephony 219 // playback only. 220 onActiveAudioPlaybackAttributesAdded(List.of(new Pair<>(audioAttributes, 221 Binder.getCallingUid())), CarAudioManager.PRIMARY_AUDIO_ZONE); 222 } 223 } 224 } 225