diff options
author | Haamed Gheibi <haamed@google.com> | 2022-03-09 12:05:14 -0800 |
---|---|---|
committer | Weijie Wang <quic_weijiew@quicinc.com> | 2022-03-15 15:38:25 +0800 |
commit | 12bb6d3cbf05cea529a165917c7430af607056f2 (patch) | |
tree | ff322630f9716306236ca70ecae1f265ae2aa2c6 /media | |
parent | a42412b7fc93a0eb852d8bf1a4d001f7df7f43b3 (diff) |
Merge SP2A.220305.013
Bug: 220074017
Change-Id: Idfdd94e902f656ac65a2a75dfdd199f6f85ba472
Diffstat (limited to 'media')
19 files changed, 1598 insertions, 53 deletions
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index a031b4cfc911..c22ab9463736 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -447,13 +447,26 @@ public final class AudioAttributes implements Parcelable { */ public static final int FLAG_CAPTURE_PRIVATE = 0x1 << 13; + /** + * @hide + * Flag indicating the audio content has been processed to provide a virtual multichannel + * audio experience + */ + public static final int FLAG_CONTENT_SPATIALIZED = 0x1 << 14; + + /** + * @hide + * Flag indicating the audio content is to never be spatialized + */ + public static final int FLAG_NEVER_SPATIALIZE = 0x1 << 15; // Note that even though FLAG_MUTE_HAPTIC is stored as a flag bit, it is not here since // it is known as a boolean value outside of AudioAttributes. private static final int FLAG_ALL = FLAG_AUDIBILITY_ENFORCED | FLAG_SECURE | FLAG_SCO | FLAG_BEACON | FLAG_HW_AV_SYNC | FLAG_HW_HOTWORD | FLAG_BYPASS_INTERRUPTION_POLICY | FLAG_BYPASS_MUTE | FLAG_LOW_LATENCY | FLAG_DEEP_BUFFER | FLAG_NO_MEDIA_PROJECTION - | FLAG_NO_SYSTEM_CAPTURE | FLAG_CAPTURE_PRIVATE; + | FLAG_NO_SYSTEM_CAPTURE | FLAG_CAPTURE_PRIVATE | FLAG_CONTENT_SPATIALIZED + | FLAG_NEVER_SPATIALIZE; private final static int FLAG_ALL_PUBLIC = FLAG_AUDIBILITY_ENFORCED | FLAG_HW_AV_SYNC | FLAG_LOW_LATENCY; /* mask of flags that can be set by SDK and System APIs through the Builder */ @@ -615,6 +628,49 @@ public final class AudioAttributes implements Parcelable { } /** + * Return true if the audio content associated with these attributes has already been + * spatialized, that is it has already been processed to offer a binaural or transaural + * immersive audio experience. + * @return {@code true} if the content has been processed + */ + public boolean isContentSpatialized() { + return (mFlags & FLAG_CONTENT_SPATIALIZED) != 0; + } + + /** @hide */ + @IntDef(flag = false, value = { + SPATIALIZATION_BEHAVIOR_AUTO, + SPATIALIZATION_BEHAVIOR_NEVER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SpatializationBehavior {}; + + /** + * Constant indicating the audio content associated with these attributes will follow the + * default platform behavior with regards to which content will be spatialized or not. + * @see #getSpatializationBehavior() + * @see Spatializer + */ + public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0; + + /** + * Constant indicating the audio content associated with these attributes should never + * be virtualized. + * @see #getSpatializationBehavior() + * @see Spatializer + */ + public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1; + + /** + * Return the behavior affecting whether spatialization will be used. + * @return the spatialization behavior + */ + public @SpatializationBehavior int getSpatializationBehavior() { + return ((mFlags & FLAG_NEVER_SPATIALIZE) != 0) + ? SPATIALIZATION_BEHAVIOR_NEVER : SPATIALIZATION_BEHAVIOR_AUTO; + } + + /** * Return the capture policy. * @return the capture policy set by {@link Builder#setAllowedCapturePolicy(int)} or * the default if it was not called. @@ -657,6 +713,8 @@ public final class AudioAttributes implements Parcelable { private int mSource = MediaRecorder.AudioSource.AUDIO_SOURCE_INVALID; private int mFlags = 0x0; private boolean mMuteHapticChannels = true; + private boolean mIsContentSpatialized = false; + private int mSpatializationBehavior = SPATIALIZATION_BEHAVIOR_AUTO; private HashSet<String> mTags = new HashSet<String>(); private Bundle mBundle; private int mPrivacySensitive = PRIVACY_SENSITIVE_DEFAULT; @@ -687,6 +745,8 @@ public final class AudioAttributes implements Parcelable { mFlags = aa.getAllFlags(); mTags = (HashSet<String>) aa.mTags.clone(); mMuteHapticChannels = aa.areHapticChannelsMuted(); + mIsContentSpatialized = aa.isContentSpatialized(); + mSpatializationBehavior = aa.getSpatializationBehavior(); } /** @@ -719,6 +779,12 @@ public final class AudioAttributes implements Parcelable { if (mMuteHapticChannels) { aa.mFlags |= FLAG_MUTE_HAPTIC; } + if (mIsContentSpatialized) { + aa.mFlags |= FLAG_CONTENT_SPATIALIZED; + } + if (mSpatializationBehavior == SPATIALIZATION_BEHAVIOR_NEVER) { + aa.mFlags |= FLAG_NEVER_SPATIALIZE; + } if (mPrivacySensitive == PRIVACY_SENSITIVE_DEFAULT) { // capturing for camcorder or communication is private by default to @@ -906,6 +972,35 @@ public final class AudioAttributes implements Parcelable { } /** + * Specifies whether the content has already been processed for spatialization. + * If it has, setting this to true will prevent issues such as double-processing. + * @param isSpatialized + * @return the same Builder instance + */ + public @NonNull Builder setIsContentSpatialized(boolean isSpatialized) { + mIsContentSpatialized = isSpatialized; + return this; + } + + /** + * Sets the behavior affecting whether spatialization will be used. + * @param sb the spatialization behavior + * @return the same Builder instance + * + */ + public @NonNull Builder setSpatializationBehavior(@SpatializationBehavior int sb) { + switch (sb) { + case SPATIALIZATION_BEHAVIOR_NEVER: + case SPATIALIZATION_BEHAVIOR_AUTO: + break; + default: + throw new IllegalArgumentException("Invalid spatialization behavior " + sb); + } + mSpatializationBehavior = sb; + return this; + } + + /** * @hide * Replaces flags. * @param flags any combination of {@link AudioAttributes#FLAG_ALL}. @@ -990,6 +1085,8 @@ public final class AudioAttributes implements Parcelable { mContentType = attributes.mContentType; mFlags = attributes.getAllFlags(); mMuteHapticChannels = attributes.areHapticChannelsMuted(); + mIsContentSpatialized = attributes.isContentSpatialized(); + mSpatializationBehavior = attributes.getSpatializationBehavior(); mTags = attributes.mTags; mBundle = attributes.mBundle; mSource = attributes.mSource; diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index b07dc9b581b2..2988ca8c715c 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -175,6 +175,28 @@ import java.util.Objects; * <br>These masks are an ORed composite of individual channel masks. For example * {@link #CHANNEL_OUT_STEREO} is composed of {@link #CHANNEL_OUT_FRONT_LEFT} and * {@link #CHANNEL_OUT_FRONT_RIGHT}. + * <p> + * The following diagram represents the layout of the output channels, as seen from above + * the listener (in the center at the "lis" position, facing the front-center channel). + * <pre> + * TFL ----- TFC ----- TFR T is Top + * | \ | / | + * | FL --- FC --- FR | F is Front + * | |\ | /| | + * | | BFL-BFC-BFR | | BF is Bottom Front + * | | | | + * | FWL lis FWR | W is Wide + * | | | | + * TSL SL TC SR TSR S is Side + * | | | | + * | BL --- BC -- BR | B is Back + * | / \ | + * TBL ----- TBC ----- TBR C is Center, L/R is Left/Right + * </pre> + * All "T" (top) channels are above the listener, all "BF" (bottom-front) channels are below the + * listener, all others are in the listener's horizontal plane. When used in conjunction, LFE1 and + * LFE2 are below the listener, when used alone, LFE plane is undefined. + * See the channel definitions for the abbreviations * * <h5 id="channelIndexMask">Channel index masks</h5> * Channel index masks are introduced in API {@link android.os.Build.VERSION_CODES#M}. They allow @@ -442,43 +464,62 @@ public final class AudioFormat implements Parcelable { // Output channel mask definitions below are translated to the native values defined in // in /system/media/audio/include/system/audio.h in the JNI code of AudioTrack + /** Front left output channel (see FL in channel diagram) */ public static final int CHANNEL_OUT_FRONT_LEFT = 0x4; + /** Front right output channel (see FR in channel diagram) */ public static final int CHANNEL_OUT_FRONT_RIGHT = 0x8; + /** Front center output channel (see FC in channel diagram) */ public static final int CHANNEL_OUT_FRONT_CENTER = 0x10; + /** LFE "low frequency effect" channel + * When used in conjunction with {@link #CHANNEL_OUT_LOW_FREQUENCY_2}, it is intended + * to contain the left low-frequency effect signal, also referred to as "LFE1" + * in ITU-R BS.2159-8 */ public static final int CHANNEL_OUT_LOW_FREQUENCY = 0x20; + /** Back left output channel (see BL in channel diagram) */ public static final int CHANNEL_OUT_BACK_LEFT = 0x40; + /** Back right output channel (see BR in channel diagram) */ public static final int CHANNEL_OUT_BACK_RIGHT = 0x80; public static final int CHANNEL_OUT_FRONT_LEFT_OF_CENTER = 0x100; public static final int CHANNEL_OUT_FRONT_RIGHT_OF_CENTER = 0x200; + /** Back center output channel (see BC in channel diagram) */ public static final int CHANNEL_OUT_BACK_CENTER = 0x400; + /** Side left output channel (see SL in channel diagram) */ public static final int CHANNEL_OUT_SIDE_LEFT = 0x800; + /** Side right output channel (see SR in channel diagram) */ public static final int CHANNEL_OUT_SIDE_RIGHT = 0x1000; - /** @hide */ + /** Top center (above listener) output channel (see TC in channel diagram) */ public static final int CHANNEL_OUT_TOP_CENTER = 0x2000; - /** @hide */ + /** Top front left output channel (see TFL in channel diagram above FL) */ public static final int CHANNEL_OUT_TOP_FRONT_LEFT = 0x4000; - /** @hide */ + /** Top front center output channel (see TFC in channel diagram above FC) */ public static final int CHANNEL_OUT_TOP_FRONT_CENTER = 0x8000; - /** @hide */ + /** Top front right output channel (see TFR in channel diagram above FR) */ public static final int CHANNEL_OUT_TOP_FRONT_RIGHT = 0x10000; - /** @hide */ + /** Top back left output channel (see TBL in channel diagram above BL) */ public static final int CHANNEL_OUT_TOP_BACK_LEFT = 0x20000; - /** @hide */ + /** Top back center output channel (see TBC in channel diagram above BC) */ public static final int CHANNEL_OUT_TOP_BACK_CENTER = 0x40000; - /** @hide */ + /** Top back right output channel (see TBR in channel diagram above BR) */ public static final int CHANNEL_OUT_TOP_BACK_RIGHT = 0x80000; - /** @hide */ + /** Top side left output channel (see TSL in channel diagram above SL) */ public static final int CHANNEL_OUT_TOP_SIDE_LEFT = 0x100000; - /** @hide */ + /** Top side right output channel (see TSR in channel diagram above SR) */ public static final int CHANNEL_OUT_TOP_SIDE_RIGHT = 0x200000; - /** @hide */ + /** Bottom front left output channel (see BFL in channel diagram below FL) */ public static final int CHANNEL_OUT_BOTTOM_FRONT_LEFT = 0x400000; - /** @hide */ + /** Bottom front center output channel (see BFC in channel diagram below FC) */ public static final int CHANNEL_OUT_BOTTOM_FRONT_CENTER = 0x800000; - /** @hide */ + /** Bottom front right output channel (see BFR in channel diagram below FR) */ public static final int CHANNEL_OUT_BOTTOM_FRONT_RIGHT = 0x1000000; - /** @hide */ + /** The second LFE channel + * When used in conjunction with {@link #CHANNEL_OUT_LOW_FREQUENCY}, it is intended + * to contain the right low-frequency effect signal, also referred to as "LFE2" + * in ITU-R BS.2159-8 */ public static final int CHANNEL_OUT_LOW_FREQUENCY_2 = 0x2000000; + /** Front wide left output channel (see FWL in channel diagram) */ + public static final int CHANNEL_OUT_FRONT_WIDE_LEFT = 0x4000000; + /** Front wide right output channel (see FWR in channel diagram) */ + public static final int CHANNEL_OUT_FRONT_WIDE_RIGHT = 0x8000000; public static final int CHANNEL_OUT_MONO = CHANNEL_OUT_FRONT_LEFT; public static final int CHANNEL_OUT_STEREO = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT); @@ -491,6 +532,7 @@ public final class AudioFormat implements Parcelable { public static final int CHANNEL_OUT_SURROUND = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT | CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_BACK_CENTER); // aka 5POINT1_BACK + /** Output channel mask for 5.1 */ public static final int CHANNEL_OUT_5POINT1 = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT | CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_LOW_FREQUENCY | CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT); /** @hide */ @@ -502,26 +544,39 @@ public final class AudioFormat implements Parcelable { @Deprecated public static final int CHANNEL_OUT_7POINT1 = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT | CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_LOW_FREQUENCY | CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT | CHANNEL_OUT_FRONT_LEFT_OF_CENTER | CHANNEL_OUT_FRONT_RIGHT_OF_CENTER); + /** Output channel mask for 7.1 */ // matches AUDIO_CHANNEL_OUT_7POINT1 public static final int CHANNEL_OUT_7POINT1_SURROUND = ( CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_FRONT_RIGHT | CHANNEL_OUT_SIDE_LEFT | CHANNEL_OUT_SIDE_RIGHT | CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT | CHANNEL_OUT_LOW_FREQUENCY); - /** @hide */ + /** Output channel mask for 5.1.2 + * Same as 5.1 with the addition of left and right top channels */ public static final int CHANNEL_OUT_5POINT1POINT2 = (CHANNEL_OUT_5POINT1 | CHANNEL_OUT_TOP_SIDE_LEFT | CHANNEL_OUT_TOP_SIDE_RIGHT); - /** @hide */ + /** Output channel mask for 5.1.4 + * Same as 5.1 with the addition of four top channels */ public static final int CHANNEL_OUT_5POINT1POINT4 = (CHANNEL_OUT_5POINT1 | CHANNEL_OUT_TOP_FRONT_LEFT | CHANNEL_OUT_TOP_FRONT_RIGHT | CHANNEL_OUT_TOP_BACK_LEFT | CHANNEL_OUT_TOP_BACK_RIGHT); - /** @hide */ + /** Output channel mask for 7.1.2 + * Same as 7.1 with the addition of left and right top channels*/ public static final int CHANNEL_OUT_7POINT1POINT2 = (CHANNEL_OUT_7POINT1_SURROUND | CHANNEL_OUT_TOP_SIDE_LEFT | CHANNEL_OUT_TOP_SIDE_RIGHT); - /** @hide */ + /** Output channel mask for 7.1.4 + * Same as 7.1 with the addition of four top channels */ public static final int CHANNEL_OUT_7POINT1POINT4 = (CHANNEL_OUT_7POINT1_SURROUND | CHANNEL_OUT_TOP_FRONT_LEFT | CHANNEL_OUT_TOP_FRONT_RIGHT | CHANNEL_OUT_TOP_BACK_LEFT | CHANNEL_OUT_TOP_BACK_RIGHT); + /** Output channel mask for 9.1.4 + * Same as 7.1.4 with the addition of left and right front wide channels */ + public static final int CHANNEL_OUT_9POINT1POINT4 = (CHANNEL_OUT_7POINT1POINT4 + | CHANNEL_OUT_FRONT_WIDE_LEFT | CHANNEL_OUT_FRONT_WIDE_RIGHT); + /** Output channel mask for 9.1.6 + * Same as 9.1.4 with the addition of left and right top side channels */ + public static final int CHANNEL_OUT_9POINT1POINT6 = (CHANNEL_OUT_9POINT1POINT4 + | CHANNEL_OUT_TOP_SIDE_LEFT | CHANNEL_OUT_TOP_SIDE_RIGHT); /** @hide */ public static final int CHANNEL_OUT_13POINT_360RA = ( CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_FRONT_RIGHT | diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 054a7183e67e..bc8ccc333002 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -805,7 +805,7 @@ public class AudioManager { } @UnsupportedAppUsage - private static IAudioService getService() + static IAudioService getService() { if (sService != null) { return sService; @@ -2443,6 +2443,19 @@ public class AudioManager { } //==================================================================== + // Immersive audio + + /** + * Return a handle to the optional platform's {@link Spatializer} + * @return the {@code Spatializer} instance. + * @see Spatializer#getImmersiveAudioLevel() to check for the level of support of the effect + * on the platform + */ + public @NonNull Spatializer getSpatializer() { + return new Spatializer(this); + } + + //==================================================================== // Bluetooth SCO control /** * Sticky broadcast intent action indicating that the Bluetooth SCO audio diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index 8a4b2d56feb0..f1197edd453d 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -18,6 +18,7 @@ package android.media; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.bluetooth.BluetoothCodecConfig; @@ -1341,7 +1342,7 @@ public class AudioSystem return DEVICE_OUT_BLE_SPEAKER_NAME; case DEVICE_OUT_DEFAULT: default: - return Integer.toString(device); + return "0x" + Integer.toHexString(device); } } @@ -2025,6 +2026,46 @@ public class AudioSystem */ public static native int setVibratorInfos(@NonNull List<Vibrator> vibrators); + /** + * @hide + * If a spatializer effect is present on the platform, this will return an + * ISpatializer interface to control this feature. + * If no spatializer is present, a null interface is returned. + * The INativeSpatializerCallback passed must not be null. + * Only one ISpatializer interface can exist at a given time. The native audio policy + * service will reject the request if an interface was already acquired and previous owner + * did not die or call ISpatializer.release(). + * @param callback the callback to receive state updates if the ISpatializer + * interface is acquired. + * @return the ISpatializer interface made available to control the + * platform spatializer + */ + @Nullable + public static ISpatializer getSpatializer(INativeSpatializerCallback callback) { + return ISpatializer.Stub.asInterface(nativeGetSpatializer(callback)); + } + private static native IBinder nativeGetSpatializer(INativeSpatializerCallback callback); + + /** + * @hide + * Queries if some kind of spatialization will be performed if the audio playback context + * described by the provided arguments is present. + * The context is made of: + * - The audio attributes describing the playback use case. + * - The audio configuration describing the audio format, channels, sampling rate ... + * - The devices describing the sink audio device selected for playback. + * All arguments are optional and only the specified arguments are used to match against + * supported criteria. For instance, supplying no argument will tell if spatialization is + * supported or not in general. + * @param attributes audio attributes describing the playback use case + * @param format audio configuration describing the audio format, channels, sampling rate... + * @param devices the sink audio device selected for playback + * @return true if spatialization is enabled for this context, false otherwise. + */ + public static native boolean canBeSpatialized(AudioAttributes attributes, + AudioFormat format, + AudioDeviceAttributes[] devices); + // Items shared with audio service /** diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 23d9532e11a0..476a9a58ef4b 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -1613,7 +1613,9 @@ public class AudioTrack extends PlayerBase AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_LEFT | AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_CENTER | AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_RIGHT | - AudioFormat.CHANNEL_OUT_LOW_FREQUENCY_2; + AudioFormat.CHANNEL_OUT_LOW_FREQUENCY_2 | + AudioFormat.CHANNEL_OUT_FRONT_WIDE_LEFT | + AudioFormat.CHANNEL_OUT_FRONT_WIDE_RIGHT; // Returns a boolean whether the attributes, format, bufferSizeInBytes, mode allow // power saving to be automatically enabled for an AudioTrack. Returns false if @@ -1787,6 +1789,8 @@ public class AudioTrack extends PlayerBase | AudioFormat.CHANNEL_OUT_TOP_SIDE_RIGHT); put("bottom front", AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_LEFT | AudioFormat.CHANNEL_OUT_BOTTOM_FRONT_RIGHT); + put("front wide", AudioFormat.CHANNEL_OUT_FRONT_WIDE_LEFT + | AudioFormat.CHANNEL_OUT_FRONT_WIDE_RIGHT); }}; /** @@ -1801,9 +1805,15 @@ public class AudioTrack extends PlayerBase return false; } final int channelCount = AudioFormat.channelCountFromOutChannelMask(channelConfig); - final int channelCountLimit = AudioFormat.isEncodingLinearFrames(encoding) - ? AudioSystem.OUT_CHANNEL_COUNT_MAX // PCM limited to OUT_CHANNEL_COUNT_MAX - : AudioSystem.FCC_24; // Compressed limited to 24 channels + final int channelCountLimit; + try { + channelCountLimit = AudioFormat.isEncodingLinearFrames(encoding) + ? AudioSystem.OUT_CHANNEL_COUNT_MAX // PCM limited to OUT_CHANNEL_COUNT_MAX + : AudioSystem.FCC_24; // Compressed limited to 24 channels + } catch (IllegalArgumentException iae) { + loge("Unsupported encoding " + iae); + return false; + } if (channelCount > channelCountLimit) { loge("Channel configuration contains too many channels for encoding " + encoding + "(" + channelCount + " > " + channelCountLimit + ")"); diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 268419f8db8f..d06871b41111 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -20,6 +20,7 @@ import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; +import android.media.AudioFormat; import android.media.AudioFocusInfo; import android.media.AudioPlaybackConfiguration; import android.media.AudioRecordingConfiguration; @@ -34,6 +35,10 @@ import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; import android.media.IStrategyPreferredDevicesDispatcher; +import android.media.ISpatializerCallback; +import android.media.ISpatializerHeadTrackingModeCallback; +import android.media.ISpatializerHeadToSoundStagePoseCallback; +import android.media.ISpatializerOutputCallback; import android.media.IVolumeController; import android.media.IVolumeController; import android.media.PlayerBase; @@ -397,4 +402,54 @@ interface IAudioService { void registerModeDispatcher(IAudioModeDispatcher dispatcher); oneway void unregisterModeDispatcher(IAudioModeDispatcher dispatcher); + + int getSpatializerImmersiveAudioLevel(); + + boolean isSpatializerEnabled(); + + boolean isSpatializerAvailable(); + + void setSpatializerEnabled(boolean enabled); + + boolean canBeSpatialized(in AudioAttributes aa, in AudioFormat af); + + void registerSpatializerCallback(in ISpatializerCallback cb); + + void unregisterSpatializerCallback(in ISpatializerCallback cb); + + void registerSpatializerHeadTrackingCallback(in ISpatializerHeadTrackingModeCallback cb); + + void unregisterSpatializerHeadTrackingCallback(in ISpatializerHeadTrackingModeCallback cb); + + void registerHeadToSoundstagePoseCallback(in ISpatializerHeadToSoundStagePoseCallback cb); + + void unregisterHeadToSoundstagePoseCallback(in ISpatializerHeadToSoundStagePoseCallback cb); + + List<AudioDeviceAttributes> getSpatializerCompatibleAudioDevices(); + + void addSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada); + + void removeSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada); + + void setDesiredHeadTrackingMode(int mode); + + int getDesiredHeadTrackingMode(); + + int[] getSupportedHeadTrackingModes(); + + int getActualHeadTrackingMode(); + + oneway void setSpatializerGlobalTransform(in float[] transform); + + oneway void recenterHeadTracker(); + + void setSpatializerParameter(int key, in byte[] value); + + void getSpatializerParameter(int key, inout byte[] value); + + int getSpatializerOutput(); + + void registerSpatializerOutputCallback(in ISpatializerOutputCallback cb); + + void unregisterSpatializerOutputCallback(in ISpatializerOutputCallback cb); } diff --git a/media/java/android/media/IMediaRouterClient.aidl b/media/java/android/media/IMediaRouterClient.aidl index 9b4912373122..6b754e157cfb 100644 --- a/media/java/android/media/IMediaRouterClient.aidl +++ b/media/java/android/media/IMediaRouterClient.aidl @@ -23,4 +23,5 @@ oneway interface IMediaRouterClient { void onStateChanged(); void onRestoreRoute(); void onGroupRouteSelected(String routeId); + void onGlobalA2dpChanged(boolean a2dpOn); } diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 48289ecde9e0..25b582d2fc8d 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -39,6 +39,7 @@ interface IMediaRouterService { MediaRouterClientState getState(IMediaRouterClient client); boolean isPlaybackActive(IMediaRouterClient client); + void setBluetoothA2dpOn(IMediaRouterClient client, boolean on); void setDiscoveryRequest(IMediaRouterClient client, int routeTypes, boolean activeScan); void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit); void requestSetVolume(IMediaRouterClient client, String routeId, int volume); diff --git a/media/java/android/media/ISpatializerCallback.aidl b/media/java/android/media/ISpatializerCallback.aidl new file mode 100644 index 000000000000..50f91e737fc5 --- /dev/null +++ b/media/java/android/media/ISpatializerCallback.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 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.media; + +/** + * AIDL for the AudioService to signal Spatializer state changes. + * + * {@hide} + */ +oneway interface ISpatializerCallback { + + void dispatchSpatializerEnabledChanged(boolean enabled); + + void dispatchSpatializerAvailableChanged(boolean available); +} diff --git a/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl b/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl new file mode 100644 index 000000000000..01a146599394 --- /dev/null +++ b/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 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.media; + +/** + * AIDL for the AudioService to signal Spatializer state changes. + * + * {@hide} + */ +oneway interface ISpatializerHeadToSoundStagePoseCallback { + + /** + * The pose is sent as an array of 6 float values, the first 3 are the translation vector, the + * other 3 are the rotation vector. + */ + void dispatchPoseChanged(in float[] pose); +} diff --git a/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl b/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl new file mode 100644 index 000000000000..c61f86e4c60e --- /dev/null +++ b/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 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.media; + +/** + * AIDL for the AudioService to signal Spatializer head tracking mode changes. + * + * {@hide} + */ +oneway interface ISpatializerHeadTrackingModeCallback { + + void dispatchSpatializerActualHeadTrackingModeChanged(int mode); + + void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode); +} diff --git a/media/java/android/media/ISpatializerOutputCallback.aidl b/media/java/android/media/ISpatializerOutputCallback.aidl new file mode 100644 index 000000000000..57572a81a366 --- /dev/null +++ b/media/java/android/media/ISpatializerOutputCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 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.media; + +/** + * AIDL for the AudioService to signal Spatializer output changes. + * + * {@hide} + */ +oneway interface ISpatializerOutputCallback { + + void dispatchSpatializerOutputChanged(int output); +} diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index 9bf0db52f66d..0847be34d90f 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -683,6 +683,19 @@ public final class MediaFormat { public static final String KEY_CHANNEL_MASK = "channel-mask"; /** + * A key describing the maximum number of channels that can be output by an audio decoder. + * By default, the decoder will output the same number of channels as present in the encoded + * stream, if supported. Set this value to limit the number of output channels, and use + * the downmix information in the stream, if available. + * <p>Values larger than the number of channels in the content to decode behave like the number + * of channels in the content (if applicable), for instance passing 99 for a 5.1 audio stream + * behaves like passing 6. + * <p>This key is only used during decoding. + */ + public static final String KEY_MAX_OUTPUT_CHANNEL_COUNT = + "max-output-channel-count"; + + /** * A key describing the number of frames to trim from the start of the decoded audio stream. * The associated value is an integer. */ diff --git a/media/java/android/media/MediaMetrics.java b/media/java/android/media/MediaMetrics.java index 3a5216e1c4e7..6eb1af8ac980 100644 --- a/media/java/android/media/MediaMetrics.java +++ b/media/java/android/media/MediaMetrics.java @@ -53,6 +53,7 @@ public class MediaMetrics { public static final String AUDIO_VOLUME = AUDIO + SEPARATOR + "volume"; public static final String AUDIO_VOLUME_EVENT = AUDIO_VOLUME + SEPARATOR + "event"; public static final String AUDIO_MODE = AUDIO + SEPARATOR + "mode"; + public static final String METRICS_MANAGER = "metrics" + SEPARATOR + "manager"; } /** @@ -120,10 +121,11 @@ public class MediaMetrics { createKey("gainDb", Double.class); public static final Key<String> GROUP = createKey("group", String.class); - // For volume - public static final Key<Integer> INDEX = createKey("index", Integer.class); - public static final Key<Integer> MAX_INDEX = createKey("maxIndex", Integer.class); - public static final Key<Integer> MIN_INDEX = createKey("minIndex", Integer.class); + + public static final Key<Integer> INDEX = createKey("index", Integer.class); // volume + public static final Key<String> LOG_SESSION_ID = createKey("logSessionId", String.class); + public static final Key<Integer> MAX_INDEX = createKey("maxIndex", Integer.class); // vol + public static final Key<Integer> MIN_INDEX = createKey("minIndex", Integer.class); // vol public static final Key<String> MODE = createKey("mode", String.class); // audio_mode public static final Key<String> MUTE = diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java index 2986f7c75f4d..748ae52d6c0c 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -654,12 +654,9 @@ public class MediaRouter { final class Client extends IMediaRouterClient.Stub { @Override public void onStateChanged() { - mHandler.post(new Runnable() { - @Override - public void run() { - if (Client.this == mClient) { - updateClientState(); - } + mHandler.post(() -> { + if (Client.this == mClient) { + updateClientState(); } }); } @@ -693,6 +690,26 @@ public class MediaRouter { } }); } + + // Called when the selection of a connected device (phone speaker or BT devices) + // is changed. + @Override + public void onGlobalA2dpChanged(boolean a2dpOn) { + mHandler.post(() -> { + if (mSelectedRoute == null || mBluetoothA2dpRoute == null) { + return; + } + if (mSelectedRoute.isDefault() && a2dpOn) { + setSelectedRoute(mBluetoothA2dpRoute, /*explicit=*/ false); + dispatchRouteUnselected(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo); + dispatchRouteSelected(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute); + } else if (mSelectedRoute.isBluetooth() && !a2dpOn) { + setSelectedRoute(mDefaultAudioVideo, /*explicit=*/ false); + dispatchRouteUnselected(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute); + dispatchRouteSelected(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo); + } + }); + } } } @@ -1070,7 +1087,8 @@ public class MediaRouter { && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 && (route.isBluetooth() || route.isDefault())) { try { - sStatic.mAudioService.setBluetoothA2dpOn(route.isBluetooth()); + sStatic.mMediaRouterService.setBluetoothA2dpOn(sStatic.mClient, + route.isBluetooth()); } catch (RemoteException e) { Log.e(TAG, "Error changing Bluetooth A2DP state", e); } @@ -1350,6 +1368,9 @@ public class MediaRouter { } static void dispatchRouteSelected(int type, RouteInfo info) { + if (DEBUG) { + Log.d(TAG, "Dispatching route selected: " + info); + } for (CallbackInfo cbi : sStatic.mCallbacks) { if (cbi.filterRouteEvent(info)) { cbi.cb.onRouteSelected(cbi.router, type, info); @@ -1358,6 +1379,9 @@ public class MediaRouter { } static void dispatchRouteUnselected(int type, RouteInfo info) { + if (DEBUG) { + Log.d(TAG, "Dispatching route unselected: " + info); + } for (CallbackInfo cbi : sStatic.mCallbacks) { if (cbi.filterRouteEvent(info)) { cbi.cb.onRouteUnselected(cbi.router, type, info); diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java new file mode 100644 index 000000000000..e6fff392c264 --- /dev/null +++ b/media/java/android/media/Spatializer.java @@ -0,0 +1,1089 @@ +/* + * Copyright (C) 2021 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.media; + +import android.annotation.CallbackExecutor; +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.media.permission.ClearCallingIdentityContext; +import android.media.permission.SafeCloseable; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * Spatializer provides access to querying capabilities and behavior of sound spatialization + * on the device. + * Sound spatialization simulates sounds originating around the listener as if they were coming + * from virtual speakers placed around the listener.<br> + * Support for spatialization is optional, use {@link AudioManager#getSpatializer()} to obtain an + * instance of this class if the feature is supported. + * + */ +public class Spatializer { + + private final @NonNull AudioManager mAm; + + private static final String TAG = "Spatializer"; + + /** + * @hide + * Constructor with AudioManager acting as proxy to AudioService + * @param am a non-null AudioManager + */ + protected Spatializer(@NonNull AudioManager am) { + mAm = Objects.requireNonNull(am); + } + + /** + * Returns whether spatialization is enabled or not. + * A false value can originate for instance from the user electing to + * disable the feature, or when the feature is not supported on the device (indicated + * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}). + * <br> + * Note that this state reflects a platform-wide state of the "desire" to use spatialization, + * but availability of the audio processing is still dictated by the compatibility between + * the effect and the hardware configuration, as indicated by {@link #isAvailable()}. + * @return {@code true} if spatialization is enabled + * @see #isAvailable() + */ + public boolean isEnabled() { + try { + return mAm.getService().isSpatializerEnabled(); + } catch (RemoteException e) { + Log.e(TAG, "Error querying isSpatializerEnabled, returning false", e); + return false; + } + } + + /** + * Returns whether spatialization is available. + * Reasons for spatialization being unavailable include situations where audio output is + * incompatible with sound spatialization, such as playback on a monophonic speaker.<br> + * Note that spatialization can be available, but disabled by the user, in which case this + * method would still return {@code true}, whereas {@link #isEnabled()} + * would return {@code false}.<br> + * Also when the feature is not supported on the device (indicated + * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}), + * the return value will be false. + * @return {@code true} if the spatializer effect is available and capable + * of processing the audio for the current configuration of the device, + * {@code false} otherwise. + * @see #isEnabled() + */ + public boolean isAvailable() { + try { + return mAm.getService().isSpatializerAvailable(); + } catch (RemoteException e) { + Log.e(TAG, "Error querying isSpatializerAvailable, returning false", e); + return false; + } + } + + /** @hide */ + @IntDef(flag = false, value = { + SPATIALIZER_IMMERSIVE_LEVEL_OTHER, + SPATIALIZER_IMMERSIVE_LEVEL_NONE, + SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ImmersiveAudioLevel {}; + + /** + * Constant indicating the {@code Spatializer} on this device supports a spatialization + * mode that differs from the ones available at this SDK level. + * @see #getImmersiveAudioLevel() + */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1; + + /** + * Constant indicating there are no spatialization capabilities supported on this device. + * @see #getImmersiveAudioLevel() + */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0; + + /** + * Constant indicating the {@code Spatializer} on this device supports multichannel + * spatialization. + * @see #getImmersiveAudioLevel() + */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1; + + /** + * @hide + * Constant indicating the {@code Spatializer} on this device supports the spatialization of + * multichannel bed plus objects. + * @see #getImmersiveAudioLevel() + */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS = 2; + + /** @hide */ + @IntDef(flag = false, value = { + HEAD_TRACKING_MODE_UNSUPPORTED, + HEAD_TRACKING_MODE_DISABLED, + HEAD_TRACKING_MODE_RELATIVE_WORLD, + HEAD_TRACKING_MODE_RELATIVE_DEVICE, + }) public @interface HeadTrackingMode {}; + + /** @hide */ + @IntDef(flag = false, value = { + HEAD_TRACKING_MODE_DISABLED, + HEAD_TRACKING_MODE_RELATIVE_WORLD, + HEAD_TRACKING_MODE_RELATIVE_DEVICE, + }) public @interface HeadTrackingModeSet {}; + + /** @hide */ + @IntDef(flag = false, value = { + HEAD_TRACKING_MODE_RELATIVE_WORLD, + HEAD_TRACKING_MODE_RELATIVE_DEVICE, + }) public @interface HeadTrackingModeSupported {}; + + /** + * @hide + * Constant indicating head tracking is not supported by this {@code Spatializer} + * @see #getHeadTrackingMode() + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public static final int HEAD_TRACKING_MODE_UNSUPPORTED = -2; + + /** + * @hide + * Constant indicating head tracking is disabled on this {@code Spatializer} + * @see #getHeadTrackingMode() + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public static final int HEAD_TRACKING_MODE_DISABLED = -1; + + /** + * @hide + * Constant indicating head tracking is in a mode whose behavior is unknown. This is not an + * error state but represents a customized behavior not defined by this API. + * @see #getHeadTrackingMode() + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public static final int HEAD_TRACKING_MODE_OTHER = 0; + + /** + * @hide + * Constant indicating head tracking is tracking the user's position / orientation relative to + * the world around them + * @see #getHeadTrackingMode() + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public static final int HEAD_TRACKING_MODE_RELATIVE_WORLD = 1; + + /** + * @hide + * Constant indicating head tracking is tracking the user's position / orientation relative to + * the device + * @see #getHeadTrackingMode() + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2; + + /** + * Return the level of support for the spatialization feature on this device. + * This level of support is independent of whether the {@code Spatializer} is currently + * enabled or available and will not change over time. + * @return the level of spatialization support + * @see #isEnabled() + * @see #isAvailable() + */ + public @ImmersiveAudioLevel int getImmersiveAudioLevel() { + int level = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + try { + level = mAm.getService().getSpatializerImmersiveAudioLevel(); + } catch (Exception e) { /* using NONE */ } + return level; + } + + /** + * @hide + * Enables / disables the spatializer effect. + * Changing the enabled state will trigger the public + * {@link OnSpatializerStateChangedListener#onSpatializerEnabledChanged(Spatializer, boolean)} + * registered listeners. + * @param enabled {@code true} for enabling the effect + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void setEnabled(boolean enabled) { + try { + mAm.getService().setSpatializerEnabled(enabled); + } catch (RemoteException e) { + Log.e(TAG, "Error calling setSpatializerEnabled", e); + } + } + + /** + * An interface to be notified of changes to the state of the spatializer effect. + */ + public interface OnSpatializerStateChangedListener { + /** + * Called when the enabled state of the spatializer effect changes + * @param spat the {@code Spatializer} instance whose state changed + * @param enabled {@code true} if the spatializer effect is enabled on the device, + * {@code false} otherwise + * @see #isEnabled() + */ + void onSpatializerEnabledChanged(@NonNull Spatializer spat, boolean enabled); + + /** + * Called when the availability of the spatializer effect changes + * @param spat the {@code Spatializer} instance whose state changed + * @param available {@code true} if the spatializer effect is available and capable + * of processing the audio for the current configuration of the device, + * {@code false} otherwise. + * @see #isAvailable() + */ + void onSpatializerAvailableChanged(@NonNull Spatializer spat, boolean available); + } + + /** + * @hide + * An interface to be notified of changes to the head tracking mode, used by the spatializer + * effect. + * Changes to the mode may come from explicitly setting a different mode + * (see {@link #setDesiredHeadTrackingMode(int)}) or a change in system conditions (see + * {@link #getHeadTrackingMode()} + * @see #addOnHeadTrackingModeChangedListener(Executor, OnHeadTrackingModeChangedListener) + * @see #removeOnHeadTrackingModeChangedListener(OnHeadTrackingModeChangedListener) + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + public interface OnHeadTrackingModeChangedListener { + /** + * Called when the actual head tracking mode of the spatializer changed. + * @param spatializer the {@code Spatializer} instance whose head tracking mode is changing + * @param mode the new head tracking mode + */ + void onHeadTrackingModeChanged(@NonNull Spatializer spatializer, + @HeadTrackingMode int mode); + + /** + * Called when the desired head tracking mode of the spatializer changed + * @param spatializer the {@code Spatializer} instance whose head tracking mode was set + * @param mode the newly set head tracking mode + */ + void onDesiredHeadTrackingModeChanged(@NonNull Spatializer spatializer, + @HeadTrackingModeSet int mode); + } + + + /** + * @hide + * An interface to be notified of changes to the output stream used by the spatializer + * effect. + * @see #getOutput() + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + public interface OnSpatializerOutputChangedListener { + /** + * Called when the id of the output stream of the spatializer effect changed. + * @param spatializer the {@code Spatializer} instance whose output is updated + * @param output the id of the output stream, or 0 when there is no spatializer output + */ + void onSpatializerOutputChanged(@NonNull Spatializer spatializer, + @IntRange(from = 0) int output); + } + + /** + * @hide + * An interface to be notified of updates to the head to soundstage pose, as represented by the + * current head tracking mode. + * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener) + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + public interface OnHeadToSoundstagePoseUpdatedListener { + /** + * Called when the head to soundstage transform is updated + * @param spatializer the {@code Spatializer} instance affected by the pose update + * @param pose the new pose data representing the transform between the frame + * of reference for the current head tracking mode (see + * {@link #getHeadTrackingMode()}) and the device being tracked (for + * instance a pair of headphones with a head tracker).<br> + * The head pose data is represented as an array of six float values, where + * the first three values are the translation vector, and the next three + * are the rotation vector. + */ + void onHeadToSoundstagePoseUpdated(@NonNull Spatializer spatializer, + @NonNull float[] pose); + } + + /** + * Returns whether audio of the given {@link AudioFormat}, played with the given + * {@link AudioAttributes} can be spatialized. + * Note that the result reflects the capabilities of the device and may change when + * audio accessories are connected/disconnected (e.g. wired headphones plugged in or not). + * The result is independent from whether spatialization processing is enabled or not. + * @param attributes the {@code AudioAttributes} of the content as used for playback + * @param format the {@code AudioFormat} of the content as used for playback + * @return {@code true} if the device is capable of spatializing the combination of audio format + * and attributes, {@code false} otherwise. + */ + public boolean canBeSpatialized( + @NonNull AudioAttributes attributes, @NonNull AudioFormat format) { + try { + return mAm.getService().canBeSpatialized( + Objects.requireNonNull(attributes), Objects.requireNonNull(format)); + } catch (RemoteException e) { + Log.e(TAG, "Error querying canBeSpatialized for attr:" + attributes + + " format:" + format + " returning false", e); + return false; + } + } + + /** + * Adds a listener to be notified of changes to the enabled state of the + * {@code Spatializer}. + * @param executor the {@code Executor} handling the callback + * @param listener the listener to receive enabled state updates + * @see #isEnabled() + */ + public void addOnSpatializerStateChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnSpatializerStateChangedListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mStateListenerLock) { + if (hasSpatializerStateListener(listener)) { + throw new IllegalArgumentException( + "Called addOnSpatializerStateChangedListener() " + + "on a previously registered listener"); + } + // lazy initialization of the list of strategy-preferred device listener + if (mStateListeners == null) { + mStateListeners = new ArrayList<>(); + } + mStateListeners.add(new StateListenerInfo(listener, executor)); + if (mStateListeners.size() == 1) { + // register binder for callbacks + if (mInfoDispatcherStub == null) { + mInfoDispatcherStub = + new SpatializerInfoDispatcherStub(); + } + try { + mAm.getService().registerSpatializerCallback( + mInfoDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + + /** + * Removes a previously added listener for changes to the enabled state of the + * {@code Spatializer}. + * @param listener the listener to receive enabled state updates + * @see #isEnabled() + */ + public void removeOnSpatializerStateChangedListener( + @NonNull OnSpatializerStateChangedListener listener) { + Objects.requireNonNull(listener); + synchronized (mStateListenerLock) { + if (!removeStateListener(listener)) { + throw new IllegalArgumentException( + "Called removeOnSpatializerStateChangedListener() " + + "on an unregistered listener"); + } + if (mStateListeners.size() == 0) { + // unregister binder for callbacks + try { + mAm.getService().unregisterSpatializerCallback(mInfoDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + mInfoDispatcherStub = null; + mStateListeners = null; + } + } + } + } + + /** + * @hide + * Returns the list of playback devices that are compatible with the playback of multichannel + * audio through virtualization + * @return a list of devices. An empty list indicates virtualization is not supported. + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() { + try { + return mAm.getService().getSpatializerCompatibleAudioDevices(); + } catch (RemoteException e) { + Log.e(TAG, "Error querying getSpatializerCompatibleAudioDevices(), " + + " returning empty list", e); + return new ArrayList<AudioDeviceAttributes>(0); + } + } + + /** + * @hide + * Adds a playback device to the list of devices compatible with the playback of multichannel + * audio through spatialization. + * @see #getCompatibleAudioDevices() + * @param ada the audio device compatible with spatialization + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + try { + mAm.getService().addSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada)); + } catch (RemoteException e) { + Log.e(TAG, "Error calling addSpatializerCompatibleAudioDevice(), ", e); + } + } + + /** + * @hide + * Remove a playback device from the list of devices compatible with the playback of + * multichannel audio through spatialization. + * @see #getCompatibleAudioDevices() + * @param ada the audio device incompatible with spatialization + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + try { + mAm.getService().removeSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada)); + } catch (RemoteException e) { + Log.e(TAG, "Error calling removeSpatializerCompatibleAudioDevice(), ", e); + } + } + + private final Object mStateListenerLock = new Object(); + /** + * List of listeners for state listener and their associated Executor. + * List is lazy-initialized on first registration + */ + @GuardedBy("mStateListenerLock") + private @Nullable ArrayList<StateListenerInfo> mStateListeners; + + @GuardedBy("mStateListenerLock") + private @Nullable SpatializerInfoDispatcherStub mInfoDispatcherStub; + + private final class SpatializerInfoDispatcherStub extends ISpatializerCallback.Stub { + @Override + public void dispatchSpatializerEnabledChanged(boolean enabled) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList<StateListenerInfo> stateListeners; + synchronized (mStateListenerLock) { + if (mStateListeners == null || mStateListeners.size() == 0) { + return; + } + stateListeners = (ArrayList<StateListenerInfo>) mStateListeners.clone(); + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + for (StateListenerInfo info : stateListeners) { + info.mExecutor.execute(() -> + info.mListener.onSpatializerEnabledChanged(Spatializer.this, enabled)); + } + } + } + + @Override + public void dispatchSpatializerAvailableChanged(boolean available) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList<StateListenerInfo> stateListeners; + synchronized (mStateListenerLock) { + if (mStateListeners == null || mStateListeners.size() == 0) { + return; + } + stateListeners = (ArrayList<StateListenerInfo>) mStateListeners.clone(); + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + for (StateListenerInfo info : stateListeners) { + info.mExecutor.execute(() -> + info.mListener.onSpatializerAvailableChanged( + Spatializer.this, available)); + } + } + } + } + + private static class StateListenerInfo { + final @NonNull OnSpatializerStateChangedListener mListener; + final @NonNull Executor mExecutor; + + StateListenerInfo(@NonNull OnSpatializerStateChangedListener listener, + @NonNull Executor exe) { + mListener = listener; + mExecutor = exe; + } + } + + @GuardedBy("mStateListenerLock") + private boolean hasSpatializerStateListener(OnSpatializerStateChangedListener listener) { + return getStateListenerInfo(listener) != null; + } + + @GuardedBy("mStateListenerLock") + private @Nullable StateListenerInfo getStateListenerInfo( + OnSpatializerStateChangedListener listener) { + if (mStateListeners == null) { + return null; + } + for (StateListenerInfo info : mStateListeners) { + if (info.mListener == listener) { + return info; + } + } + return null; + } + + @GuardedBy("mStateListenerLock") + /** + * @return true if the listener was removed from the list + */ + private boolean removeStateListener(OnSpatializerStateChangedListener listener) { + final StateListenerInfo infoToRemove = getStateListenerInfo(listener); + if (infoToRemove != null) { + mStateListeners.remove(infoToRemove); + return true; + } + return false; + } + + + /** + * @hide + * Return the current head tracking mode as used by the system. + * Note this may differ from the desired head tracking mode. Reasons for the two to differ + * include: a head tracking device is not available for the current audio output device, + * the transmission conditions between the tracker and device have deteriorated and tracking + * has been disabled. + * @see #getDesiredHeadTrackingMode() + * @return the current head tracking mode + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public @HeadTrackingMode int getHeadTrackingMode() { + try { + return mAm.getService().getActualHeadTrackingMode(); + } catch (RemoteException e) { + Log.e(TAG, "Error calling getActualHeadTrackingMode", e); + return HEAD_TRACKING_MODE_UNSUPPORTED; + } + + } + + /** + * @hide + * Return the desired head tracking mode. + * Note this may differ from the actual head tracking mode, reflected by + * {@link #getHeadTrackingMode()}. + * @return the desired head tring mode + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public @HeadTrackingMode int getDesiredHeadTrackingMode() { + try { + return mAm.getService().getDesiredHeadTrackingMode(); + } catch (RemoteException e) { + Log.e(TAG, "Error calling getDesiredHeadTrackingMode", e); + return HEAD_TRACKING_MODE_UNSUPPORTED; + } + } + + /** + * @hide + * Returns the list of supported head tracking modes. + * @return the list of modes that can be used in {@link #setDesiredHeadTrackingMode(int)} to + * enable head tracking. The list will be empty if {@link #getHeadTrackingMode()} + * is {@link #HEAD_TRACKING_MODE_UNSUPPORTED}. Values can be + * {@link #HEAD_TRACKING_MODE_OTHER}, + * {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD} or + * {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE} + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public @NonNull List<Integer> getSupportedHeadTrackingModes() { + try { + final int[] modes = mAm.getService().getSupportedHeadTrackingModes(); + final ArrayList<Integer> list = new ArrayList<>(0); + for (int mode : modes) { + list.add(mode); + } + return list; + } catch (RemoteException e) { + Log.e(TAG, "Error calling getSupportedHeadTrackModes", e); + return new ArrayList(0); + } + } + + /** + * @hide + * Sets the desired head tracking mode. + * Note a set desired mode may differ from the actual head tracking mode. + * @see #getHeadTrackingMode() + * @param mode the desired head tracking mode, one of the values returned by + * {@link #getSupportedHeadTrackModes()}, or {@link #HEAD_TRACKING_MODE_DISABLED} to + * disable head tracking. + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void setDesiredHeadTrackingMode(@HeadTrackingModeSet int mode) { + try { + mAm.getService().setDesiredHeadTrackingMode(mode); + } catch (RemoteException e) { + Log.e(TAG, "Error calling setDesiredHeadTrackingMode to " + mode, e); + } + } + + /** + * @hide + * Recenters the head tracking at the current position / orientation. + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void recenterHeadTracker() { + try { + mAm.getService().recenterHeadTracker(); + } catch (RemoteException e) { + Log.e(TAG, "Error calling recenterHeadTracker", e); + } + } + + /** + * @hide + * Adds a listener to be notified of changes to the head tracking mode of the + * {@code Spatializer} + * @param executor the {@code Executor} handling the callbacks + * @param listener the listener to register + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void addOnHeadTrackingModeChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnHeadTrackingModeChangedListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mHeadTrackingListenerLock) { + if (hasListener(listener, mHeadTrackingListeners)) { + throw new IllegalArgumentException( + "Called addOnHeadTrackingModeChangedListener() " + + "on a previously registered listener"); + } + // lazy initialization of the list of strategy-preferred device listener + if (mHeadTrackingListeners == null) { + mHeadTrackingListeners = new ArrayList<>(); + } + mHeadTrackingListeners.add( + new ListenerInfo<OnHeadTrackingModeChangedListener>(listener, executor)); + if (mHeadTrackingListeners.size() == 1) { + // register binder for callbacks + if (mHeadTrackingDispatcherStub == null) { + mHeadTrackingDispatcherStub = + new SpatializerHeadTrackingDispatcherStub(); + } + try { + mAm.getService().registerSpatializerHeadTrackingCallback( + mHeadTrackingDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + + /** + * @hide + * Removes a previously added listener for changes to the head tracking mode of the + * {@code Spatializer}. + * @param listener the listener to unregister + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void removeOnHeadTrackingModeChangedListener( + @NonNull OnHeadTrackingModeChangedListener listener) { + Objects.requireNonNull(listener); + synchronized (mHeadTrackingListenerLock) { + if (!removeListener(listener, mHeadTrackingListeners)) { + throw new IllegalArgumentException( + "Called removeOnHeadTrackingModeChangedListener() " + + "on an unregistered listener"); + } + if (mHeadTrackingListeners.size() == 0) { + // unregister binder for callbacks + try { + mAm.getService().unregisterSpatializerHeadTrackingCallback( + mHeadTrackingDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + mHeadTrackingDispatcherStub = null; + mHeadTrackingListeners = null; + } + } + } + } + + /** + * @hide + * Set the listener to receive head to soundstage pose updates. + * @param executor the {@code Executor} handling the callbacks + * @param listener the listener to register + * @see #clearOnHeadToSoundstagePoseUpdatedListener() + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void setOnHeadToSoundstagePoseUpdatedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnHeadToSoundstagePoseUpdatedListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mPoseListenerLock) { + if (mPoseListener != null) { + throw new IllegalStateException("Trying to overwrite existing listener"); + } + mPoseListener = + new ListenerInfo<OnHeadToSoundstagePoseUpdatedListener>(listener, executor); + mPoseDispatcher = new SpatializerPoseDispatcherStub(); + try { + mAm.getService().registerHeadToSoundstagePoseCallback(mPoseDispatcher); + } catch (RemoteException e) { + mPoseListener = null; + mPoseDispatcher = null; + } + } + } + + /** + * @hide + * Clears the listener for head to soundstage pose updates + * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener) + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void clearOnHeadToSoundstagePoseUpdatedListener() { + synchronized (mPoseListenerLock) { + if (mPoseDispatcher == null) { + throw (new IllegalStateException("No listener to clear")); + } + try { + mAm.getService().unregisterHeadToSoundstagePoseCallback(mPoseDispatcher); + } catch (RemoteException e) { } + mPoseListener = null; + mPoseDispatcher = null; + } + } + + /** + * @hide + * Sets an additional transform over the soundstage. + * The transform represents the pose of the soundstage, relative + * to either the device (in {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE} mode), the world (in + * {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD}) or the listener’s head (in + * {@link #HEAD_TRACKING_MODE_DISABLED} mode). + * @param transform an array of 6 float values, the first 3 are the translation vector, the + * other 3 are the rotation vector. + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void setGlobalTransform(@NonNull float[] transform) { + if (Objects.requireNonNull(transform).length != 6) { + throw new IllegalArgumentException("transform array must be of size 6, was " + + transform.length); + } + try { + mAm.getService().setSpatializerGlobalTransform(transform); + } catch (RemoteException e) { + Log.e(TAG, "Error calling setGlobalTransform", e); + } + } + + /** + * @hide + * Sets a parameter on the platform spatializer effect implementation. + * This is to be used for vendor-specific configurations of their effect, keys and values are + * not reuseable across implementations. + * @param key the parameter to change + * @param value an array for the value of the parameter to change + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void setEffectParameter(int key, @NonNull byte[] value) { + Objects.requireNonNull(value); + try { + mAm.getService().setSpatializerParameter(key, value); + } catch (RemoteException e) { + Log.e(TAG, "Error calling setEffectParameter", e); + } + } + + /** + * @hide + * Retrieves a parameter value from the platform spatializer effect implementation. + * This is to be used for vendor-specific configurations of their effect, keys and values are + * not reuseable across implementations. + * @param key the parameter for which the value is queried + * @param value a non-empty array to contain the return value. The caller is responsible for + * passing an array of size matching the parameter. + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void getEffectParameter(int key, @NonNull byte[] value) { + Objects.requireNonNull(value); + try { + mAm.getService().getSpatializerParameter(key, value); + } catch (RemoteException e) { + Log.e(TAG, "Error calling getEffectParameter", e); + } + } + + /** + * @hide + * Returns the id of the output stream used for the spatializer effect playback + * @return id of the output stream, or 0 if no spatializer playback is active + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public @IntRange(from = 0) int getOutput() { + try { + return mAm.getService().getSpatializerOutput(); + } catch (RemoteException e) { + Log.e(TAG, "Error calling getSpatializerOutput", e); + return 0; + } + } + + /** + * @hide + * Sets the listener to receive spatializer effect output updates + * @param executor the {@code Executor} handling the callbacks + * @param listener the listener to register + * @see #clearOnSpatializerOutputChangedListener() + * @see #getOutput() + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void setOnSpatializerOutputChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnSpatializerOutputChangedListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mOutputListenerLock) { + if (mOutputListener != null) { + throw new IllegalStateException("Trying to overwrite existing listener"); + } + mOutputListener = + new ListenerInfo<OnSpatializerOutputChangedListener>(listener, executor); + mOutputDispatcher = new SpatializerOutputDispatcherStub(); + try { + mAm.getService().registerSpatializerOutputCallback(mOutputDispatcher); + } catch (RemoteException e) { + mOutputListener = null; + mOutputDispatcher = null; + } + } + } + + /** + * @hide + * Clears the listener for spatializer effect output updates + * @see #setOnSpatializerOutputChangedListener(Executor, OnSpatializerOutputChangedListener) + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void clearOnSpatializerOutputChangedListener() { + synchronized (mOutputListenerLock) { + if (mOutputDispatcher == null) { + throw (new IllegalStateException("No listener to clear")); + } + try { + mAm.getService().unregisterSpatializerOutputCallback(mOutputDispatcher); + } catch (RemoteException e) { } + mOutputListener = null; + mOutputDispatcher = null; + } + } + + //----------------------------------------------------------------------------- + // callback helper definitions + + private static class ListenerInfo<T> { + final @NonNull T mListener; + final @NonNull Executor mExecutor; + + ListenerInfo(T listener, Executor exe) { + mListener = listener; + mExecutor = exe; + } + } + + private static <T> ListenerInfo<T> getListenerInfo( + T listener, ArrayList<ListenerInfo<T>> listeners) { + if (listeners == null) { + return null; + } + for (ListenerInfo<T> info : listeners) { + if (info.mListener == listener) { + return info; + } + } + return null; + } + + private static <T> boolean hasListener(T listener, ArrayList<ListenerInfo<T>> listeners) { + return getListenerInfo(listener, listeners) != null; + } + + private static <T> boolean removeListener(T listener, ArrayList<ListenerInfo<T>> listeners) { + final ListenerInfo<T> infoToRemove = getListenerInfo(listener, listeners); + if (infoToRemove != null) { + listeners.remove(infoToRemove); + return true; + } + return false; + } + + //----------------------------------------------------------------------------- + // head tracking callback management and stub + + private final Object mHeadTrackingListenerLock = new Object(); + /** + * List of listeners for head tracking mode listener and their associated Executor. + * List is lazy-initialized on first registration + */ + @GuardedBy("mHeadTrackingListenerLock") + private @Nullable ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> + mHeadTrackingListeners; + + @GuardedBy("mHeadTrackingListenerLock") + private @Nullable SpatializerHeadTrackingDispatcherStub mHeadTrackingDispatcherStub; + + private final class SpatializerHeadTrackingDispatcherStub + extends ISpatializerHeadTrackingModeCallback.Stub { + @Override + public void dispatchSpatializerActualHeadTrackingModeChanged(int mode) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners; + synchronized (mHeadTrackingListenerLock) { + if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) { + return; + } + headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>) + mHeadTrackingListeners.clone(); + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) { + info.mExecutor.execute(() -> info.mListener + .onHeadTrackingModeChanged(Spatializer.this, mode)); + } + } + } + + @Override + public void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners; + synchronized (mHeadTrackingListenerLock) { + if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) { + return; + } + headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>) + mHeadTrackingListeners.clone(); + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) { + info.mExecutor.execute(() -> info.mListener + .onDesiredHeadTrackingModeChanged(Spatializer.this, mode)); + } + } + } + } + + //----------------------------------------------------------------------------- + // head pose callback management and stub + private final Object mPoseListenerLock = new Object(); + /** + * Listener for head to soundstage updates + */ + @GuardedBy("mPoseListenerLock") + private @Nullable ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> mPoseListener; + @GuardedBy("mPoseListenerLock") + private @Nullable SpatializerPoseDispatcherStub mPoseDispatcher; + + private final class SpatializerPoseDispatcherStub + extends ISpatializerHeadToSoundStagePoseCallback.Stub { + + @Override + public void dispatchPoseChanged(float[] pose) { + // make a copy of ref to listener so callback is not executed under lock + final ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> listener; + synchronized (mPoseListenerLock) { + listener = mPoseListener; + } + if (listener == null) { + return; + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + listener.mExecutor.execute(() -> listener.mListener + .onHeadToSoundstagePoseUpdated(Spatializer.this, pose)); + } + } + } + + //----------------------------------------------------------------------------- + // output callback management and stub + private final Object mOutputListenerLock = new Object(); + /** + * Listener for output updates + */ + @GuardedBy("mOutputListenerLock") + private @Nullable ListenerInfo<OnSpatializerOutputChangedListener> mOutputListener; + @GuardedBy("mOutputListenerLock") + private @Nullable SpatializerOutputDispatcherStub mOutputDispatcher; + + private final class SpatializerOutputDispatcherStub + extends ISpatializerOutputCallback.Stub { + + @Override + public void dispatchSpatializerOutputChanged(int output) { + // make a copy of ref to listener so callback is not executed under lock + final ListenerInfo<OnSpatializerOutputChangedListener> listener; + synchronized (mOutputListenerLock) { + listener = mOutputListener; + } + if (listener == null) { + return; + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + listener.mExecutor.execute(() -> listener.mListener + .onSpatializerOutputChanged(Spatializer.this, output)); + } + } + } +} diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index 37e141537c79..5259c4f07639 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -16,15 +16,16 @@ package android.media.projection; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; -import android.media.projection.IMediaProjection; -import android.media.projection.IMediaProjectionCallback; import android.os.Handler; +import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; @@ -100,19 +101,22 @@ public final class MediaProjection { public VirtualDisplay createVirtualDisplay(@NonNull String name, int width, int height, int dpi, boolean isSecure, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { - DisplayManager dm = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE); int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; if (isSecure) { flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE; } - final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, - height, dpi); + Context windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(), + TYPE_APPLICATION, null /* options */); + final VirtualDisplayConfig.Builder builder = buildMirroredVirtualDisplay(name, width, + height, dpi, windowContext.getWindowContextToken()); builder.setFlags(flags); if (surface != null) { builder.setSurface(surface); } - return dm.createVirtualDisplay(this, builder.build(), callback, handler); + VirtualDisplay virtualDisplay = createVirtualDisplay(builder.build(), callback, handler, + windowContext); + return virtualDisplay; } /** @@ -141,13 +145,35 @@ public final class MediaProjection { public VirtualDisplay createVirtualDisplay(@NonNull String name, int width, int height, int dpi, int flags, @Nullable Surface surface, @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { - final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, - height, dpi); + Context windowContext = mContext.createWindowContext(mContext.getDisplayNoVerify(), + TYPE_APPLICATION, null /* options */); + final VirtualDisplayConfig.Builder builder = buildMirroredVirtualDisplay(name, width, + height, dpi, windowContext.getWindowContextToken()); builder.setFlags(flags); if (surface != null) { builder.setSurface(surface); } - return createVirtualDisplay(builder.build(), callback, handler); + VirtualDisplay virtualDisplay = createVirtualDisplay(builder.build(), callback, handler, + windowContext); + return virtualDisplay; + } + + /** + * Constructs a {@link VirtualDisplayConfig.Builder}, which will mirror the contents of a + * DisplayArea. The DisplayArea to mirror is from the DisplayArea the caller is launched on. + * + * @param name The name of the virtual display, must be non-empty. + * @param width The width of the virtual display in pixels. Must be greater than 0. + * @param height The height of the virtual display in pixels. Must be greater than 0. + * @param dpi The density of the virtual display in dpi. Must be greater than 0. + * @return a config representing a VirtualDisplay + */ + private VirtualDisplayConfig.Builder buildMirroredVirtualDisplay(@NonNull String name, + int width, int height, int dpi, IBinder windowContextToken) { + final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(name, width, + height, dpi); + builder.setWindowTokenClientToMirror(windowContextToken); + return builder; } /** @@ -156,20 +182,22 @@ public final class MediaProjection { * * @param virtualDisplayConfig The arguments for the virtual display configuration. See * {@link VirtualDisplayConfig} for using it. - * @param callback Callback to call when the virtual display's state - * changes, or null if none. - * @param handler The {@link android.os.Handler} on which the callback should be - * invoked, or null if the callback should be invoked on the calling - * thread's main {@link android.os.Looper}. + * @param callback Callback to call when the virtual display's state changes, or null if none. + * @param handler The {@link android.os.Handler} on which the callback should be invoked, or + * null if the callback should be invoked on the calling thread's main + * {@link android.os.Looper}. + * @param windowContext the WindowContext associated with the caller. * * @see android.hardware.display.VirtualDisplay * @hide */ @Nullable public VirtualDisplay createVirtualDisplay(@NonNull VirtualDisplayConfig virtualDisplayConfig, - @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler) { + @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler, + Context windowContext) { DisplayManager dm = mContext.getSystemService(DisplayManager.class); - return dm.createVirtualDisplay(this, virtualDisplayConfig, callback, handler); + return dm.createVirtualDisplay(this, virtualDisplayConfig, callback, handler, + windowContext); } /** diff --git a/media/packages/BluetoothMidiService/AndroidManifest.xml b/media/packages/BluetoothMidiService/AndroidManifest.xml index 03606bac6840..90390116df06 100644 --- a/media/packages/BluetoothMidiService/AndroidManifest.xml +++ b/media/packages/BluetoothMidiService/AndroidManifest.xml @@ -20,7 +20,7 @@ xmlns:tools="http://schemas.android.com/tools" package="com.android.bluetoothmidiservice" > - <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" /> + <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" /> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> diff --git a/media/packages/BluetoothMidiService/AndroidManifestBase.xml b/media/packages/BluetoothMidiService/AndroidManifestBase.xml index bfb05469adb9..5a900c794dd1 100644 --- a/media/packages/BluetoothMidiService/AndroidManifestBase.xml +++ b/media/packages/BluetoothMidiService/AndroidManifestBase.xml @@ -19,7 +19,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.bluetoothmidiservice" > - <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" /> + <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" /> <application android:label="BluetoothMidi" android:defaultToDeviceProtectedStorage="true" |