blob: 6ab4b0402ec51387d406e9c8d0033fd1727742af [file] [log] [blame]
/*
* Copyright (C) 2018 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 com.android.car.audio.FocusInteraction.AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL_URI;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.car.builtin.util.Slogf;
import android.car.media.CarAudioManager;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioPolicy;
import android.os.Bundle;
import android.util.ArraySet;
import android.util.SparseArray;
import com.android.car.CarLocalServices;
import com.android.car.CarLog;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.util.IndentingPrintWriter;
import com.android.car.oem.CarOemProxyService;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Implements {@link AudioPolicy.AudioPolicyFocusListener}
*
* <p><b>Note:</b> Manages audio focus on a per zone basis.
*/
final class CarZonesAudioFocus extends AudioPolicy.AudioPolicyFocusListener {
private static final String TAG = CarLog.tagFor(CarZonesAudioFocus.class);
private final CarFocusCallback mCarFocusCallback;
private CarAudioService mCarAudioService; // Dynamically assigned just after construction
private AudioPolicy mAudioPolicy; // Dynamically assigned just after construction
private final SparseArray<CarAudioFocus> mFocusZones;
public static CarZonesAudioFocus createCarZonesAudioFocus(AudioManager audioManager,
PackageManager packageManager,
SparseArray<CarAudioZone> carAudioZones,
CarAudioSettings carAudioSettings,
CarFocusCallback carFocusCallback,
CarVolumeInfoWrapper carVolumeInfoWrapper) {
Objects.requireNonNull(audioManager, "Audio manager cannot be null");
Objects.requireNonNull(packageManager, "Package manager cannot be null");
Objects.requireNonNull(carAudioZones, "Car audio zones cannot be null");
Preconditions.checkArgument(carAudioZones.size() != 0,
"There must be a minimum of one audio zone");
Objects.requireNonNull(carAudioSettings, "Car audio settings cannot be null");
Objects.requireNonNull(carVolumeInfoWrapper, "Car volume info cannot be null");
SparseArray<CarAudioFocus> audioFocusPerZone = new SparseArray<>();
//Create focus for all the zones
for (int i = 0; i < carAudioZones.size(); i++) {
CarAudioZone audioZone = carAudioZones.valueAt(i);
int audioZoneId = audioZone.getId();
Slogf.d(TAG, "Adding new zone %d", audioZoneId);
CarAudioFocus zoneFocusListener = new CarAudioFocus(audioManager,
packageManager, new FocusInteraction(carAudioSettings,
new ContentObserverFactory(AUDIO_FOCUS_NAVIGATION_REJECTED_DURING_CALL_URI),
audioZone.getCarAudioContext()),
audioZone.getCarAudioContext(), carVolumeInfoWrapper, audioZoneId);
audioFocusPerZone.put(audioZoneId, zoneFocusListener);
}
return new CarZonesAudioFocus(audioFocusPerZone, carFocusCallback);
}
@VisibleForTesting
CarZonesAudioFocus(SparseArray<CarAudioFocus> focusZones, CarFocusCallback carFocusCallback) {
mFocusZones = focusZones;
mCarFocusCallback = carFocusCallback;
}
/**
* Query the current list of focus loser in zoneId for uid
* @param uid uid to query for current focus losers
* @param zoneId zone id to query for info
* @return list of current focus losers for uid
*/
ArrayList<AudioFocusInfo> getAudioFocusLosersForUid(int uid, int zoneId) {
CarAudioFocus focus = mFocusZones.get(zoneId);
return focus.getAudioFocusLosersForUid(uid);
}
/**
* Query the current list of focus holders in zoneId for uid
* @param uid uid to query for current focus holders
* @param zoneId zone id to query
* @return list of current focus holders that for uid
*/
ArrayList<AudioFocusInfo> getAudioFocusHoldersForUid(int uid, int zoneId) {
CarAudioFocus focus = mFocusZones.get(zoneId);
return focus.getAudioFocusHoldersForUid(uid);
}
/**
* For the zone queried, transiently lose all active focus entries
* @param zoneId zone id where all focus entries should be lost
* @return focus entries lost in the zone.
*/
List<AudioFocusInfo> transientlyLoseAllFocusHoldersInZone(int zoneId) {
CarAudioFocus focus = mFocusZones.get(zoneId);
List<AudioFocusInfo> activeFocusInfos = focus.getAudioFocusHolders();
if (!activeFocusInfos.isEmpty()) {
transientlyLoseInFocusInZone(activeFocusInfos, zoneId);
}
return activeFocusInfos;
}
/**
* For each entry in list, transiently lose focus
* @param afiList list of audio focus entries
* @param zoneId zone id where focus should be lost
*/
void transientlyLoseInFocusInZone(List<AudioFocusInfo> afiList, int zoneId) {
CarAudioFocus focus = mFocusZones.get(zoneId);
transientlyLoseInFocusInZone(afiList, focus);
}
private void transientlyLoseInFocusInZone(List<AudioFocusInfo> audioFocusInfos,
CarAudioFocus focus) {
for (int index = 0; index < audioFocusInfos.size(); index++) {
AudioFocusInfo info = audioFocusInfos.get(index);
focus.removeAudioFocusInfoAndTransientlyLoseFocus(info);
}
}
/**
* For each entry in list, reevaluate and regain focus and notify focus listener of its zone
*
* @param audioFocusInfos list of audio focus entries to reevaluate and regain
* @return list of results for regaining focus
*/
List<Integer> reevaluateAndRegainAudioFocusList(List<AudioFocusInfo> audioFocusInfos) {
List<Integer> res = new ArrayList<>(audioFocusInfos.size());
ArraySet<Integer> zoneIds = new ArraySet<>();
for (int index = 0; index < audioFocusInfos.size(); index++) {
AudioFocusInfo afi = audioFocusInfos.get(index);
int zoneId = getAudioZoneIdForAudioFocusInfo(afi);
res.add(getCarAudioFocusForZoneId(zoneId).reevaluateAndRegainAudioFocus(afi));
zoneIds.add(zoneId);
}
int[] zoneIdArray = new int[zoneIds.size()];
for (int zoneIdIndex = 0; zoneIdIndex < zoneIds.size(); zoneIdIndex++) {
zoneIdArray[zoneIdIndex] = zoneIds.valueAt(zoneIdIndex);
}
notifyFocusListeners(zoneIdArray);
return res;
}
int reevaluateAndRegainAudioFocus(AudioFocusInfo afi) {
int zoneId = getAudioZoneIdForAudioFocusInfo(afi);
return getCarAudioFocusForZoneId(zoneId).reevaluateAndRegainAudioFocus(afi);
}
/**
* Sets the owning policy of the audio focus
*
* <p><b>Note:</b> This has to happen after the construction to avoid a chicken and egg
* problem when setting up the AudioPolicy which must depend on this object.
* @param carAudioService owning car audio service
* @param parentPolicy owning parent car audio policy
*/
void setOwningPolicy(CarAudioService carAudioService, AudioPolicy parentPolicy) {
mAudioPolicy = parentPolicy;
mCarAudioService = carAudioService;
for (int i = 0; i < mFocusZones.size(); i++) {
mFocusZones.valueAt(i).setOwningPolicy(mAudioPolicy);
}
}
void setRestrictFocus(boolean isFocusRestricted) {
int[] zoneIds = new int[mFocusZones.size()];
for (int i = 0; i < mFocusZones.size(); i++) {
zoneIds[i] = mFocusZones.keyAt(i);
mFocusZones.valueAt(i).setRestrictFocus(isFocusRestricted);
}
notifyFocusListeners(zoneIds);
}
@Override
public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {
int zoneId = getAudioZoneIdForAudioFocusInfo(afi);
getCarAudioFocusForZoneId(zoneId).onAudioFocusRequest(afi, requestResult);
notifyFocusListeners(new int[]{zoneId});
}
/**
* @see AudioManager#abandonAudioFocus(AudioManager.OnAudioFocusChangeListener, AudioAttributes)
* Note that we'll get this call for a focus holder that dies while in the focus stack, so
* we don't need to watch for death notifications directly.
*/
@Override
public void onAudioFocusAbandon(AudioFocusInfo afi) {
int zoneId = getAudioZoneIdForAudioFocusInfo(afi);
getCarAudioFocusForZoneId(zoneId).onAudioFocusAbandon(afi);
notifyFocusListeners(new int[]{zoneId});
}
private CarAudioFocus getCarAudioFocusForZoneId(int zoneId) {
return mFocusZones.get(zoneId);
}
private int getAudioZoneIdForAudioFocusInfo(AudioFocusInfo afi) {
int zoneId = mCarAudioService.getZoneIdForAudioFocusInfo(afi);
// If the bundle attribute for AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID has been assigned
// Use zone id from that instead.
Bundle bundle = afi.getAttributes().getBundle();
if (bundle != null) {
int bundleZoneId =
bundle.getInt(CarAudioManager.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID,
-1);
// check if the zone id is within current zones bounds
if (mCarAudioService.isAudioZoneIdValid(bundleZoneId)) {
Slogf.d(TAG, "getFocusForAudioFocusInfo valid zoneId %d with bundle request for"
+ " client %s", bundleZoneId, afi.getClientId());
zoneId = bundleZoneId;
} else {
Slogf.w(TAG, "getFocusForAudioFocusInfo invalid zoneId %d with bundle request for "
+ "client %s, dispatching focus request to zoneId %d", bundleZoneId,
afi.getClientId(), zoneId);
}
}
return zoneId;
}
private void notifyFocusListeners(int[] zoneIds) {
SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId = new SparseArray<>(zoneIds.length);
for (int i = 0; i < zoneIds.length; i++) {
int zoneId = zoneIds[i];
focusHoldersByZoneId.put(zoneId, mFocusZones.get(zoneId).getAudioFocusHolders());
sendFocusChangeToOemService(getCarAudioFocusForZoneId(zoneId), zoneId);
}
if (mCarFocusCallback == null) {
return;
}
mCarFocusCallback.onFocusChange(zoneIds, focusHoldersByZoneId);
}
private void sendFocusChangeToOemService(CarAudioFocus carAudioFocus, int zoneId) {
CarOemProxyService proxy = CarLocalServices.getService(CarOemProxyService.class);
if (!proxy.isOemServiceEnabled() || !proxy.isOemServiceReady()
|| proxy.getCarOemAudioFocusService() == null) {
return;
}
proxy.getCarOemAudioFocusService().notifyAudioFocusChange(
carAudioFocus.getAudioFocusHolders(), carAudioFocus.getAudioFocusLosers(), zoneId);
}
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
void dump(IndentingPrintWriter writer) {
writer.println("*CarZonesAudioFocus*");
writer.increaseIndent();
writer.printf("Has Focus Callback: %b\n", mCarFocusCallback != null);
writer.println("Car Zones Audio Focus Listeners:");
writer.increaseIndent();
for (int i = 0; i < mFocusZones.size(); i++) {
writer.printf("Zone Id: %d\n", mFocusZones.keyAt(i));
writer.increaseIndent();
mFocusZones.valueAt(i).dump(writer);
writer.decreaseIndent();
}
writer.decreaseIndent();
writer.decreaseIndent();
}
public void updateUserForZoneId(int audioZoneId, @UserIdInt int userId) {
Preconditions.checkArgument(mCarAudioService.isAudioZoneIdValid(audioZoneId),
"Invalid zoneId %d", audioZoneId);
mFocusZones.get(audioZoneId).getFocusInteraction().setUserIdForSettings(userId);
}
AudioFocusStack transientlyLoseMediaAudioFocusForUser(int userId, int zoneId) {
AudioAttributes audioAttributes =
new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
CarAudioFocus carAudioFocus = mFocusZones.get(zoneId);
List<AudioFocusInfo> activeFocusInfos = carAudioFocus
.getActiveAudioFocusForUserAndAudioAttributes(audioAttributes, userId);
List<AudioFocusInfo> inactiveFocusInfos = carAudioFocus
.getInactiveAudioFocusForUserAndAudioAttributes(audioAttributes, userId);
return transientlyLoserFocusForFocusStack(carAudioFocus, activeFocusInfos,
inactiveFocusInfos);
}
AudioFocusStack transientlyLoseAudioFocusForZone(int zoneId) {
CarAudioFocus carAudioFocus = mFocusZones.get(zoneId);
List<AudioFocusInfo> activeFocusInfos = carAudioFocus.getAudioFocusHolders();
List<AudioFocusInfo> inactiveFocusInfos = carAudioFocus.getAudioFocusLosers();
return transientlyLoserFocusForFocusStack(carAudioFocus, activeFocusInfos,
inactiveFocusInfos);
}
private AudioFocusStack transientlyLoserFocusForFocusStack(CarAudioFocus carAudioFocus,
List<AudioFocusInfo> activeFocusInfos, List<AudioFocusInfo> inactiveFocusInfos) {
// Order matters here: Remove the focus losers first
// then do the current holder to prevent loser from popping up while
// the focus is being removed for current holders
// Remove focus for current focus losers
if (!inactiveFocusInfos.isEmpty()) {
transientlyLoseInFocusInZone(inactiveFocusInfos, carAudioFocus);
}
if (!activeFocusInfos.isEmpty()) {
transientlyLoseInFocusInZone(activeFocusInfos, carAudioFocus);
}
return new AudioFocusStack(activeFocusInfos, inactiveFocusInfos);
}
void regainMediaAudioFocusInZone(AudioFocusStack mediaFocusStack, int zoneId) {
CarAudioFocus carAudioFocus = mFocusZones.get(zoneId);
// Order matters here: Regain focus for
// previously lost focus holders then regain
// focus for holders that had it last.
// Regain focus for the focus losers from previous zone.
if (!mediaFocusStack.getInactiveFocusList().isEmpty()) {
regainMediaAudioFocusInZone(mediaFocusStack.getInactiveFocusList(), carAudioFocus);
}
if (!mediaFocusStack.getActiveFocusList().isEmpty()) {
regainMediaAudioFocusInZone(mediaFocusStack.getActiveFocusList(), carAudioFocus);
}
}
private void regainMediaAudioFocusInZone(List<AudioFocusInfo> focusInfos,
CarAudioFocus carAudioFocus) {
for (int index = 0; index < focusInfos.size(); index++) {
carAudioFocus.reevaluateAndRegainAudioFocus(focusInfos.get(index));
}
}
/**
* Callback to get notified of the active focus holders after any focus request or abandon call
*/
public interface CarFocusCallback {
/**
* Called after a focus request or abandon call is handled.
*
* @param audioZoneIds IDs of the zones where the changes took place
* @param focusHoldersByZoneId sparse array by zone ID, where each value is a list of
* {@link AudioFocusInfo}s holding focus in specified audio zone
*/
void onFocusChange(int[] audioZoneIds,
@NonNull SparseArray<List<AudioFocusInfo>> focusHoldersByZoneId);
}
}