1 /* 2 * Copyright (C) 2021 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 com.android.car.audio.CarAudioService.SystemClockWrapper; 20 import static com.android.car.audio.CarAudioUtils.hasExpired; 21 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 22 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.media.AudioAttributes; 26 import android.media.AudioPlaybackConfiguration; 27 import android.util.ArrayMap; 28 import android.util.Pair; 29 import android.util.proto.ProtoOutputStream; 30 31 import com.android.car.audio.CarAudioDumpProto.CarAudioPlaybackCallbackProto; 32 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 33 import com.android.car.internal.util.IndentingPrintWriter; 34 import com.android.internal.annotations.GuardedBy; 35 import com.android.internal.util.Preconditions; 36 37 import java.util.ArrayList; 38 import java.util.Collection; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Objects; 42 43 final class ZoneAudioPlaybackCallback { 44 private final Object mLock = new Object(); 45 @GuardedBy("mLock") 46 private final ArrayMap<AudioAttributes, Long> mAudioAttributesStartTime = new ArrayMap<>(); 47 @GuardedBy("mLock") 48 private final ArrayMap<String, AudioPlaybackConfiguration> mLastActiveConfigs = 49 new ArrayMap<>(); 50 private final CarAudioZone mCarAudioZone; 51 private final @Nullable CarAudioPlaybackMonitor mCarAudioPlaybackMonitor; 52 private final SystemClockWrapper mClock; 53 private final int mVolumeKeyEventTimeoutMs; 54 ZoneAudioPlaybackCallback(@onNull CarAudioZone carAudioZone, @Nullable CarAudioPlaybackMonitor carAudioPlaybackMonitor, @NonNull SystemClockWrapper clock, int volumeKeyEventTimeoutMs)55 ZoneAudioPlaybackCallback(@NonNull CarAudioZone carAudioZone, 56 @Nullable CarAudioPlaybackMonitor carAudioPlaybackMonitor, 57 @NonNull SystemClockWrapper clock, 58 int volumeKeyEventTimeoutMs) { 59 mCarAudioZone = Objects.requireNonNull(carAudioZone, "Audio zone cannot be null"); 60 mCarAudioPlaybackMonitor = carAudioPlaybackMonitor; 61 mClock = Objects.requireNonNull(clock, "Clock cannot be null"); 62 mVolumeKeyEventTimeoutMs = Preconditions.checkArgumentNonnegative(volumeKeyEventTimeoutMs, 63 "Volume key event timeout must be positive"); 64 } 65 onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configurations)66 public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configurations) { 67 ArrayMap<String, AudioPlaybackConfiguration> newActiveConfigs = 68 filterNewActiveConfiguration(configurations); 69 List<Pair<AudioAttributes, Integer>> newlyActiveAudioAttributesWithUid = new ArrayList<>(); 70 71 synchronized (mLock) { 72 List<AudioPlaybackConfiguration> newlyInactiveConfigurations = 73 getNewlyInactiveConfigurationsLocked(newActiveConfigs); 74 if (mCarAudioPlaybackMonitor != null) { 75 newlyActiveAudioAttributesWithUid = getNewlyActiveAudioAttributes(newActiveConfigs); 76 } 77 78 mLastActiveConfigs.clear(); 79 mLastActiveConfigs.putAll(newActiveConfigs); 80 81 startTimersForContextThatBecameInactiveLocked(newlyInactiveConfigurations); 82 } 83 84 if (mCarAudioPlaybackMonitor != null && !newlyActiveAudioAttributesWithUid.isEmpty()) { 85 mCarAudioPlaybackMonitor.onActiveAudioPlaybackAttributesAdded( 86 newlyActiveAudioAttributesWithUid, mCarAudioZone.getId()); 87 } 88 } 89 90 /** 91 * Returns all active contexts for the primary zone 92 * @return all active audio contexts, including those that recently became inactive but are 93 * considered active due to the audio playback timeout. 94 */ getAllActiveAudioAttributes()95 public List<AudioAttributes> getAllActiveAudioAttributes() { 96 synchronized (mLock) { 97 List<AudioAttributes> activeContexts = getCurrentlyActiveAttributesLocked(); 98 activeContexts 99 .addAll(getStillActiveContextAndRemoveExpiredContextsLocked()); 100 return activeContexts; 101 } 102 } 103 104 @GuardedBy("mLock") startTimersForContextThatBecameInactiveLocked( List<AudioPlaybackConfiguration> inactiveConfigs)105 private void startTimersForContextThatBecameInactiveLocked( 106 List<AudioPlaybackConfiguration> inactiveConfigs) { 107 List<AudioAttributes> activeAttributes = mCarAudioZone 108 .findActiveAudioAttributesFromPlaybackConfigurations(inactiveConfigs); 109 110 for (int index = 0; index < activeAttributes.size(); index++) { 111 mAudioAttributesStartTime.put(activeAttributes.get(index), mClock.uptimeMillis()); 112 } 113 } 114 115 @GuardedBy("mLock") getNewlyInactiveConfigurationsLocked( Map<String, AudioPlaybackConfiguration> newActiveConfigurations)116 private List<AudioPlaybackConfiguration> getNewlyInactiveConfigurationsLocked( 117 Map<String, AudioPlaybackConfiguration> newActiveConfigurations) { 118 List<AudioPlaybackConfiguration> newlyInactiveConfigurations = new ArrayList<>(); 119 for (int index = 0; index < mLastActiveConfigs.size(); index++) { 120 if (newActiveConfigurations 121 .containsKey(mLastActiveConfigs.keyAt(index))) { 122 continue; 123 } 124 newlyInactiveConfigurations.add(mLastActiveConfigs.valueAt(index)); 125 } 126 return newlyInactiveConfigurations; 127 } 128 129 @GuardedBy("mLock") getNewlyActiveAudioAttributes( ArrayMap<String, AudioPlaybackConfiguration> newActiveConfigurations)130 private List<Pair<AudioAttributes, Integer>> getNewlyActiveAudioAttributes( 131 ArrayMap<String, AudioPlaybackConfiguration> newActiveConfigurations) { 132 List<AudioPlaybackConfiguration> audioPlaybackConfigurationsWithNewAttributes = 133 new ArrayList<>(); 134 for (int index = 0; index < newActiveConfigurations.size(); index++) { 135 if (mLastActiveConfigs.containsKey(newActiveConfigurations.keyAt(index))) { 136 continue; 137 } 138 audioPlaybackConfigurationsWithNewAttributes 139 .add(newActiveConfigurations.valueAt(index)); 140 } 141 List<Pair<AudioAttributes, Integer>> attributesUidList = new ArrayList<>(); 142 for (int index = 0; index < audioPlaybackConfigurationsWithNewAttributes.size(); index++) { 143 AudioPlaybackConfiguration configuration = audioPlaybackConfigurationsWithNewAttributes 144 .get(index); 145 List<AudioAttributes> attributes = getAudioAttributesFromPlaybacks( 146 List.of(configuration)); 147 if (attributes.isEmpty()) { 148 continue; 149 } 150 attributesUidList.add(new Pair<>(attributes.get(0), configuration.getClientUid())); 151 } 152 return attributesUidList; 153 } 154 filterNewActiveConfiguration( List<AudioPlaybackConfiguration> configurations)155 private ArrayMap<String, AudioPlaybackConfiguration> filterNewActiveConfiguration( 156 List<AudioPlaybackConfiguration> configurations) { 157 ArrayMap<String, AudioPlaybackConfiguration> newActiveConfigs = new ArrayMap<>(); 158 for (int index = 0; index < configurations.size(); index++) { 159 AudioPlaybackConfiguration configuration = configurations.get(index); 160 if (!configuration.isActive()) { 161 continue; 162 } 163 if (mCarAudioZone 164 .isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) { 165 newActiveConfigs.put( 166 configuration.getAudioDeviceInfo().getAddress(), configuration); 167 } 168 } 169 return newActiveConfigs; 170 } 171 172 @GuardedBy("mLock") getCurrentlyActiveAttributesLocked()173 private List<AudioAttributes> getCurrentlyActiveAttributesLocked() { 174 return getAudioAttributesFromPlaybacks(mLastActiveConfigs.values()); 175 } 176 177 @GuardedBy("mLock") getStillActiveContextAndRemoveExpiredContextsLocked()178 private List<AudioAttributes> getStillActiveContextAndRemoveExpiredContextsLocked() { 179 List<AudioAttributes> attributesToRemove = new ArrayList<>(); 180 List<AudioAttributes> activeAttributes = new ArrayList<>(); 181 for (int index = 0; index < mAudioAttributesStartTime.size(); index++) { 182 long startTime = mAudioAttributesStartTime.valueAt(index); 183 if (hasExpired(startTime, mClock.uptimeMillis(), mVolumeKeyEventTimeoutMs)) { 184 attributesToRemove.add(mAudioAttributesStartTime.keyAt(index)); 185 continue; 186 } 187 activeAttributes.add(mAudioAttributesStartTime.keyAt(index)); 188 } 189 190 for (int indexToRemove = 0; indexToRemove < attributesToRemove.size(); indexToRemove++) { 191 mAudioAttributesStartTime.remove(attributesToRemove.get(indexToRemove)); 192 } 193 return activeAttributes; 194 } 195 resetStillActiveContexts()196 void resetStillActiveContexts() { 197 synchronized (mLock) { 198 mAudioAttributesStartTime.clear(); 199 } 200 } 201 getAudioAttributesFromPlaybacks( Collection<AudioPlaybackConfiguration> playbacks)202 private List<AudioAttributes> getAudioAttributesFromPlaybacks( 203 Collection<AudioPlaybackConfiguration> playbacks) { 204 return mCarAudioZone.findActiveAudioAttributesFromPlaybackConfigurations( 205 new ArrayList<>(playbacks)); 206 } 207 208 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)209 public void dump(IndentingPrintWriter writer) { 210 writer.printf("Audio zone: %d\n", mCarAudioZone.getId()); 211 212 dumpLastActiveConfigsAndAudioAttributesStartTime(writer); 213 } 214 215 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpLastActiveConfigsAndAudioAttributesStartTime(IndentingPrintWriter writer)216 public void dumpLastActiveConfigsAndAudioAttributesStartTime(IndentingPrintWriter writer) { 217 synchronized (mLock) { 218 writer.println("Last active configs:"); 219 writer.increaseIndent(); 220 for (int i = 0; i < mLastActiveConfigs.size(); i++) { 221 writer.printf("Audio device address %s to config %s\n", 222 mLastActiveConfigs.keyAt(i), mLastActiveConfigs.valueAt(i)); 223 } 224 writer.decreaseIndent(); 225 writer.println("Audio attributes start times:"); 226 writer.increaseIndent(); 227 for (int i = 0; i < mAudioAttributesStartTime.size(); i++) { 228 writer.printf("Audio Attributes %s mapped to start time of %d\n", 229 mAudioAttributesStartTime.keyAt(i), mAudioAttributesStartTime.valueAt(i)); 230 } 231 writer.decreaseIndent(); 232 } 233 } 234 235 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProto(ProtoOutputStream proto)236 void dumpProto(ProtoOutputStream proto) { 237 long token = proto.start(CarAudioPlaybackCallbackProto.ZONE_AUDIO_PLAYBACK_CALLBACKS); 238 proto.write(CarAudioPlaybackCallbackProto.ZoneAudioPlaybackCallbackProto.ZONE_ID, 239 mCarAudioZone.getId()); 240 dumpProtoLastActiveConfigsAndAudioAttributesStartTime(proto); 241 proto.end(token); 242 } 243 244 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dumpProtoLastActiveConfigsAndAudioAttributesStartTime(ProtoOutputStream proto)245 public void dumpProtoLastActiveConfigsAndAudioAttributesStartTime(ProtoOutputStream proto) { 246 synchronized (mLock) { 247 for (int i = 0; i < mLastActiveConfigs.size(); i++) { 248 long lastActiveConfigToken = proto.start(CarAudioPlaybackCallbackProto 249 .ZoneAudioPlaybackCallbackProto.LAST_ACTIVE_CONFIGS); 250 proto.write(CarAudioPlaybackCallbackProto.ZoneAudioPlaybackCallbackProto 251 .AudioDeviceAddressToConfig.ADDRESS, mLastActiveConfigs.keyAt(i)); 252 proto.write(CarAudioPlaybackCallbackProto.ZoneAudioPlaybackCallbackProto 253 .AudioDeviceAddressToConfig.CONFIG, mLastActiveConfigs.valueAt(i) 254 .toString()); 255 proto.end(lastActiveConfigToken); 256 } 257 258 for (int i = 0; i < mAudioAttributesStartTime.size(); i++) { 259 long audioAttributeToStartTimeToken = proto.start(CarAudioPlaybackCallbackProto 260 .ZoneAudioPlaybackCallbackProto.AUDIO_ATTRIBUTES_TO_START_TIMES); 261 CarAudioContextInfo.dumpCarAudioAttributesProto(mAudioAttributesStartTime.keyAt(i), 262 CarAudioPlaybackCallbackProto.ZoneAudioPlaybackCallbackProto 263 .AudioAttributesToStartTime.AUDIO_ATTRIBUTES, proto); 264 proto.write(CarAudioPlaybackCallbackProto.ZoneAudioPlaybackCallbackProto 265 .AudioAttributesToStartTime.START_TIME, 266 mAudioAttributesStartTime.valueAt(i)); 267 proto.end(audioAttributeToStartTimeToken); 268 } 269 } 270 } 271 } 272