/* * Copyright (C) 2015 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 android.car.media; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.BOILERPLATE_CODE; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DEPRECATED_CODE; import static com.android.car.internal.common.CommonConstants.EMPTY_INT_ARRAY; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.car.Car; import android.car.CarLibLog; import android.car.CarManagerBase; import android.car.CarOccupantZoneManager; import android.car.CarOccupantZoneManager.OccupantZoneInfo; import android.car.feature.Flags; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.util.Slog; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.ICarBase; import com.android.car.internal.annotation.AttributeUsage; import com.android.internal.annotations.GuardedBy; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; /** * APIs for handling audio in a car. * *
In a car environment, we introduced the support to turn audio dynamic routing on/off by * setting the "audioUseDynamicRouting" attribute in config.xml
* *When audio dynamic routing is enabled:
*When audio dynamic routing is disabled:
** If enabled, car volume group muting APIs can be used to mute each volume group, * also car volume group muting changed callback will be called upon group mute changes. If * disabled, car volume will toggle master mute instead. */ public static final int AUDIO_FEATURE_VOLUME_GROUP_MUTING = 2; /** * This is used to determine if the OEM audio service is enabled via * {@link #isAudioFeatureEnabled(int)} * *
If enabled, car audio focus, car audio volume, and ducking control behaviour can change * as it can be OEM dependent. */ public static final int AUDIO_FEATURE_OEM_AUDIO_SERVICE = 3; /** * This is used to determine if volume group events is supported via * {@link #isAudioFeatureEnabled(int)} * *
If enabled, the car volume group event callback can be used to receive event changes * to volume, mute, attenuation. * If disabled, the register/unregister APIs will return {@code false}. */ public static final int AUDIO_FEATURE_VOLUME_GROUP_EVENTS = 4; /** * This is used to determine if audio mirroring is supported via * {@link #isAudioFeatureEnabled(int)} * *
If enabled, audio mirroring can be managed by using the following APIs: * {@link #setAudioZoneMirrorStatusCallback(Executor, AudioZonesMirrorStatusCallback)}, * {@link #clearAudioZonesMirrorStatusCallback()}, {@link #canEnableAudioMirror()}, * {@link #enableMirrorForAudioZones(List)}, {@link #extendAudioMirrorRequest(long, List)}, * {@link #disableAudioMirrorForZone(int)}, {@link #disableAudioMirror(long)}, * {@link #getMirrorAudioZonesForAudioZone(int)}, * {@link #getMirrorAudioZonesForMirrorRequest(long)} */ public static final int AUDIO_FEATURE_AUDIO_MIRRORING = 5; /** * This is used to determine if min/max activation volume level is supported via * {@link #isAudioFeatureEnabled(int)} * *
If enabled, the volume of the volume group with min/max activation volume setting * will be set to min activation volume or max activation volume if volume during activation * is lower than min activation volume or higher than max activation volume respectively. */ @FlaggedApi(Flags.FLAG_CAR_AUDIO_MIN_MAX_ACTIVATION_VOLUME) public static final int AUDIO_FEATURE_MIN_MAX_ACTIVATION_VOLUME = 6; /** @hide */ @IntDef(flag = false, prefix = "AUDIO_FEATURE", value = { AUDIO_FEATURE_DYNAMIC_ROUTING, AUDIO_FEATURE_VOLUME_GROUP_MUTING, AUDIO_FEATURE_OEM_AUDIO_SERVICE, AUDIO_FEATURE_VOLUME_GROUP_EVENTS, AUDIO_FEATURE_AUDIO_MIRRORING, AUDIO_FEATURE_MIN_MAX_ACTIVATION_VOLUME }) @Retention(RetentionPolicy.SOURCE) public @interface CarAudioFeature {} /** * Volume Group ID when volume group not found. * @hide */ public static final int INVALID_VOLUME_GROUP_ID = -1; /** * Use to identify if the request from {@link #requestMediaAudioOnPrimaryZone} is invalid * * @hide */ @SystemApi public static final long INVALID_REQUEST_ID = -1; /** * Extra for {@link android.media.AudioAttributes.Builder#addBundle(Bundle)}: when used in an * {@link android.media.AudioFocusRequest}, the requester should receive all audio focus events, * including {@link android.media.AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK}. * The requester must hold {@link Car#PERMISSION_RECEIVE_CAR_AUDIO_DUCKING_EVENTS}; otherwise, * this extra is ignored. * * @hide */ @SystemApi public static final String AUDIOFOCUS_EXTRA_RECEIVE_DUCKING_EVENTS = "android.car.media.AUDIOFOCUS_EXTRA_RECEIVE_DUCKING_EVENTS"; /** * Extra for {@link android.media.AudioAttributes.Builder#addBundle(Bundle)}: when used in an * {@link android.media.AudioFocusRequest}, the requester should receive all audio focus for the * the zone. If the zone id is not defined: the audio focus request will default to the * currently mapped zone for the requesting uid or {@link CarAudioManager#PRIMARY_AUDIO_ZONE} * if no uid mapping currently exist. * * @hide */ public static final String AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID = "android.car.media.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID"; /** * Use to inform media request callbacks about approval of a media request * * @hide */ @SystemApi public static final int AUDIO_REQUEST_STATUS_APPROVED = 1; /** * Use to inform media request callbacks about rejection of a media request * * @hide */ @SystemApi public static final int AUDIO_REQUEST_STATUS_REJECTED = 2; /** * Use to inform media request callbacks about cancellation of a pending request * * @hide */ @SystemApi public static final int AUDIO_REQUEST_STATUS_CANCELLED = 3; /** * Use to inform media request callbacks about the stop of a media request * * @hide */ @SystemApi public static final int AUDIO_REQUEST_STATUS_STOPPED = 4; /** @hide */ @IntDef(flag = false, prefix = "AUDIO_REQUEST_STATUS", value = { AUDIO_REQUEST_STATUS_APPROVED, AUDIO_REQUEST_STATUS_REJECTED, AUDIO_REQUEST_STATUS_CANCELLED, AUDIO_REQUEST_STATUS_STOPPED }) @Retention(RetentionPolicy.SOURCE) public @interface MediaAudioRequestStatus {} /** * This will be returned by {@link #canEnableAudioMirror()} in case there is an error when * calling the car audio service * * @hide */ @SystemApi public static final int AUDIO_MIRROR_INTERNAL_ERROR = -1; /** * This will be returned by {@link #canEnableAudioMirror()} and determines that it is possible * to enable audio mirroring using the {@link #enableMirrorForAudioZones(List)} * * @hide */ @SystemApi public static final int AUDIO_MIRROR_CAN_ENABLE = 1; /** * This will be returned by {@link #canEnableAudioMirror()} and determines that it is not * possible to enable audio mirroring using the {@link #enableMirrorForAudioZones(List)}. * This informs that there are no more audio mirror output devices available to route audio. * * @hide */ @SystemApi public static final int AUDIO_MIRROR_OUT_OF_OUTPUT_DEVICES = 2; /** @hide */ @IntDef(flag = false, prefix = "AUDIO_MIRROR_", value = { AUDIO_MIRROR_INTERNAL_ERROR, AUDIO_MIRROR_CAN_ENABLE, AUDIO_MIRROR_OUT_OF_OUTPUT_DEVICES, }) @Retention(RetentionPolicy.SOURCE) public @interface AudioMirrorStatus {} /** * Status indicating the dynamic audio configurations info have been updated. * *
Note The list of devices on audio * {@link AudioZoneConfigurationsChangeCallback#onAudioZoneConfigurationsChanged(List, int)}, * will contain all the configuration and each configuration can be perused to find * availability status. For an active configuration becoming disabled due to device * availability, the {@link #CONFIG_STATUS_AUTO_SWITCHED} will be triggered instead. * *
Note This API will only be triggered when a configuration's active status has * changed due to a device connection state changing. * * @hide */ @SystemApi @FlaggedApi(Flags.FLAG_CAR_AUDIO_DYNAMIC_DEVICES) public static final int CONFIG_STATUS_CHANGED = 1; /** * Status indicating the dynamic audio config info has auto switched. * *
Note The list of devices on audio
* {@link AudioZoneConfigurationsChangeCallback#onAudioZoneConfigurationsChanged(List, int)},
* will contain the previously selected configuration and the newly selected configuration only.
*
* @hide
*/
@SystemApi
@FlaggedApi(Flags.FLAG_CAR_AUDIO_DYNAMIC_DEVICES)
public static final int CONFIG_STATUS_AUTO_SWITCHED = 2;
/** @hide */
@IntDef(flag = false, prefix = "CONFIG_STATUS_", value = {
CONFIG_STATUS_CHANGED,
CONFIG_STATUS_AUTO_SWITCHED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface AudioConfigStatus {}
private final ICarAudio mService;
private final CopyOnWriteArrayList The volume information, including mute, blocked, limited state will reflect the state
* of the volume group at the time of query.
*
* @param zoneId zone id for the group to query
* @param groupId group id for the group to query
* @throws IllegalArgumentException if the audio zone or group id are invalid
*
* @return the current volume group info
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
@Nullable
public CarVolumeGroupInfo getVolumeGroupInfo(int zoneId, int groupId) {
try {
return mService.getVolumeGroupInfo(zoneId, groupId);
} catch (RemoteException e) {
return handleRemoteExceptionFromCarService(e, null);
}
}
/**
* Returns a list of volume group info associated with the zone id.
*
* The volume information, including mute, blocked, limited state will reflect the state
* of the volume group at the time of query.
*
* @param zoneId zone id for the group to query
* @throws IllegalArgumentException if the audio zone is invalid
*
* @return all the current volume group info's for the zone id
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
@NonNull
public List If the car audio configuration does not include zone configurations, a default
* configuration consisting current output devices for the zone is returned.
*
* @param zoneId Zone id for the configuration to query
* @return the current car audio zone configuration info, or {@code null} if car audio service
* throws {@link RemoteException}
* @throws IllegalStateException if dynamic audio routing is not enabled
* @throws IllegalArgumentException if the audio zone id is invalid
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
@Nullable
public CarAudioZoneConfigInfo getCurrentAudioZoneConfigInfo(int zoneId) {
try {
return mService.getCurrentAudioZoneConfigInfo(zoneId);
} catch (RemoteException e) {
return handleRemoteExceptionFromCarService(e, null);
}
}
/**
* Returns a list of car audio zone configuration info associated with the zone id
*
* If the car audio configuration does not include zone configurations, a default
* configuration consisting current output devices for each zone is returned.
*
* There exists exactly one zone configuration in primary zone.
*
* @param zoneId Zone id for the configuration to query
* @return all the car audio zone configuration info for the zone id
* @throws IllegalStateException if dynamic audio routing is not enabled
* @throws IllegalArgumentException if the audio zone id is invalid
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
@NonNull
public List To receive the volume group change after configuration is changed, a
* {@link CarVolumeGroupEventCallback} must be registered through
* {@link #registerCarVolumeGroupEventCallback(Executor, CarVolumeGroupEventCallback)} first.
*
* @param zoneConfig Audio zone configuration to switch to
* @param executor Executor on which callback will be invoked
* @param callback Callback that will report the result of switching car audio zone
* configuration
* @throws NullPointerException if either executor or callback are {@code null}
* @throws IllegalStateException if dynamic audio routing is not enabled
* @throws IllegalStateException if no user is assigned to the audio zone
* @throws IllegalStateException if the audio zone is currently in a mirroring configuration
* or sharing audio with primary audio zone
* @throws IllegalArgumentException if the audio zone configuration is invalid
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
public void switchAudioZoneToConfig(@NonNull CarAudioZoneConfigInfo zoneConfig,
@NonNull @CallbackExecutor Executor executor,
@NonNull SwitchAudioZoneConfigCallback callback) {
Objects.requireNonNull(zoneConfig, "Audio zone configuration can not be null");
Objects.requireNonNull(executor, "Executor can not be null");
Objects.requireNonNull(callback,
"Switching audio zone configuration result callback can not be null");
SwitchAudioZoneConfigCallbackWrapper wrapper =
new SwitchAudioZoneConfigCallbackWrapper(executor, callback);
try {
mService.switchZoneToConfig(zoneConfig, wrapper);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
/**
* Sets the audio zone configurations change callback
*
* Note: There are two types on configuration changes.
*
* Config active status changes, signaled by status {@link #CONFIG_STATUS_CHANGED},
* represent changes to the configurations due to a configuration becoming active or inactive as
* a result of a dynamic device being connected or disconnected respectively.
*
* Config auto switch changes, signaled by status {@link #CONFIG_STATUS_AUTO_SWITCHED},
* represent changes to the configurations due a currently selected configuration becoming
* inactive as a result of a dynamic device being disconnected.
*
* @param executor Executor on which callback will be invoked
* @param callback Callback that will be triggered on audio configuration changes
* @return {@code true} if the callback is successfully registered, {@code false} otherwise
* @throws NullPointerException if either executor or callback are {@code null}
* @throws IllegalStateException if dynamic audio routing is not enabled
* @throws IllegalStateException if there is a callback already set
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
@FlaggedApi(Flags.FLAG_CAR_AUDIO_DYNAMIC_DEVICES)
public boolean setAudioZoneConfigsChangeCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull AudioZoneConfigurationsChangeCallback callback) {
Objects.requireNonNull(executor, "Executor can not be null");
Objects.requireNonNull(callback, "Audio zone configs change callback can not be null");
synchronized (mLock) {
if (mZoneConfigurationsChangeCallbackWrapper != null) {
throw new IllegalStateException("Audio zone configs change "
+ "callback is already set");
}
}
AudioZoneConfigurationsChangeCallbackWrapper wrapper =
new AudioZoneConfigurationsChangeCallbackWrapper(executor, callback);
boolean succeeded;
try {
succeeded = mService.registerAudioZoneConfigsChangeCallback(wrapper);
} catch (RemoteException e) {
return handleRemoteExceptionFromCarService(e, false);
}
if (!succeeded) {
return false;
}
boolean error;
synchronized (mLock) {
error = mZoneConfigurationsChangeCallbackWrapper != null;
if (!error) {
mZoneConfigurationsChangeCallbackWrapper = wrapper;
}
}
// In case there was an error, unregister the listener and throw an exception
if (error) {
try {
mService.unregisterAudioZoneConfigsChangeCallback(wrapper);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
throw new IllegalStateException("Audio zone config change callback is already set");
}
return true;
}
/**
* Clears the currently set {@link AudioZoneConfigurationsChangeCallback}
*
* @throws IllegalStateException if dynamic audio routing is not enabled
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
@FlaggedApi(Flags.FLAG_CAR_AUDIO_DYNAMIC_DEVICES)
public void clearAudioZoneConfigsCallback() {
AudioZoneConfigurationsChangeCallbackWrapper wrapper;
synchronized (mLock) {
if (mZoneConfigurationsChangeCallbackWrapper == null) {
Slog.w(TAG, "Audio zone configs callback was already cleared");
return;
}
wrapper = mZoneConfigurationsChangeCallbackWrapper;
mZoneConfigurationsChangeCallbackWrapper = null;
}
try {
mService.unregisterAudioZoneConfigsChangeCallback(wrapper);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
/**
* Gets the audio zones currently available
*
* @return audio zone ids
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
public @NonNull List Note: The results will be notified in the {@link AudioZonesMirrorStatusCallback}
* set via {@link #setAudioZoneMirrorStatusCallback(Executor, AudioZonesMirrorStatusCallback)}
*
* @param audioZonesToMirror List of audio zones that should have audio mirror enabled,
* a minimum of two audio zones are needed to enable mirroring
* @return returns a valid mirror request id if successful or {@code INVALID_REQUEST_ID}
* otherwise
* @throws NullPointerException if the audio mirror list is {@code null}
* @throws IllegalArgumentException if the audio mirror list size is less than 2, if a zone id
* repeats within the list, or if the list contains the {@link #PRIMARY_AUDIO_ZONE}
* @throws IllegalStateException if dynamic audio routing is not enabled, or there is an
* attempt to merge zones from two different mirroring request, or any of the zone ids
* are currently sharing audio to primary zone as allowed via
* {@link #allowMediaAudioOnPrimaryZone(long, boolean)}
* @throws IllegalStateException if audio mirroring feature is disabled, which can be verified
* using {@link #isAudioFeatureEnabled(int)} with the {@link #AUDIO_FEATURE_AUDIO_MIRRORING}
* feature flag
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
public long enableMirrorForAudioZones(@NonNull List Note: The results will be notified in the {@link AudioZonesMirrorStatusCallback}
* set via {@link #setAudioZoneMirrorStatusCallback(Executor, AudioZonesMirrorStatusCallback)}.
* For example, to further extend a mirroring request currently containing zones 1 and 2, with
* a new zone (3) Simply call the API with zone 3 in the list, after the completion of audio
* mirroring extension, zones 1, 2, and 3 will now have mirroring enabled.
*
* @param audioZonesToMirror List of audio zones that will be added to the mirroring request
* @param mirrorId Audio mirroring request to expand with more audio zones
* @throws NullPointerException if the audio mirror list is {@code null}
* @throws IllegalArgumentException if a zone id repeats within the list, or if the list
* contains the {@link #PRIMARY_AUDIO_ZONE}, or if the request id to expand is no longer valid
* @throws IllegalStateException if dynamic audio routing is not enabled, or there is an
* attempt to merge zones from two different mirroring request, or any of the zone ids
* are currently sharing audio to primary zone as allowed via
* {@link #allowMediaAudioOnPrimaryZone(long, boolean)}
* @throws IllegalStateException if audio mirroring feature is disabled, which can be verified
* using {@link #isAudioFeatureEnabled(int)} with the {@link #AUDIO_FEATURE_AUDIO_MIRRORING}
* feature flag
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
public void extendAudioMirrorRequest(long mirrorId, @NonNull List Note: The results will be notified in the {@link AudioZonesMirrorStatusCallback}
* set via {@link #setAudioZoneMirrorStatusCallback(Executor, AudioZonesMirrorStatusCallback)}.
* The results will contain the information for the audio zones whose mirror was cancelled.
* For example, if the mirror configuration only has two zones, mirroring will be undone for
* both zones and the callback will have both zones. On the other hand, if the mirroring
* configuration contains three zones, then this API will only cancel mirroring for one zone
* and the other two zone will continue mirroring. In this case, the callback will only have
* information about the cancelled zone
*
* @param zoneId Zone id where audio mirror should be disabled
* @throws IllegalArgumentException if the zoneId is invalid
* @throws IllegalStateException if dynamic audio routing is not enabled
* @throws IllegalStateException if audio mirroring feature is disabled, which can be verified
* using {@link #isAudioFeatureEnabled(int)} with the {@link #AUDIO_FEATURE_AUDIO_MIRRORING}
* feature flag
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
public void disableAudioMirrorForZone(int zoneId) {
try {
mService.disableAudioMirrorForZone(zoneId);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
/**
* Disables audio mirror for all the zones mirroring in a particular request
*
* Note: The results will be notified in the {@link AudioZonesMirrorStatusCallback}
* set via {@link #setAudioZoneMirrorStatusCallback(Executor, AudioZonesMirrorStatusCallback)}
*
* @param mirrorId Whose audio mirroring should be disabled as obtained via
* {@link #enableMirrorForAudioZones(List)}
* @throws IllegalArgumentException if the request id is no longer valid
* @throws IllegalStateException if dynamic audio routing is not enabled
* @throws IllegalStateException if audio mirroring feature is disabled, which can be verified
* using {@link #isAudioFeatureEnabled(int)} with the {@link #AUDIO_FEATURE_AUDIO_MIRRORING}
* feature flag
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
public void disableAudioMirror(long mirrorId) {
try {
mService.disableAudioMirror(mirrorId);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
/**
* Determines the current mirror configuration for an audio zone as set by
* {@link #enableMirrorForAudioZones(List)} or extended via
* {@link #extendAudioMirrorRequest(long, List)}
*
* @param zoneId The audio zone id where mirror audio should be queried
* @return A list of audio zones where the queried audio zone is mirroring or empty if the
* audio zone is not mirroring with any other audio zone. The list of zones will contain the
* queried zone if audio mirroring is enabled for that zone.
* @throws IllegalArgumentException if the audio zone id is invalid
* @throws IllegalStateException if dynamic audio routing is not enabled
* @throws IllegalStateException if audio mirroring feature is disabled, which can be verified
* using {@link #isAudioFeatureEnabled(int)} with the {@link #AUDIO_FEATURE_AUDIO_MIRRORING}
* feature flag
*
* @hide
*/
@SystemApi
@NonNull
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
public List Note: To be used for routing to a specific device. Most applications should
* use the regular routing mechanism, which is to set audio attribute usage to
* an audio track.
*
* @param zoneId zone id to query for device
* @param usage usage where audio is routed
* @return Audio device info, returns {@code null} if audio device usage fails to map to
* an active audio device. This is different from the using an invalid value for
* {@link AudioAttributes} usage. In the latter case the query will fail with a
* RuntimeException indicating the issue.
*
* @hide
*/
@SystemApi
@Nullable
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
public AudioDeviceInfo getOutputDeviceForUsage(int zoneId, @AttributeUsage int usage) {
try {
String deviceAddress = mService.getOutputDeviceAddressForUsage(zoneId, usage);
if (deviceAddress == null) {
return null;
}
AudioDeviceInfo[] outputDevices =
mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
for (AudioDeviceInfo info : outputDevices) {
if (info.getAddress().equals(deviceAddress)) {
return info;
}
}
return null;
} catch (RemoteException e) {
return handleRemoteExceptionFromCarService(e, null);
}
}
/**
* Gets the input devices for an audio zone
*
* @return list of input devices
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS)
public @NonNull List
* Requires permission Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME
*/
public void registerCarVolumeCallback(@NonNull CarVolumeCallback callback) {
Objects.requireNonNull(callback);
if (mCarVolumeCallbacks.isEmpty()) {
registerVolumeCallback();
}
mCarVolumeCallbacks.add(callback);
}
/**
* Unregisters a {@link CarVolumeCallback} from receiving volume change callbacks
* @param callback {@link CarVolumeCallback} instance previously registered, can not be null
*
* Requires permission Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME
*/
public void unregisterCarVolumeCallback(@NonNull CarVolumeCallback callback) {
Objects.requireNonNull(callback);
if (mCarVolumeCallbacks.contains(callback) && (mCarVolumeCallbacks.size() == 1)) {
unregisterVolumeCallback();
}
mCarVolumeCallbacks.remove(callback);
}
private void registerVolumeCallback() {
try {
mService.registerVolumeCallback(mCarVolumeCallbackImpl.asBinder());
} catch (RemoteException e) {
Slog.e(CarLibLog.TAG_CAR, "registerVolumeCallback failed", e);
}
}
private void unregisterVolumeCallback() {
try {
mService.unregisterVolumeCallback(mCarVolumeCallbackImpl.asBinder());
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
/**
* Registers a {@link CarVolumeGroupEventCallback} to receive volume group event callbacks
*
* @param executor Executor on which callback will be invoked
* @param callback Callback that will report volume group events
* @return {@code true} if the callback is successfully registered, {@code false} otherwise
* @throws NullPointerException if executor or callback parameters is {@code null}
* @throws IllegalStateException if dynamic audio routing is not enabled
* @throws IllegalStateException if volume group events are not enabled
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
public boolean registerCarVolumeGroupEventCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull CarVolumeGroupEventCallback callback) {
Objects.requireNonNull(executor, "Executor can not be null");
Objects.requireNonNull(callback, "Car volume event callback can not be null");
if (mCarVolumeEventCallbacks.isEmpty()) {
if (!registerVolumeGroupEventCallback()) {
return false;
}
}
return mCarVolumeEventCallbacks.addIfAbsent(
new CarVolumeGroupEventCallbackWrapper(executor, callback));
}
private boolean registerVolumeGroupEventCallback() {
try {
if (!mService.registerCarVolumeEventCallback(mCarVolumeEventCallbackImpl)) {
return false;
}
} catch (RemoteException e) {
Slog.e(CarLibLog.TAG_CAR, "registerCarVolumeEventCallback failed", e);
return handleRemoteExceptionFromCarService(e, /* returnValue= */ false);
}
return true;
}
/**
* Unregisters a {@link CarVolumeGroupEventCallback} registered via
* {@link #registerCarVolumeGroupEventCallback}
*
* @param callback The callback to be removed
* @throws NullPointerException if callback is {@code null}
* @throws IllegalStateException if dynamic audio routing is not enabled
* @throws IllegalStateException if volume group events are not enabled
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
public void unregisterCarVolumeGroupEventCallback(
@NonNull CarVolumeGroupEventCallback callback) {
Objects.requireNonNull(callback, "Car volume event callback can not be null");
CarVolumeGroupEventCallbackWrapper callbackWrapper =
new CarVolumeGroupEventCallbackWrapper(/* executor= */ null, callback);
if (mCarVolumeEventCallbacks.contains(callbackWrapper)
&& (mCarVolumeEventCallbacks.size() == 1)) {
unregisterVolumeGroupEventCallback();
}
mCarVolumeEventCallbacks.remove(callbackWrapper);
}
private boolean unregisterVolumeGroupEventCallback() {
try {
if (!mService.unregisterCarVolumeEventCallback(mCarVolumeEventCallbackImpl)) {
Slog.e(CarLibLog.TAG_CAR,
"unregisterCarVolumeEventCallback failed with service");
return false;
}
} catch (RemoteException e) {
Slog.e(CarLibLog.TAG_CAR,
"unregisterCarVolumeEventCallback failed with exception", e);
handleRemoteExceptionFromCarService(e);
}
return true;
}
/**
* Returns the whether a volume group is muted
*
* Note: If {@link #AUDIO_FEATURE_VOLUME_GROUP_MUTING} is disabled this will always
* return {@code false} as group mute is disabled.
*
* @param zoneId The zone id whose volume groups is queried.
* @param groupId The volume group id whose mute state is returned.
* @return {@code true} if the volume group is muted, {@code false}
* otherwise
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
public boolean isVolumeGroupMuted(int zoneId, int groupId) {
try {
return mService.isVolumeGroupMuted(zoneId, groupId);
} catch (RemoteException e) {
return handleRemoteExceptionFromCarService(e, false);
}
}
/**
* Sets a volume group mute
*
* Note: If {@link #AUDIO_FEATURE_VOLUME_GROUP_MUTING} is disabled this will throw an
* error indicating the issue.
*
* @param zoneId The zone id whose volume groups will be changed.
* @param groupId The volume group id whose mute state will be changed.
* @param mute {@code true} to mute volume group, {@code false} otherwise
* @param flags One or more flags (e.g., {@link android.media.AudioManager#FLAG_SHOW_UI},
* {@link android.media.AudioManager#FLAG_PLAY_SOUND})
*
* @hide
*/
@SystemApi
@RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME)
public void setVolumeGroupMute(int zoneId, int groupId, boolean mute, int flags) {
try {
mService.setVolumeGroupMute(zoneId, groupId, mute, flags);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
private List Notes:
* Note: If {@link CarAudioManager#AUDIO_FEATURE_VOLUME_GROUP_MUTING} is disabled
* this will be triggered on mute changes. Otherwise, car audio mute changes will trigger
* {@link #onGroupMuteChanged(int, int, int)}
*
* @param zoneId Id of the audio zone that global mute state change happens
* @param flags see {@link android.media.AudioManager} for flag definitions
*/
@ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
public void onMasterMuteChanged(int zoneId, int flags) {}
/**
* This is called whenever a group mute state is changed.
*
* The changed-to mute state is not included, the caller is encouraged to
* get the current group mute state via CarAudioManager.
*
* Notes:
*
*
*
* @param zoneId Id of the audio zone that volume change happens
* @param groupId Id of the volume group that volume is changed
* @param flags see {@link android.media.AudioManager} for flag definitions
*/
@ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
public void onGroupVolumeChanged(int zoneId, int groupId, int flags) {}
/**
* This is called whenever the global mute state is changed.
* The changed-to global mute state is not included, the caller is encouraged to
* get the current global mute state via AudioManager.
*
*
*
*
* @param zoneId Id of the audio zone that volume change happens
* @param groupId Id of the volume group that volume is changed
* @param flags see {@link android.media.AudioManager} for flag definitions
*/
@ExcludeFromCodeCoverageGeneratedReport(reason = BOILERPLATE_CODE)
public void onGroupMuteChanged(int zoneId, int groupId, int flags) {}
}
private static final class MediaAudioRequestStatusCallbackWrapper
extends IMediaAudioRequestStatusCallback.Stub {
private final Executor mExecutor;
private final MediaAudioRequestStatusCallback mCallback;
MediaAudioRequestStatusCallbackWrapper(Executor executor,
MediaAudioRequestStatusCallback callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public void onMediaAudioRequestStatusChanged(CarOccupantZoneManager.OccupantZoneInfo info,
long requestId,
@CarAudioManager.MediaAudioRequestStatus int status) throws RemoteException {
long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() ->
mCallback.onMediaAudioRequestStatusChanged(info, requestId, status));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
private static final class SwitchAudioZoneConfigCallbackWrapper
extends ISwitchAudioZoneConfigCallback.Stub {
private final Executor mExecutor;
private final SwitchAudioZoneConfigCallback mCallback;
SwitchAudioZoneConfigCallbackWrapper(Executor executor,
SwitchAudioZoneConfigCallback callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public void onAudioZoneConfigSwitched(CarAudioZoneConfigInfo zoneConfig,
boolean isSuccessful) {
long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() ->
mCallback.onAudioZoneConfigSwitched(zoneConfig, isSuccessful));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
private static final class CarVolumeGroupEventCallbackWrapper {
private final Executor mExecutor;
private final CarVolumeGroupEventCallback mCallback;
CarVolumeGroupEventCallbackWrapper(Executor executor,
CarVolumeGroupEventCallback callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CarVolumeGroupEventCallbackWrapper)) {
return false;
}
CarVolumeGroupEventCallbackWrapper rhs = (CarVolumeGroupEventCallbackWrapper) o;
return mCallback == rhs.mCallback;
}
@Override
public int hashCode() {
return mCallback.hashCode();
}
}
private static final class AudioZonesMirrorStatusCallbackWrapper
extends IAudioZonesMirrorStatusCallback.Stub {
private final Executor mExecutor;
private final AudioZonesMirrorStatusCallback mCallback;
AudioZonesMirrorStatusCallbackWrapper(Executor executor,
AudioZonesMirrorStatusCallback callback) {
mExecutor = executor;
mCallback = callback;
}
public void onAudioZonesMirrorStatusChanged(int[] mirroredAudioZones,
int status) {
long identity = Binder.clearCallingIdentity();
try {
mExecutor.execute(() -> mCallback.onAudioZonesMirrorStatusChanged(
asList(mirroredAudioZones), status));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
private static final class AudioZoneConfigurationsChangeCallbackWrapper extends
IAudioZoneConfigurationsChangeCallback.Stub {
private final Executor mExecutor;
private final AudioZoneConfigurationsChangeCallback mCallback;
private AudioZoneConfigurationsChangeCallbackWrapper(Executor executor,
AudioZoneConfigurationsChangeCallback callback) {
mExecutor = executor;
mCallback = callback;
}
@Override
public void onAudioZoneConfigurationsChanged(List