| /* |
| * 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 android.annotation.CallbackExecutor; |
| 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.annotation.AddedInOrBefore; |
| import android.car.annotation.ApiRequirements; |
| 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.Log; |
| |
| 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: |
| * - Audio devices are grouped into zones |
| * - There is at least one primary zone, and extra secondary zones such as RSE |
| * (Rear Seat Entertainment) |
| * - Within each zone, audio devices are grouped into volume groups for volume control |
| * - Audio is assigned to an audio device based on its AudioAttributes usage |
| * |
| * When audio dynamic routing is disabled: |
| * - There is exactly one audio zone, which is the primary zone |
| * - Each volume group represents a controllable STREAM_TYPE, same as AudioManager |
| */ |
| public final class CarAudioManager extends CarManagerBase { |
| |
| private static final String TAG = CarAudioManager.class.getSimpleName(); |
| |
| /** |
| * Zone id of the primary audio zone. |
| * @hide |
| */ |
| @SystemApi |
| @AddedInOrBefore(majorVersion = 33) |
| public static final int PRIMARY_AUDIO_ZONE = 0x0; |
| |
| /** |
| * Zone id of the invalid audio zone. |
| * @hide |
| */ |
| @SystemApi |
| @AddedInOrBefore(majorVersion = 33) |
| public static final int INVALID_AUDIO_ZONE = 0xffffffff; |
| |
| /** |
| * This is used to determine if dynamic routing is enabled via |
| * {@link #isAudioFeatureEnabled()} |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| public static final int AUDIO_FEATURE_DYNAMIC_ROUTING = 1; |
| |
| /** |
| * This is used to determine if volume group muting is enabled via |
| * {@link #isAudioFeatureEnabled()} |
| * |
| * <p> |
| * 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. |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| 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()} |
| * |
| * <p>If enabled, car audio focus, car audio volume, and ducking control behaviour can change |
| * as it can be OEM dependent. |
| */ |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.TIRAMISU_3, |
| minPlatformVersion = ApiRequirements.PlatformVersion.TIRAMISU_0) |
| public static final int AUDIO_FEATURE_OEM_AUDIO_SERVICE = 3; |
| |
| /** @hide */ |
| @IntDef(flag = false, prefix = "AUDIO_FEATURE", value = { |
| AUDIO_FEATURE_DYNAMIC_ROUTING, |
| AUDIO_FEATURE_VOLUME_GROUP_MUTING, |
| AUDIO_FEATURE_OEM_AUDIO_SERVICE |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface CarAudioFeature {} |
| |
| /** |
| * Volume Group ID when volume group not found. |
| * @hide |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| public static final int INVALID_VOLUME_GROUP_ID = -1; |
| |
| /** |
| * Use to identify if the request from {@link #requestMediaAudioOnPrimaryZone} is invalid |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.TIRAMISU_0) |
| 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 |
| @AddedInOrBefore(majorVersion = 33) |
| 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 |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| 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 |
| */ |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @SystemApi |
| public static final int AUDIO_REQUEST_STATUS_APPROVED = 1; |
| |
| /** |
| * Use to inform media request callbacks about rejection of a media request |
| * |
| * @hide |
| */ |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @SystemApi |
| public static final int AUDIO_REQUEST_STATUS_REJECTED = 2; |
| |
| /** |
| * Use to inform media request callbacks about cancellation of a pending request |
| * |
| * @hide |
| */ |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @SystemApi |
| public static final int AUDIO_REQUEST_STATUS_CANCELLED = 3; |
| |
| /** |
| * Use to inform media request callbacks about the stop of a media request |
| * |
| * @hide |
| */ |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @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 {} |
| |
| private final ICarAudio mService; |
| private final CopyOnWriteArrayList<CarVolumeCallback> mCarVolumeCallbacks; |
| private final AudioManager mAudioManager; |
| |
| private final EventHandler mEventHandler; |
| |
| private final Object mLock = new Object(); |
| @GuardedBy("mLock") |
| private PrimaryZoneMediaAudioRequestCallback mPrimaryZoneMediaAudioRequestCallback; |
| @GuardedBy("mLock") |
| private Executor mPrimaryZoneMediaAudioRequestCallbackExecutor; |
| |
| private final ConcurrentHashMap<Long, MediaAudioRequestStatusCallbackWrapper> |
| mRequestIdToMediaAudioRequestStatusCallbacks = new ConcurrentHashMap<>(); |
| |
| private final IPrimaryZoneMediaAudioRequestCallback mIPrimaryZoneMediaAudioRequestCallback = |
| new IPrimaryZoneMediaAudioRequestCallback.Stub() { |
| @Override |
| public void onRequestMediaOnPrimaryZone(OccupantZoneInfo info, |
| long requestId) { |
| runOnExecutor((callback) -> |
| callback.onRequestMediaOnPrimaryZone(info, requestId)); |
| } |
| |
| @Override |
| public void onMediaAudioRequestStatusChanged( |
| @NonNull CarOccupantZoneManager.OccupantZoneInfo info, |
| long requestId, int status) throws RemoteException { |
| runOnExecutor((callback) -> |
| callback.onMediaAudioRequestStatusChanged(info, requestId, status)); |
| } |
| |
| private void runOnExecutor(PrimaryZoneMediaAudioRequestCallbackRunner runner) { |
| PrimaryZoneMediaAudioRequestCallback callback; |
| Executor executor; |
| synchronized (mLock) { |
| if (mPrimaryZoneMediaAudioRequestCallbackExecutor == null |
| || mPrimaryZoneMediaAudioRequestCallback == null) { |
| Log.w(TAG, "Media request removed before change dispatched"); |
| return; |
| } |
| callback = mPrimaryZoneMediaAudioRequestCallback; |
| executor = mPrimaryZoneMediaAudioRequestCallbackExecutor; |
| } |
| |
| long identity = Binder.clearCallingIdentity(); |
| try { |
| executor.execute(() -> runner.runOnCallback(callback)); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| } |
| }; |
| |
| private interface PrimaryZoneMediaAudioRequestCallbackRunner { |
| void runOnCallback(PrimaryZoneMediaAudioRequestCallback callback); |
| } |
| |
| private final ICarVolumeCallback mCarVolumeCallbackImpl = |
| new android.car.media.ICarVolumeCallback.Stub() { |
| @Override |
| public void onGroupVolumeChanged(int zoneId, int groupId, int flags) { |
| mEventHandler.dispatchOnGroupVolumeChanged(zoneId, groupId, flags); |
| } |
| |
| @Override |
| public void onGroupMuteChanged(int zoneId, int groupId, int flags) { |
| mEventHandler.dispatchOnGroupMuteChanged(zoneId, groupId, flags); |
| } |
| |
| @Override |
| public void onMasterMuteChanged(int zoneId, int flags) { |
| mEventHandler.dispatchOnMasterMuteChanged(zoneId, flags); |
| } |
| }; |
| |
| /** |
| * @return Whether dynamic routing is enabled or not. |
| * |
| * @deprecated use {@link #isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING)} instead. |
| * |
| * @hide |
| */ |
| @TestApi |
| @Deprecated |
| @AddedInOrBefore(majorVersion = 33) |
| public boolean isDynamicRoutingEnabled() { |
| return isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING); |
| } |
| |
| /** |
| * Determines if an audio feature is enabled. |
| * |
| * @param audioFeature audio feature to query, can be {@link #AUDIO_FEATURE_DYNAMIC_ROUTING} or |
| * {@link #AUDIO_FEATURE_VOLUME_GROUP_MUTING} |
| * @return Returns {@code true} if the feature is enabled, {@code false} otherwise. |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| public boolean isAudioFeatureEnabled(@CarAudioFeature int audioFeature) { |
| try { |
| return mService.isAudioFeatureEnabled(audioFeature); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, false); |
| } |
| } |
| |
| /** |
| * Sets the volume index for a volume group in primary zone. |
| * |
| * @see {@link #setGroupVolume(int, int, int, int)} |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public void setGroupVolume(int groupId, int index, int flags) { |
| setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, index, flags); |
| } |
| |
| /** |
| * Sets the volume index for a volume group. |
| * |
| * @param zoneId The zone id whose volume group is affected. |
| * @param groupId The volume group id whose volume index should be set. |
| * @param index The volume index to set. See |
| * {@link #getGroupMaxVolume(int, int)} for the largest valid value. |
| * @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) |
| @AddedInOrBefore(majorVersion = 33) |
| public void setGroupVolume(int zoneId, int groupId, int index, int flags) { |
| try { |
| mService.setGroupVolume(zoneId, groupId, index, flags); |
| } catch (RemoteException e) { |
| handleRemoteExceptionFromCarService(e); |
| } |
| } |
| |
| /** |
| * Returns the maximum volume index for a volume group in primary zone. |
| * |
| * @see {@link #getGroupMaxVolume(int, int)} |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getGroupMaxVolume(int groupId) { |
| return getGroupMaxVolume(PRIMARY_AUDIO_ZONE, groupId); |
| } |
| |
| /** |
| * Returns the maximum volume index for a volume group. |
| * |
| * @param zoneId The zone id whose volume group is queried. |
| * @param groupId The volume group id whose maximum volume index is returned. |
| * @return The maximum valid volume index for the given group. |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getGroupMaxVolume(int zoneId, int groupId) { |
| try { |
| return mService.getGroupMaxVolume(zoneId, groupId); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, 0); |
| } |
| } |
| |
| /** |
| * Returns the minimum volume index for a volume group in primary zone. |
| * |
| * @see {@link #getGroupMinVolume(int, int)} |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getGroupMinVolume(int groupId) { |
| return getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId); |
| } |
| |
| /** |
| * Returns the minimum volume index for a volume group. |
| * |
| * @param zoneId The zone id whose volume group is queried. |
| * @param groupId The volume group id whose minimum volume index is returned. |
| * @return The minimum valid volume index for the given group, non-negative |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getGroupMinVolume(int zoneId, int groupId) { |
| try { |
| return mService.getGroupMinVolume(zoneId, groupId); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, 0); |
| } |
| } |
| |
| /** |
| * Returns the current volume index for a volume group in primary zone. |
| * |
| * @see {@link #getGroupVolume(int, int)} |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getGroupVolume(int groupId) { |
| return getGroupVolume(PRIMARY_AUDIO_ZONE, groupId); |
| } |
| |
| /** |
| * Returns the current volume index for a volume group. |
| * |
| * @param zoneId The zone id whose volume groups is queried. |
| * @param groupId The volume group id whose volume index is returned. |
| * @return The current volume index for the given group. |
| * |
| * @see #getGroupMaxVolume(int, int) |
| * @see #setGroupVolume(int, int, int, int) |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getGroupVolume(int zoneId, int groupId) { |
| try { |
| return mService.getGroupVolume(zoneId, groupId); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, 0); |
| } |
| } |
| |
| /** |
| * Adjust the relative volume in the front vs back of the vehicle cabin. |
| * |
| * @param value in the range -1.0 to 1.0 for fully toward the back through |
| * fully toward the front. 0.0 means evenly balanced. |
| * |
| * @throws IllegalArgumentException if {@code value} is less than -1.0 or |
| * greater than 1.0 |
| * @see #setBalanceTowardRight(float) |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public void setFadeTowardFront(float value) { |
| try { |
| mService.setFadeTowardFront(value); |
| } catch (RemoteException e) { |
| handleRemoteExceptionFromCarService(e); |
| } |
| } |
| |
| /** |
| * Adjust the relative volume on the left vs right side of the vehicle cabin. |
| * |
| * @param value in the range -1.0 to 1.0 for fully toward the left through |
| * fully toward the right. 0.0 means evenly balanced. |
| * |
| * @throws IllegalArgumentException if {@code value} is less than -1.0 or |
| * greater than 1.0 |
| * @see #setFadeTowardFront(float) |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public void setBalanceTowardRight(float value) { |
| try { |
| mService.setBalanceTowardRight(value); |
| } catch (RemoteException e) { |
| handleRemoteExceptionFromCarService(e); |
| } |
| } |
| |
| /** |
| * Queries the system configuration in order to report the available, non-microphone audio |
| * input devices. |
| * |
| * @return An array of strings representing the available input ports. |
| * Each port is identified by it's "address" tag in the audioPolicyConfiguration xml file. |
| * Empty array if we find nothing. |
| * |
| * @see #createAudioPatch(String, int, int) |
| * @see #releaseAudioPatch(CarAudioPatchHandle) |
| * |
| * @deprecated use {@link AudioManager#getDevices(int)} with |
| * {@link AudioManager#GET_DEVICES_INPUTS} instead |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| @Deprecated |
| @AddedInOrBefore(majorVersion = 33) |
| public @NonNull String[] getExternalSources() { |
| try { |
| return mService.getExternalSources(); |
| } catch (RemoteException e) { |
| handleRemoteExceptionFromCarService(e); |
| return new String[0]; |
| } |
| } |
| |
| /** |
| * Given an input port identified by getExternalSources(), request that it's audio signal |
| * be routed below the HAL to the output port associated with the given usage. For example, |
| * The output of a tuner might be routed directly to the output buss associated with |
| * AudioAttributes.USAGE_MEDIA while the tuner is playing. |
| * |
| * @param sourceAddress the input port name obtained from getExternalSources(). |
| * @param usage the type of audio represented by this source (usually USAGE_MEDIA). |
| * @param gainInMillibels How many steps above the minimum value defined for the source port to |
| * set the gain when creating the patch. |
| * This may be used for source balancing without affecting the user |
| * controlled volumes applied to the destination ports. A value of |
| * 0 indicates no gain change is requested. |
| * @return A handle for the created patch which can be used to later remove it. |
| * |
| * @see #getExternalSources() |
| * @see #releaseAudioPatch(CarAudioPatchHandle) |
| * |
| * @deprecated use {@link android.media.HwAudioSource} instead |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| @Deprecated |
| @AddedInOrBefore(majorVersion = 33) |
| public CarAudioPatchHandle createAudioPatch(String sourceAddress, @AttributeUsage int usage, |
| int gainInMillibels) { |
| try { |
| return mService.createAudioPatch(sourceAddress, usage, gainInMillibels); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, null); |
| } |
| } |
| |
| /** |
| * Removes the association between an input port and an output port identified by the provided |
| * handle. |
| * |
| * @param patch CarAudioPatchHandle returned from createAudioPatch(). |
| * |
| * @see #getExternalSources() |
| * @see #createAudioPatch(String, int, int) |
| * |
| * @deprecated use {@link android.media.HwAudioSource} instead |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| @Deprecated |
| @AddedInOrBefore(majorVersion = 33) |
| public void releaseAudioPatch(CarAudioPatchHandle patch) { |
| try { |
| mService.releaseAudioPatch(patch); |
| } catch (RemoteException e) { |
| handleRemoteExceptionFromCarService(e); |
| } |
| } |
| |
| /** |
| * Gets the count of available volume groups in primary zone. |
| * |
| * @see {@link #getVolumeGroupCount(int)} |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getVolumeGroupCount() { |
| return getVolumeGroupCount(PRIMARY_AUDIO_ZONE); |
| } |
| |
| /** |
| * Gets the count of available volume groups in the system. |
| * |
| * @param zoneId The zone id whois count of volume groups is queried. |
| * @return Count of volume groups |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getVolumeGroupCount(int zoneId) { |
| try { |
| return mService.getVolumeGroupCount(zoneId); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, 0); |
| } |
| } |
| |
| /** |
| * Gets the volume group id for a given {@link AudioAttributes} usage in primary zone. |
| * |
| * @see {@link #getVolumeGroupIdForUsage(int, int)} |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getVolumeGroupIdForUsage(@AttributeUsage int usage) { |
| return getVolumeGroupIdForUsage(PRIMARY_AUDIO_ZONE, usage); |
| } |
| |
| /** |
| * Gets the volume group id for a given {@link AudioAttributes} usage. |
| * |
| * @param zoneId The zone id whose volume group is queried. |
| * @param usage The {@link AudioAttributes} usage to get a volume group from. |
| * @return The volume group id where the usage belongs to |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getVolumeGroupIdForUsage(int zoneId, @AttributeUsage int usage) { |
| try { |
| return mService.getVolumeGroupIdForUsage(zoneId, usage); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, 0); |
| } |
| } |
| |
| /** |
| * Gets array of {@link AudioAttributes} usages for a volume group in primary zone. |
| * |
| * @see {@link #getUsagesForVolumeGroupId(int, int)} |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public @NonNull int[] getUsagesForVolumeGroupId(int groupId) { |
| return getUsagesForVolumeGroupId(PRIMARY_AUDIO_ZONE, groupId); |
| } |
| |
| /** |
| * Returns the volume group info associated with the zone id and group id. |
| * |
| * <p>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 |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.TIRAMISU_3, |
| minPlatformVersion = ApiRequirements.PlatformVersion.TIRAMISU_0) |
| @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. |
| * |
| * <p>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 |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.TIRAMISU_3, |
| minPlatformVersion = ApiRequirements.PlatformVersion.TIRAMISU_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @NonNull |
| public List<CarVolumeGroupInfo> getVolumeGroupInfosForZone(int zoneId) { |
| try { |
| return mService.getVolumeGroupInfosForZone(zoneId); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, Collections.EMPTY_LIST); |
| } |
| } |
| |
| /** |
| * Returns a list of audio attributes associated with the volume group info. |
| * |
| * @param groupInfo group info to query |
| * @throws NullPointerException if the volume group info is {@code null} |
| * |
| * @return list of audio attributes associated with the volume group info |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.TIRAMISU_3, |
| minPlatformVersion = ApiRequirements.PlatformVersion.TIRAMISU_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @NonNull |
| public List<AudioAttributes> getAudioAttributesForVolumeGroup( |
| @NonNull CarVolumeGroupInfo groupInfo) { |
| try { |
| return mService.getAudioAttributesForVolumeGroup(groupInfo); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, Collections.EMPTY_LIST); |
| } |
| } |
| |
| /** |
| * Gets array of {@link AudioAttributes} usages for a volume group in a zone. |
| * |
| * @param zoneId The zone id whose volume group is queried. |
| * @param groupId The volume group id whose associated audio usages is returned. |
| * @return Array of {@link AudioAttributes} usages for a given volume group id |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public @NonNull int[] getUsagesForVolumeGroupId(int zoneId, int groupId) { |
| try { |
| return mService.getUsagesForVolumeGroupId(zoneId, groupId); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, new int[0]); |
| } |
| } |
| |
| /** |
| * Determines if a particular volume group has any audio playback in a zone |
| * |
| * @param zoneId The zone id whose volume group is queried. |
| * @param groupId The volume group id whose associated audio usages is returned. |
| * @return {@code true} if the group has active playback, {@code false} otherwise |
| * |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME) |
| @AddedInOrBefore(majorVersion = 33) |
| public boolean isPlaybackOnVolumeGroupActive(int zoneId, int groupId) { |
| try { |
| return mService.isPlaybackOnVolumeGroupActive(zoneId, groupId); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, false); |
| } |
| } |
| |
| /** |
| * Returns the current car audio zone configuration info associated with the zone id |
| * |
| * <p>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 |
| * @throws IllegalStateException if dynamic audio routing is not enabled |
| * @throws IllegalArgumentException if the audio zone id is invalid |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| @NonNull |
| 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 |
| * |
| * <p>If the car audio configuration does not include zone configurations, a default |
| * configuration consisting current output devices for each zone is returned. |
| * |
| * @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 |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| @NonNull |
| public List<CarAudioZoneConfigInfo> getAudioZoneConfigInfos(int zoneId) { |
| try { |
| return mService.getAudioZoneConfigInfos(zoneId); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, Collections.EMPTY_LIST); |
| } |
| } |
| |
| /** |
| * Switches the car audio zone configuration |
| * |
| * @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 IllegalArgumentException if the audio zone configuration is invalid |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @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); |
| } |
| } |
| |
| /** |
| * Gets the audio zones currently available |
| * |
| * @return audio zone ids |
| * @hide |
| */ |
| @SystemApi |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| @AddedInOrBefore(majorVersion = 33) |
| public @NonNull List<Integer> getAudioZoneIds() { |
| try { |
| int[] zoneIdArray = mService.getAudioZoneIds(); |
| List<Integer> zoneIdList = new ArrayList<Integer>(zoneIdArray.length); |
| for (int zoneIdValue : zoneIdArray) { |
| zoneIdList.add(zoneIdValue); |
| } |
| return zoneIdList; |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, Collections.emptyList()); |
| } |
| } |
| |
| /** |
| * Gets the audio zone id currently mapped to uId, |
| * defaults to PRIMARY_AUDIO_ZONE if no mapping exist |
| * |
| * @param uid The uid to map |
| * @return zone id mapped to uid |
| * @hide |
| */ |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| @AddedInOrBefore(majorVersion = 33) |
| public int getZoneIdForUid(int uid) { |
| try { |
| return mService.getZoneIdForUid(uid); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, 0); |
| } |
| } |
| |
| /** |
| * Maps the audio zone id to uid |
| * |
| * @param zoneId The audio zone id |
| * @param uid The uid to map |
| * @return true if the uid is successfully mapped |
| * @hide |
| */ |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| @AddedInOrBefore(majorVersion = 33) |
| public boolean setZoneIdForUid(int zoneId, int uid) { |
| try { |
| return mService.setZoneIdForUid(zoneId, uid); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, false); |
| } |
| } |
| |
| /** |
| * Clears the current zone mapping of the uid |
| * |
| * @param uid The uid to clear |
| * @return true if the zone was successfully cleared |
| * |
| * @hide |
| */ |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| @AddedInOrBefore(majorVersion = 33) |
| public boolean clearZoneIdForUid(int uid) { |
| try { |
| return mService.clearZoneIdForUid(uid); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, false); |
| } |
| } |
| |
| /** |
| * Sets a {@code PrimaryZoneMediaAudioRequestStatusCallback} to listen for request to play |
| * media audio in primary audio zone |
| * |
| * @param executor Executor on which callback will be invoked |
| * @param callback Media audio request callback to monitor for audio requests |
| * @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 |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| public boolean setPrimaryZoneMediaAudioRequestCallback( |
| @NonNull @CallbackExecutor Executor executor, |
| @NonNull PrimaryZoneMediaAudioRequestCallback callback) { |
| Objects.requireNonNull(executor, "Executor can not be null"); |
| Objects.requireNonNull(callback, "Audio media request callback can not be null"); |
| synchronized (mLock) { |
| if (mPrimaryZoneMediaAudioRequestCallback != null) { |
| throw new IllegalStateException("Primary zone media audio request is already set"); |
| } |
| } |
| |
| try { |
| if (!mService.registerPrimaryZoneMediaAudioRequestCallback( |
| mIPrimaryZoneMediaAudioRequestCallback)) { |
| return false; |
| } |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, /* returnValue= */ false); |
| } |
| |
| synchronized (mLock) { |
| mPrimaryZoneMediaAudioRequestCallback = callback; |
| mPrimaryZoneMediaAudioRequestCallbackExecutor = executor; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Clears the currently set {@code PrimaryZoneMediaAudioRequestCallback} |
| * |
| * @throws IllegalStateException if dynamic audio routing is not enabled |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| public void clearPrimaryZoneMediaAudioRequestCallback() { |
| synchronized (mLock) { |
| if (mPrimaryZoneMediaAudioRequestCallback == null) { |
| return; |
| } |
| } |
| |
| try { |
| mService.unregisterPrimaryZoneMediaAudioRequestCallback( |
| mIPrimaryZoneMediaAudioRequestCallback); |
| } catch (RemoteException e) { |
| handleRemoteExceptionFromCarService(e); |
| } |
| |
| synchronized (mLock) { |
| mPrimaryZoneMediaAudioRequestCallback = null; |
| mPrimaryZoneMediaAudioRequestCallbackExecutor = null; |
| } |
| } |
| |
| /** |
| * Cancels a request set by {@code requestMediaAudioOnPrimaryZone} |
| * |
| * @param requestId Request id to cancel |
| * @return {@code true} if request is successfully cancelled |
| * @throws IllegalStateException if dynamic audio routing is not enabled |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| public boolean cancelMediaAudioOnPrimaryZone(long requestId) { |
| try { |
| if (removeMediaRequestCallback(requestId)) { |
| return mService.cancelMediaAudioOnPrimaryZone(requestId); |
| } |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, /* returnValue= */ false); |
| } |
| |
| return true; |
| } |
| |
| private boolean removeMediaRequestCallback(long requestId) { |
| return mRequestIdToMediaAudioRequestStatusCallbacks.remove(requestId) != null; |
| } |
| |
| /** |
| * Requests to play audio in primary zone with information contained in {@code request} |
| * |
| * @param info Occupant zone info whose media audio should be shared to primary zone |
| * @param executor Executor on which callback will be invoked |
| * @param callback Callback that will report the status changes of the request |
| * @return returns a valid request id if successful or {@code INVALID_REQUEST_ID} otherwise |
| * @throws NullPointerException if any of info, executor, or callback parameters are |
| * {@code null} |
| * @throws IllegalStateException if dynamic audio routing is not enabled |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| public long requestMediaAudioOnPrimaryZone(@NonNull OccupantZoneInfo info, |
| @NonNull @CallbackExecutor Executor executor, |
| @NonNull MediaAudioRequestStatusCallback callback) { |
| Objects.requireNonNull(executor, "Occupant zone info can not be null"); |
| Objects.requireNonNull(executor, "Executor can not be null"); |
| Objects.requireNonNull(callback, "Media audio request status callback can not be null"); |
| |
| MediaAudioRequestStatusCallbackWrapper wrapper = |
| new MediaAudioRequestStatusCallbackWrapper(executor, callback); |
| |
| long requestId; |
| try { |
| requestId = mService.requestMediaAudioOnPrimaryZone(wrapper, info); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, INVALID_REQUEST_ID); |
| } |
| |
| if (requestId == INVALID_REQUEST_ID) { |
| return requestId; |
| } |
| |
| mRequestIdToMediaAudioRequestStatusCallbacks.put(requestId, wrapper); |
| return requestId; |
| } |
| |
| /** |
| * Allow/rejects audio to play for a request |
| * {@code requestMediaAudioOnPrimaryZone(MediaRequest, Handler)} |
| * |
| * @param requestId Request id to approve |
| * @param allow Boolean indicating to allow or reject, {@code true} to allow audio |
| * playback on primary zone, {@code false} otherwise |
| * @return {@code false} if media is not successfully allowed/rejected for the request, |
| * including the case when the request id is {@link #INVALID_REQUEST_ID} |
| * @throws IllegalStateException if no {@code PrimaryZoneMediaAudioRequestCallback} is |
| * registered prior to calling this method. |
| * @throws IllegalStateException if dynamic audio routing is not enabled |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| public boolean allowMediaAudioOnPrimaryZone(long requestId, boolean allow) { |
| synchronized (mLock) { |
| if (mPrimaryZoneMediaAudioRequestCallback == null) { |
| throw new IllegalStateException("Primary zone media audio request callback must be " |
| + "registered to allow/reject playback"); |
| } |
| } |
| |
| try { |
| return mService.allowMediaAudioOnPrimaryZone( |
| mIPrimaryZoneMediaAudioRequestCallback.asBinder(), requestId, allow); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, /* returnValue= */ false); |
| } |
| } |
| |
| /** |
| * Resets the media audio playback in primary zone from occupant |
| * |
| * @param info Occupant's audio to reset in primary zone |
| * @return {@code true} if audio is successfully reset, {@code false} otherwise including case |
| * where audio is not currently assigned |
| * @throws IllegalStateException if dynamic audio routing is not enabled |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| public boolean resetMediaAudioOnPrimaryZone(@NonNull OccupantZoneInfo info) { |
| try { |
| return mService.resetMediaAudioOnPrimaryZone(info); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, /* returnValue= */ false); |
| } |
| } |
| |
| /** |
| * Determines if audio from occupant is allowed in primary zone |
| * |
| * @param info Occupant zone info to query |
| * @return {@code true} if audio playback from occupant is allowed in primary zone |
| * @throws IllegalStateException if dynamic audio routing is not enabled |
| * |
| * @hide |
| */ |
| @SystemApi |
| @ApiRequirements(minCarVersion = ApiRequirements.CarVersion.UPSIDE_DOWN_CAKE_0, |
| minPlatformVersion = ApiRequirements.PlatformVersion.UPSIDE_DOWN_CAKE_0) |
| @RequiresPermission(Car.PERMISSION_CAR_CONTROL_AUDIO_SETTINGS) |
| public boolean isMediaAudioAllowedInPrimaryZone(@NonNull OccupantZoneInfo info) { |
| try { |
| return mService.isMediaAudioAllowedInPrimaryZone(info); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, /* returnValue= */ false); |
| } |
| } |
| |
| /** |
| * Gets the output device for a given {@link AudioAttributes} usage in zoneId. |
| * |
| * <p><b>Note:</b> 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) |
| @AddedInOrBefore(majorVersion = 33) |
| 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) |
| @AddedInOrBefore(majorVersion = 33) |
| public @NonNull List<AudioDeviceInfo> getInputDevicesForZoneId(int zoneId) { |
| try { |
| return convertInputDevicesToDeviceInfos( |
| mService.getInputDevicesForZoneId(zoneId), |
| AudioManager.GET_DEVICES_INPUTS); |
| } catch (RemoteException e) { |
| return handleRemoteExceptionFromCarService(e, Collections.EMPTY_LIST); |
| } |
| } |
| |
| /** @hide */ |
| @Override |
| @AddedInOrBefore(majorVersion = 33) |
| public void onCarDisconnected() { |
| if (mService != null && !mCarVolumeCallbacks.isEmpty()) { |
| unregisterVolumeCallback(); |
| } |
| } |
| |
| /** @hide */ |
| public CarAudioManager(Car car, IBinder service) { |
| super(car); |
| mService = ICarAudio.Stub.asInterface(service); |
| mAudioManager = getContext().getSystemService(AudioManager.class); |
| mCarVolumeCallbacks = new CopyOnWriteArrayList<>(); |
| mEventHandler = new EventHandler(getEventHandler().getLooper()); |
| } |
| |
| /** |
| * Registers a {@link CarVolumeCallback} to receive volume change callbacks |
| * @param callback {@link CarVolumeCallback} instance, can not be null |
| * <p> |
| * Requires permission Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| 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 |
| * <p> |
| * Requires permission Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| public void unregisterCarVolumeCallback(@NonNull CarVolumeCallback callback) { |
| Objects.requireNonNull(callback); |
| if (mCarVolumeCallbacks.remove(callback) && mCarVolumeCallbacks.isEmpty()) { |
| unregisterVolumeCallback(); |
| } |
| } |
| |
| private void registerVolumeCallback() { |
| try { |
| mService.registerVolumeCallback(mCarVolumeCallbackImpl.asBinder()); |
| } catch (RemoteException e) { |
| Log.e(CarLibLog.TAG_CAR, "registerVolumeCallback failed", e); |
| } |
| } |
| |
| private void unregisterVolumeCallback() { |
| try { |
| mService.unregisterVolumeCallback(mCarVolumeCallbackImpl.asBinder()); |
| } catch (RemoteException e) { |
| handleRemoteExceptionFromCarService(e); |
| } |
| } |
| |
| /** |
| * Returns the whether a volume group is muted |
| * |
| * <p><b>Note:<b/> 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) |
| @AddedInOrBefore(majorVersion = 33) |
| 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 |
| * |
| * <p><b>Note:<b/> 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) |
| @AddedInOrBefore(majorVersion = 33) |
| 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<AudioDeviceInfo> convertInputDevicesToDeviceInfos( |
| List<AudioDeviceAttributes> devices, int flag) { |
| int addressesSize = devices.size(); |
| Set<String> deviceAddressMap = new HashSet<>(addressesSize); |
| for (int i = 0; i < addressesSize; ++i) { |
| AudioDeviceAttributes device = devices.get(i); |
| deviceAddressMap.add(device.getAddress()); |
| } |
| List<AudioDeviceInfo> deviceInfoList = new ArrayList<>(devices.size()); |
| AudioDeviceInfo[] inputDevices = mAudioManager.getDevices(flag); |
| for (int i = 0; i < inputDevices.length; ++i) { |
| AudioDeviceInfo info = inputDevices[i]; |
| if (info.isSource() && deviceAddressMap.contains(info.getAddress())) { |
| deviceInfoList.add(info); |
| } |
| } |
| return deviceInfoList; |
| } |
| |
| private final class EventHandler extends Handler { |
| private static final int MSG_GROUP_VOLUME_CHANGE = 1; |
| private static final int MSG_GROUP_MUTE_CHANGE = 2; |
| private static final int MSG_MASTER_MUTE_CHANGE = 3; |
| |
| private EventHandler(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_GROUP_VOLUME_CHANGE: |
| VolumeGroupChangeInfo volumeInfo = (VolumeGroupChangeInfo) msg.obj; |
| handleOnGroupVolumeChanged(volumeInfo.mZoneId, volumeInfo.mGroupId, |
| volumeInfo.mFlags); |
| break; |
| case MSG_GROUP_MUTE_CHANGE: |
| VolumeGroupChangeInfo muteInfo = (VolumeGroupChangeInfo) msg.obj; |
| handleOnGroupMuteChanged(muteInfo.mZoneId, muteInfo.mGroupId, muteInfo.mFlags); |
| break; |
| case MSG_MASTER_MUTE_CHANGE: |
| handleOnMasterMuteChanged(msg.arg1, msg.arg2); |
| break; |
| default: |
| Log.e(CarLibLog.TAG_CAR, "Unknown message not handled:" + msg.what); |
| break; |
| } |
| } |
| |
| private void dispatchOnGroupVolumeChanged(int zoneId, int groupId, int flags) { |
| VolumeGroupChangeInfo volumeInfo = new VolumeGroupChangeInfo(zoneId, groupId, flags); |
| sendMessage(obtainMessage(MSG_GROUP_VOLUME_CHANGE, volumeInfo)); |
| } |
| |
| private void dispatchOnMasterMuteChanged(int zoneId, int flags) { |
| sendMessage(obtainMessage(MSG_MASTER_MUTE_CHANGE, zoneId, flags)); |
| } |
| |
| private void dispatchOnGroupMuteChanged(int zoneId, int groupId, int flags) { |
| VolumeGroupChangeInfo volumeInfo = new VolumeGroupChangeInfo(zoneId, groupId, flags); |
| sendMessage(obtainMessage(MSG_GROUP_MUTE_CHANGE, volumeInfo)); |
| } |
| |
| private class VolumeGroupChangeInfo { |
| public int mZoneId; |
| public int mGroupId; |
| public int mFlags; |
| |
| VolumeGroupChangeInfo(int zoneId, int groupId, int flags) { |
| mZoneId = zoneId; |
| mGroupId = groupId; |
| mFlags = flags; |
| } |
| } |
| } |
| |
| private void handleOnGroupVolumeChanged(int zoneId, int groupId, int flags) { |
| for (CarVolumeCallback callback : mCarVolumeCallbacks) { |
| callback.onGroupVolumeChanged(zoneId, groupId, flags); |
| } |
| } |
| |
| private void handleOnMasterMuteChanged(int zoneId, int flags) { |
| for (CarVolumeCallback callback : mCarVolumeCallbacks) { |
| callback.onMasterMuteChanged(zoneId, flags); |
| } |
| } |
| |
| private void handleOnGroupMuteChanged(int zoneId, int groupId, int flags) { |
| for (CarVolumeCallback callback : mCarVolumeCallbacks) { |
| callback.onGroupMuteChanged(zoneId, groupId, flags); |
| } |
| } |
| |
| /** |
| * Callback interface to receive volume change events in a car. |
| * Extend this class and register it with {@link #registerCarVolumeCallback(CarVolumeCallback)} |
| * and unregister it via {@link #unregisterCarVolumeCallback(CarVolumeCallback)} |
| */ |
| public abstract static class CarVolumeCallback { |
| /** |
| * This is called whenever a group volume is changed. |
| * The changed-to volume index is not included, the caller is encouraged to |
| * get the current group volume index via CarAudioManager. |
| * |
| * @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 |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| 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. |
| * |
| * <p><b>Note:<b/> 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 |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| 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. |
| * |
| * <p><b>Note:<b/> If {@link CarAudioManager#AUDIO_FEATURE_VOLUME_GROUP_MUTING} is enabled |
| * this will be triggered on mute changes. Otherwise, car audio mute changes will trigger |
| * {@link #onMasterMuteChanged(int, int)} |
| * |
| * @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 |
| */ |
| @AddedInOrBefore(majorVersion = 33) |
| 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); |
| } |
| } |
| } |
| } |