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.CarAudioContext.AudioContext; 20 import static com.android.car.audio.CarAudioService.SystemClockWrapper; 21 import static com.android.car.audio.CarAudioUtils.hasExpired; 22 23 import android.annotation.NonNull; 24 import android.media.AudioManager; 25 import android.media.AudioPlaybackConfiguration; 26 import android.util.SparseLongArray; 27 28 import com.android.internal.annotations.GuardedBy; 29 import com.android.internal.util.Preconditions; 30 31 import java.util.ArrayList; 32 import java.util.HashMap; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Objects; 36 37 final class CarAudioPlaybackCallback extends AudioManager.AudioPlaybackCallback { 38 private final Object mLock = new Object(); 39 @GuardedBy("mLock") 40 private final SparseLongArray mContextStartTime = new SparseLongArray(); 41 @GuardedBy("mLock") 42 private final Map<String, AudioPlaybackConfiguration> mLastActiveConfigs = new HashMap<>(); 43 private final CarAudioZone mCarPrimaryAudioZone; 44 private final SystemClockWrapper mClock; 45 private final int mVolumeKeyEventTimeoutMs; 46 CarAudioPlaybackCallback(@onNull CarAudioZone carPrimaryAudioZone, @NonNull SystemClockWrapper clock, int volumeKeyEventTimeoutMs)47 CarAudioPlaybackCallback(@NonNull CarAudioZone carPrimaryAudioZone, 48 @NonNull SystemClockWrapper clock, 49 int volumeKeyEventTimeoutMs) { 50 mCarPrimaryAudioZone = Objects.requireNonNull(carPrimaryAudioZone); 51 mClock = Objects.requireNonNull(clock); 52 mVolumeKeyEventTimeoutMs = Preconditions.checkArgumentNonnegative(volumeKeyEventTimeoutMs); 53 } 54 55 @Override onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configurations)56 public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configurations) { 57 Map<String, AudioPlaybackConfiguration> newActiveConfigs = 58 filterNewActiveConfiguration(configurations); 59 60 synchronized (mLock) { 61 List<AudioPlaybackConfiguration> newlyInactiveConfigurations = 62 getNewlyInactiveConfigurationsLocked(newActiveConfigs); 63 64 mLastActiveConfigs.clear(); 65 mLastActiveConfigs.putAll(newActiveConfigs); 66 67 startTimersForContextThatBecameInactiveLocked(newlyInactiveConfigurations); 68 } 69 } 70 71 /** 72 * Returns all active contexts for the primary zone 73 * @return all active audio contexts, including those that recently became inactive but are 74 * considered active due to the audio playback timeout. 75 */ getAllActiveContextsForPrimaryZone()76 public List<Integer> getAllActiveContextsForPrimaryZone() { 77 synchronized (mLock) { 78 List<Integer> activeContexts = getCurrentlyActiveContextsLocked(); 79 activeContexts 80 .addAll(getStillActiveContextAndRemoveExpiredContextsLocked()); 81 return activeContexts; 82 } 83 } 84 startTimersForContextThatBecameInactiveLocked( List<AudioPlaybackConfiguration> inactiveConfigs)85 private void startTimersForContextThatBecameInactiveLocked( 86 List<AudioPlaybackConfiguration> inactiveConfigs) { 87 List<Integer> activeContexts = mCarPrimaryAudioZone 88 .findActiveContextsFromPlaybackConfigurations(inactiveConfigs); 89 90 for (int activeContext : activeContexts) { 91 mContextStartTime.put(activeContext, mClock.uptimeMillis()); 92 } 93 } 94 getNewlyInactiveConfigurationsLocked( Map<String, AudioPlaybackConfiguration> newActiveConfigurations)95 private List<AudioPlaybackConfiguration> getNewlyInactiveConfigurationsLocked( 96 Map<String, AudioPlaybackConfiguration> newActiveConfigurations) { 97 List<AudioPlaybackConfiguration> newlyInactiveConfigurations = new ArrayList<>(); 98 for (String address : mLastActiveConfigs.keySet()) { 99 if (newActiveConfigurations.containsKey(address)) { 100 continue; 101 } 102 newlyInactiveConfigurations.add(mLastActiveConfigs.get(address)); 103 } 104 return newlyInactiveConfigurations; 105 } 106 filterNewActiveConfiguration( List<AudioPlaybackConfiguration> configurations)107 private Map<String, AudioPlaybackConfiguration> filterNewActiveConfiguration( 108 List<AudioPlaybackConfiguration> configurations) { 109 Map<String, AudioPlaybackConfiguration> newActiveConfigs = new HashMap<>(); 110 for (int index = 0; index < configurations.size(); index++) { 111 AudioPlaybackConfiguration configuration = configurations.get(index); 112 if (!configuration.isActive()) { 113 continue; 114 } 115 if (mCarPrimaryAudioZone 116 .isAudioDeviceInfoValidForZone(configuration.getAudioDeviceInfo())) { 117 newActiveConfigs.put( 118 configuration.getAudioDeviceInfo().getAddress(), configuration); 119 } 120 } 121 return newActiveConfigs; 122 } 123 getCurrentlyActiveContextsLocked()124 private List<Integer> getCurrentlyActiveContextsLocked() { 125 return mCarPrimaryAudioZone.findActiveContextsFromPlaybackConfigurations( 126 new ArrayList<>(mLastActiveConfigs.values())); 127 } 128 getStillActiveContextAndRemoveExpiredContextsLocked()129 private List<Integer> getStillActiveContextAndRemoveExpiredContextsLocked() { 130 List<Integer> contextsToRemove = new ArrayList<>(); 131 List<Integer> stillActiveContexts = new ArrayList<>(); 132 for (int index = 0; index < mContextStartTime.size(); index++) { 133 @AudioContext int context = mContextStartTime.keyAt(index); 134 if (hasExpired(mContextStartTime.valueAt(index), 135 mClock.uptimeMillis(), mVolumeKeyEventTimeoutMs)) { 136 contextsToRemove.add(context); 137 continue; 138 } 139 stillActiveContexts.add(context); 140 } 141 142 for (int indexToRemove = 0; indexToRemove < contextsToRemove.size(); indexToRemove++) { 143 mContextStartTime.delete(contextsToRemove.get(indexToRemove)); 144 } 145 return stillActiveContexts; 146 } 147 resetStillActiveContexts()148 void resetStillActiveContexts() { 149 synchronized (mLock) { 150 mContextStartTime.clear(); 151 } 152 } 153 } 154