summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Eric Laurent <elaurent@google.com> 2021-11-09 16:10:42 +0100
committer Eric Laurent <elaurent@google.com> 2021-11-25 17:03:20 +0100
commit78eef3ab9d636383cb46078b1c038b397ccb2665 (patch)
tree93e1166af373fa626688c1ed24871d07de29c75e
parentc86875fb006212c6517b5fcf1ebdf2cb626e3fa6 (diff)
audio: add call audio redirection APIs
Add AudioManager system APIs to acquire call audio injection AudioTrack and extraction AudioRecord. Add AudioTrack and AudioRecord Builder system APIs to create different variants of call redirection AudioTrack and AudioRecord: - If the call is PSTN, only the AudioTrack or AudioRecord is created with audio usage or capture preset corresponding to call audio uplink injection or downlink capture. - If the call is VoIP, a dynamic audio policy is installed for voice communication interception and the AudioTrack or AudioRecord is obtained from the audio policy. Bug: 189472651 Test: make Change-Id: I6d3ed3948c13253ceea4a0fab836a753b5ee9ad0
-rw-r--r--core/api/system-current.txt3
-rw-r--r--core/api/test-current.txt3
-rw-r--r--data/etc/privapp-permissions-platform.xml2
-rw-r--r--media/java/android/media/AudioAttributes.java42
-rw-r--r--media/java/android/media/AudioManager.java279
-rw-r--r--media/java/android/media/AudioRecord.java87
-rw-r--r--media/java/android/media/AudioTrack.java97
-rwxr-xr-xmedia/java/android/media/IAudioService.aidl2
-rw-r--r--media/java/android/media/audiopolicy/AudioMix.java5
-rw-r--r--media/java/android/media/audiopolicy/AudioMixingRule.java17
-rw-r--r--media/java/android/media/audiopolicy/AudioPolicy.java46
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java58
12 files changed, 615 insertions, 26 deletions
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index e289172e7fe7..faf57b1f7646 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -5363,6 +5363,8 @@ package android.media {
method @IntRange(from=0) public long getAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo);
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static java.util.List<android.media.audiopolicy.AudioProductStrategy> getAudioProductStrategies();
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static java.util.List<android.media.audiopolicy.AudioVolumeGroup> getAudioVolumeGroups();
+ method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull android.media.AudioFormat);
+ method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioTrack getCallUplinkInjectionAudioTrack(@NonNull android.media.AudioFormat);
method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public int getDeviceVolumeBehavior(@NonNull android.media.AudioDeviceAttributes);
method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public java.util.List<android.media.AudioDeviceAttributes> getDevicesForAttributes(@NonNull android.media.AudioAttributes);
method @IntRange(from=0) public long getMaxAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo);
@@ -5375,6 +5377,7 @@ package android.media {
method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
method public boolean isAudioServerRunning();
method public boolean isHdmiSystemAudioSupported();
+ method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable();
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int registerAudioPolicy(@NonNull android.media.audiopolicy.AudioPolicy);
method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener);
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index ba0b6aaf1bf2..6b3986584862 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1449,6 +1449,8 @@ package android.media {
public class AudioManager {
method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int abandonAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String);
+ method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull android.media.AudioFormat);
+ method @NonNull @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public android.media.AudioTrack getCallUplinkInjectionAudioTrack(@NonNull android.media.AudioFormat);
method @Nullable public static android.media.AudioDeviceInfo getDeviceInfoFromType(int);
method @IntRange(from=0) @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public long getFadeOutDurationOnFocusLossMillis(@NonNull android.media.AudioAttributes);
method public static final int[] getPublicStreamTypes();
@@ -1457,6 +1459,7 @@ package android.media {
method @NonNull public java.util.Map<java.lang.Integer,java.lang.Boolean> getSurroundFormats();
method public boolean hasRegisteredDynamicPolicy();
method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.QUERY_AUDIO_STATE}) public boolean isFullVolumeDevice();
+ method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable();
method @RequiresPermission("android.permission.QUERY_AUDIO_STATE") public int requestAudioFocusForTest(@NonNull android.media.AudioFocusRequest, @NonNull String, int, int);
}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 103b836ae237..9c4ca1d120f5 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -489,6 +489,8 @@ applications that come with the platform
<!-- Permission required for CTS test - SystemMediaRouter2Test -->
<permission name="android.permission.MEDIA_CONTENT_CONTROL"/>
<permission name="android.permission.MODIFY_AUDIO_ROUTING"/>
+ <!-- Permission required for CTS test - CallAudioInterceptionTest -->
+ <permission name="android.permission.CALL_AUDIO_INTERCEPTION"/>
<!-- Permission required for CTS test - CtsPermission5TestCases -->
<permission name="android.permission.RENOUNCE_PERMISSIONS" />
<permission name="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS" />
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index 54252b5b712a..9993ce9dec72 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -481,13 +481,20 @@ public final class AudioAttributes implements Parcelable {
*/
public static final int FLAG_NEVER_SPATIALIZE = 0x1 << 15;
+ /**
+ * @hide
+ * Flag indicating the audio is part of a call redirection.
+ * Valid for playback and capture.
+ */
+ public static final int FLAG_CALL_REDIRECTION = 0x1 << 16;
+
// 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_CONTENT_SPATIALIZED
- | FLAG_NEVER_SPATIALIZE;
+ | FLAG_NEVER_SPATIALIZE | FLAG_CALL_REDIRECTION;
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 */
@@ -707,6 +714,14 @@ public final class AudioAttributes implements Parcelable {
return ALLOW_CAPTURE_BY_ALL;
}
+ /**
+ * @hide
+ * Indicates if the audio is used for call redirection
+ * @return true if used for call redirection, false otherwise.
+ */
+ public boolean isForCallRedirection() {
+ return (mFlags & FLAG_CALL_REDIRECTION) == FLAG_CALL_REDIRECTION;
+ }
/**
* Builder class for {@link AudioAttributes} objects.
@@ -763,11 +778,15 @@ public final class AudioAttributes implements Parcelable {
public Builder(AudioAttributes aa) {
mUsage = aa.mUsage;
mContentType = aa.mContentType;
+ mSource = aa.mSource;
mFlags = aa.getAllFlags();
mTags = (HashSet<String>) aa.mTags.clone();
mMuteHapticChannels = aa.areHapticChannelsMuted();
mIsContentSpatialized = aa.isContentSpatialized();
mSpatializationBehavior = aa.getSpatializationBehavior();
+ if ((mFlags & FLAG_CAPTURE_PRIVATE) != 0) {
+ mPrivacySensitive = PRIVACY_SENSITIVE_ENABLED;
+ }
}
/**
@@ -1071,6 +1090,17 @@ public final class AudioAttributes implements Parcelable {
}
/**
+ * @hide
+ * Replace all custom tags
+ * @param tags
+ * @return the same Builder instance.
+ */
+ public Builder replaceTags(HashSet<String> tags) {
+ mTags = (HashSet<String>) tags.clone();
+ return this;
+ }
+
+ /**
* Sets attributes as inferred from the legacy stream types.
* Warning: do not use this method in combination with setting any other attributes such as
* usage, content type, flags or haptic control, as this method will overwrite (the more
@@ -1245,6 +1275,16 @@ public final class AudioAttributes implements Parcelable {
privacySensitive ? PRIVACY_SENSITIVE_ENABLED : PRIVACY_SENSITIVE_DISABLED;
return this;
}
+
+ /**
+ * @hide
+ * Designates the audio to be used for call redirection
+ * @return the same Builder instance.
+ */
+ public Builder setForCallRedirection() {
+ mFlags |= FLAG_CALL_REDIRECTION;
+ return this;
+ }
};
@Override
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 2916b0156055..fb186db91489 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -86,7 +86,7 @@ import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
-
+import java.util.concurrent.Executors;
/**
* AudioManager provides access to volume and ringer mode control.
@@ -7519,7 +7519,7 @@ public class AudioManager {
return getDeviceInfoFromTypeAndAddress(deviceType, null);
}
- /**
+ /**
* @hide
* Returns an {@link AudioDeviceInfo} corresponding to a connected device of the type and
* address provided.
@@ -7641,6 +7641,281 @@ public class AudioManager {
}
}
+
+ /**
+ * @hide
+ * Indicates if the platform allows accessing the uplink and downlink audio of an ongoing
+ * PSTN call.
+ * When true, {@link getCallUplinkInjectionAudioTrack(AudioFormat)} can be used to obtain
+ * an AudioTrack for call uplink audio injection and
+ * {@link getCallDownlinkExtractionAudioRecord(AudioFormat)} can be used to obtain
+ * an AudioRecord for call downlink audio extraction.
+ * @return true if PSTN call audio is accessible, false otherwise.
+ */
+ @TestApi
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION)
+ public boolean isPstnCallAudioInterceptable() {
+ final IAudioService service = getService();
+ try {
+ return service.isPstnCallAudioInterceptable();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ @IntDef(flag = false, prefix = "CALL_REDIRECT_", value = {
+ CALL_REDIRECT_NONE,
+ CALL_REDIRECT_PSTN,
+ CALL_REDIRECT_VOIP }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CallRedirectionMode {}
+
+ /**
+ * Not used for call redirection
+ * @hide
+ */
+ public static final int CALL_REDIRECT_NONE = 0;
+ /**
+ * Used to redirect PSTN call
+ * @hide
+ */
+ public static final int CALL_REDIRECT_PSTN = 1;
+ /**
+ * Used to redirect VoIP call
+ * @hide
+ */
+ public static final int CALL_REDIRECT_VOIP = 2;
+
+
+ private @CallRedirectionMode int getCallRedirectMode() {
+ int mode = getMode();
+ if (mode == MODE_IN_CALL || mode == MODE_CALL_SCREENING
+ || mode == MODE_CALL_REDIRECT) {
+ return CALL_REDIRECT_PSTN;
+ } else if (mode == MODE_IN_COMMUNICATION || mode == MODE_COMMUNICATION_REDIRECT) {
+ return CALL_REDIRECT_VOIP;
+ }
+ return CALL_REDIRECT_NONE;
+ }
+
+ private void checkCallRedirectionFormat(AudioFormat format, boolean isOutput) {
+ if (format.getEncoding() != AudioFormat.ENCODING_PCM_16BIT
+ && format.getEncoding() != AudioFormat.ENCODING_PCM_FLOAT) {
+ throw new UnsupportedOperationException(" Unsupported encoding ");
+ }
+ if (format.getSampleRate() < 8000
+ || format.getSampleRate() > 48000) {
+ throw new UnsupportedOperationException(" Unsupported sample rate ");
+ }
+ if (isOutput && format.getChannelMask() != AudioFormat.CHANNEL_OUT_MONO
+ && format.getChannelMask() != AudioFormat.CHANNEL_OUT_STEREO) {
+ throw new UnsupportedOperationException(" Unsupported output channel mask ");
+ }
+ if (!isOutput && format.getChannelMask() != AudioFormat.CHANNEL_IN_MONO
+ && format.getChannelMask() != AudioFormat.CHANNEL_IN_STEREO) {
+ throw new UnsupportedOperationException(" Unsupported input channel mask ");
+ }
+ }
+
+ class CallIRedirectionClientInfo {
+ public WeakReference trackOrRecord;
+ public int redirectMode;
+ }
+
+ private Object mCallRedirectionLock = new Object();
+ @GuardedBy("mCallRedirectionLock")
+ private CallInjectionModeChangedListener mCallRedirectionModeListener;
+ @GuardedBy("mCallRedirectionLock")
+ private ArrayList<CallIRedirectionClientInfo> mCallIRedirectionClients;
+
+ /**
+ * @hide
+ * Returns an AudioTrack that can be used to inject audio to an active call uplink.
+ * This can be used for functions like call screening or call audio redirection and is reserved
+ * to system apps with privileged permission.
+ * @param format the desired audio format for audio playback.
+ * p>Formats accepted are:
+ * <ul>
+ * <li><em>Sampling rate</em> - 8kHz to 48kHz. </li>
+ * <li><em>Channel mask</em> - Mono or Stereo </li>
+ * <li><em>Sample format</em> - PCM 16 bit or FLOAT 32 bit </li>
+ * </ul>
+ *
+ * @return The AudioTrack used for audio injection
+ * @throws NullPointerException if AudioFormat argument is null.
+ * @throws UnsupportedOperationException if on unsupported AudioFormat is specified.
+ * @throws IllegalArgumentException if an invalid AudioFormat is specified.
+ * @throws SecurityException if permission CALL_AUDIO_INTERCEPTION is missing .
+ * @throws IllegalStateException if current audio mode is not MODE_IN_CALL,
+ * MODE_IN_COMMUNICATION, MODE_CALL_SCREENING, MODE_CALL_REDIRECT
+ * or MODE_COMMUNICATION_REDIRECT.
+ */
+ @TestApi
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION)
+ public @NonNull AudioTrack getCallUplinkInjectionAudioTrack(@NonNull AudioFormat format) {
+ Objects.requireNonNull(format);
+ checkCallRedirectionFormat(format, true /* isOutput */);
+
+ AudioTrack track = null;
+ int redirectMode = getCallRedirectMode();
+ if (redirectMode == CALL_REDIRECT_NONE) {
+ throw new IllegalStateException(
+ " not available in mode " + AudioSystem.modeToString(getMode()));
+ } else if (redirectMode == CALL_REDIRECT_PSTN && !isPstnCallAudioInterceptable()) {
+ throw new UnsupportedOperationException(" PSTN Call audio not accessible ");
+ }
+
+ track = new AudioTrack.Builder()
+ .setAudioAttributes(new AudioAttributes.Builder()
+ .setSystemUsage(AudioAttributes.USAGE_CALL_ASSISTANT)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+ .build())
+ .setAudioFormat(format)
+ .setCallRedirectionMode(redirectMode)
+ .build();
+
+ if (track != null && track.getState() != AudioTrack.STATE_UNINITIALIZED) {
+ synchronized (mCallRedirectionLock) {
+ if (mCallRedirectionModeListener == null) {
+ mCallRedirectionModeListener = new CallInjectionModeChangedListener();
+ try {
+ addOnModeChangedListener(
+ Executors.newSingleThreadExecutor(), mCallRedirectionModeListener);
+ } catch (Exception e) {
+ Log.e(TAG, "addOnModeChangedListener failed with exception: " + e);
+ mCallRedirectionModeListener = null;
+ throw new UnsupportedOperationException(" Cannot register mode listener ");
+ }
+ mCallIRedirectionClients = new ArrayList<CallIRedirectionClientInfo>();
+ }
+ CallIRedirectionClientInfo info = new CallIRedirectionClientInfo();
+ info.redirectMode = redirectMode;
+ info.trackOrRecord = new WeakReference<AudioTrack>(track);
+ mCallIRedirectionClients.add(info);
+ }
+ } else {
+ throw new UnsupportedOperationException(" Cannot create the AudioTrack");
+ }
+ return track;
+ }
+
+ /**
+ * @hide
+ * Returns an AudioRecord that can be used to extract audio from an active call downlink.
+ * This can be used for functions like call screening or call audio redirection and is reserved
+ * to system apps with privileged permission.
+ * @param format the desired audio format for audio capture.
+ *<p>Formats accepted are:
+ * <ul>
+ * <li><em>Sampling rate</em> - 8kHz to 48kHz. </li>
+ * <li><em>Channel mask</em> - Mono or Stereo </li>
+ * <li><em>Sample format</em> - PCM 16 bit or FLOAT 32 bit </li>
+ * </ul>
+ *
+ * @return The AudioRecord used for audio extraction
+ * @throws UnsupportedOperationException if on unsupported AudioFormat is specified.
+ * @throws IllegalArgumentException if an invalid AudioFormat is specified.
+ * @throws NullPointerException if AudioFormat argument is null.
+ * @throws SecurityException if permission CALL_AUDIO_INTERCEPTION is missing .
+ * @throws IllegalStateException if current audio mode is not MODE_IN_CALL,
+ * MODE_IN_COMMUNICATION, MODE_CALL_SCREENING, MODE_CALL_REDIRECT
+ * or MODE_COMMUNICATION_REDIRECT.
+ */
+ @TestApi
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION)
+ public @NonNull AudioRecord getCallDownlinkExtractionAudioRecord(@NonNull AudioFormat format) {
+ Objects.requireNonNull(format);
+ checkCallRedirectionFormat(format, false /* isOutput */);
+
+ AudioRecord record = null;
+ int redirectMode = getCallRedirectMode();
+ if (redirectMode == CALL_REDIRECT_NONE) {
+ throw new IllegalStateException(
+ " not available in mode " + AudioSystem.modeToString(getMode()));
+ } else if (redirectMode == CALL_REDIRECT_PSTN && !isPstnCallAudioInterceptable()) {
+ throw new UnsupportedOperationException(" PSTN Call audio not accessible ");
+ }
+
+ record = new AudioRecord.Builder()
+ .setAudioAttributes(new AudioAttributes.Builder()
+ .setInternalCapturePreset(MediaRecorder.AudioSource.VOICE_DOWNLINK)
+ .build())
+ .setAudioFormat(format)
+ .setCallRedirectionMode(redirectMode)
+ .build();
+
+ if (record != null && record.getState() != AudioRecord.STATE_UNINITIALIZED) {
+ synchronized (mCallRedirectionLock) {
+ if (mCallRedirectionModeListener == null) {
+ mCallRedirectionModeListener = new CallInjectionModeChangedListener();
+ try {
+ addOnModeChangedListener(
+ Executors.newSingleThreadExecutor(), mCallRedirectionModeListener);
+ } catch (Exception e) {
+ Log.e(TAG, "addOnModeChangedListener failed with exception: " + e);
+ mCallRedirectionModeListener = null;
+ throw new UnsupportedOperationException(" Cannot register mode listener ");
+ }
+ mCallIRedirectionClients = new ArrayList<CallIRedirectionClientInfo>();
+ }
+ CallIRedirectionClientInfo info = new CallIRedirectionClientInfo();
+ info.redirectMode = redirectMode;
+ info.trackOrRecord = new WeakReference<AudioRecord>(record);
+ mCallIRedirectionClients.add(info);
+ }
+ } else {
+ throw new UnsupportedOperationException(" Cannot create the AudioRecord");
+ }
+ return record;
+ }
+
+ class CallInjectionModeChangedListener implements OnModeChangedListener {
+ @Override
+ public void onModeChanged(@AudioMode int mode) {
+ synchronized (mCallRedirectionLock) {
+ final ArrayList<CallIRedirectionClientInfo> clientInfos =
+ (ArrayList<CallIRedirectionClientInfo>) mCallIRedirectionClients.clone();
+ for (CallIRedirectionClientInfo info : clientInfos) {
+ Object trackOrRecord = info.trackOrRecord.get();
+ if (trackOrRecord != null) {
+ if ((info.redirectMode == CALL_REDIRECT_PSTN
+ && mode != MODE_IN_CALL && mode != MODE_CALL_SCREENING
+ && mode != MODE_CALL_REDIRECT)
+ || (info.redirectMode == CALL_REDIRECT_VOIP
+ && mode != MODE_IN_COMMUNICATION
+ && mode != MODE_COMMUNICATION_REDIRECT)) {
+ if (trackOrRecord instanceof AudioTrack) {
+ AudioTrack track = (AudioTrack) trackOrRecord;
+ track.release();
+ } else {
+ AudioRecord record = (AudioRecord) trackOrRecord;
+ record.release();
+ }
+ mCallIRedirectionClients.remove(info);
+ }
+ }
+ }
+ if (mCallIRedirectionClients.isEmpty()) {
+ try {
+ if (mCallRedirectionModeListener != null) {
+ removeOnModeChangedListener(mCallRedirectionModeListener);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "removeOnModeChangedListener failed with exception: " + e);
+ } finally {
+ mCallRedirectionModeListener = null;
+ mCallIRedirectionClients = null;
+ }
+ }
+ }
+ }
+ }
+
//---------------------------------------------------------
// Inner classes
//--------------------
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 7c6ae28bdd30..e76bb42560f6 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -32,6 +32,7 @@ import android.content.AttributionSource.ScopedParcelState;
import android.content.Context;
import android.media.MediaRecorder.Source;
import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
import android.media.audiopolicy.AudioPolicy;
import android.media.metrics.LogSessionId;
import android.media.projection.MediaProjection;
@@ -58,6 +59,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
@@ -404,7 +406,9 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
// is this AudioRecord using REMOTE_SUBMIX at full volume?
if (attributes.getCapturePreset() == MediaRecorder.AudioSource.REMOTE_SUBMIX) {
- final AudioAttributes.Builder filteredAttr = new AudioAttributes.Builder();
+ final AudioAttributes.Builder ab =
+ new AudioAttributes.Builder(attributes);
+ HashSet<String> filteredTags = new HashSet<String>();
final Iterator<String> tagsIter = attributes.getTags().iterator();
while (tagsIter.hasNext()) {
final String tag = tagsIter.next();
@@ -412,15 +416,15 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
mIsSubmixFullVolume = true;
Log.v(TAG, "Will record from REMOTE_SUBMIX at full fixed volume");
} else { // SUBMIX_FIXED_VOLUME: is not to be propagated to the native layers
- filteredAttr.addTag(tag);
+ filteredTags.add(tag);
}
}
- filteredAttr.setInternalCapturePreset(attributes.getCapturePreset());
- mAudioAttributes = filteredAttr.build();
- } else {
- mAudioAttributes = attributes;
+ ab.replaceTags(filteredTags);
+ attributes = ab.build();
}
+ mAudioAttributes = attributes;
+
int rate = format.getSampleRate();
if (rate == AudioFormat.SAMPLE_RATE_UNSPECIFIED) {
rate = 0;
@@ -432,7 +436,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
encoding = format.getEncoding();
}
- audioParamCheck(attributes.getCapturePreset(), rate, encoding);
+ audioParamCheck(mAudioAttributes.getCapturePreset(), rate, encoding);
if ((format.getPropertySetMask()
& AudioFormat.AUDIO_FORMAT_HAS_PROPERTY_CHANNEL_INDEX_MASK) != 0) {
@@ -595,6 +599,8 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
private int mPrivacySensitive = PRIVACY_SENSITIVE_DEFAULT;
private int mMaxSharedAudioHistoryMs = 0;
+ private int mCallRedirectionMode = AudioManager.CALL_REDIRECT_NONE;
+
private static final int PRIVACY_SENSITIVE_DEFAULT = -1;
private static final int PRIVACY_SENSITIVE_DISABLED = 0;
private static final int PRIVACY_SENSITIVE_ENABLED = 1;
@@ -791,6 +797,65 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
/**
* @hide
+ * Sets the {@link AudioRecord} call redirection mode.
+ * Used when creating an AudioRecord to extract audio from call downlink path. The mode
+ * indicates if the call is a PSTN call or a VoIP call in which case a dynamic audio
+ * policy is created to forward all playback with voice communication usage this record.
+ *
+ * @param callRedirectionMode one of
+ * {@link AudioManager#CALL_REDIRECT_NONE},
+ * {@link AudioManager#CALL_REDIRECT_PSTN},
+ * or {@link AAudioManager#CALL_REDIRECT_VOIP}.
+ * @return the same Builder instance.
+ * @throws IllegalArgumentException if {@code callRedirectionMode} is not valid.
+ */
+ public @NonNull Builder setCallRedirectionMode(
+ @AudioManager.CallRedirectionMode int callRedirectionMode) {
+ switch (callRedirectionMode) {
+ case AudioManager.CALL_REDIRECT_NONE:
+ case AudioManager.CALL_REDIRECT_PSTN:
+ case AudioManager.CALL_REDIRECT_VOIP:
+ mCallRedirectionMode = callRedirectionMode;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid call redirection mode " + callRedirectionMode);
+ }
+ return this;
+ }
+
+ private @NonNull AudioRecord buildCallExtractionRecord() {
+ AudioMixingRule audioMixingRule = new AudioMixingRule.Builder()
+ .addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE,
+ new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
+ .setForCallRedirection()
+ .build())
+ .addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE,
+ new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
+ .setForCallRedirection()
+ .build())
+ .setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS)
+ .build();
+ AudioMix audioMix = new AudioMix.Builder(audioMixingRule)
+ .setFormat(mFormat)
+ .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
+ .build();
+ AudioPolicy audioPolicy = new AudioPolicy.Builder(null).addMix(audioMix).build();
+ if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
+ throw new UnsupportedOperationException("Error: could not register audio policy");
+ }
+ AudioRecord record = audioPolicy.createAudioRecordSink(audioMix);
+ if (record == null) {
+ throw new UnsupportedOperationException("Cannot create extraction AudioRecord");
+ }
+ record.unregisterAudioPolicyOnRelease(audioPolicy);
+ return record;
+ }
+
+ /**
+ * @hide
* Specifies the maximum duration in the past of the this AudioRecord's capture buffer
* that can be shared with another app by calling
* {@link AudioRecord#shareAudioHistory(String, long)}.
@@ -897,6 +962,14 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection,
.build();
}
+ if (mCallRedirectionMode == AudioManager.CALL_REDIRECT_VOIP) {
+ return buildCallExtractionRecord();
+ } else if (mCallRedirectionMode == AudioManager.CALL_REDIRECT_PSTN) {
+ mAttributes = new AudioAttributes.Builder(mAttributes)
+ .setForCallRedirection()
+ .build();
+ }
+
try {
// If the buffer size is not specified,
// use a single frame for the buffer size and let the
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 0450a808df35..ea692b940c81 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -26,6 +26,9 @@ import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
+import android.media.audiopolicy.AudioPolicy;
import android.media.metrics.LogSessionId;
import android.os.Binder;
import android.os.Build;
@@ -574,6 +577,8 @@ public class AudioTrack extends PlayerBase
*/
@NonNull private LogSessionId mLogSessionId = LogSessionId.LOG_SESSION_ID_NONE;
+ private AudioPolicy mAudioPolicy;
+
//--------------------------------
// Used exclusively by native code
//--------------------
@@ -1032,6 +1037,7 @@ public class AudioTrack extends PlayerBase
private int mPerformanceMode = PERFORMANCE_MODE_NONE;
private boolean mOffload = false;
private TunerConfiguration mTunerConfiguration;
+ private int mCallRedirectionMode = AudioManager.CALL_REDIRECT_NONE;
/**
* Constructs a new Builder with the default values as described above.
@@ -1227,6 +1233,74 @@ public class AudioTrack extends PlayerBase
}
/**
+ * Sets the tuner configuration for the {@code AudioTrack}.
+ *
+ * The {@link AudioTrack.TunerConfiguration} consists of parameters obtained from
+ * the Android TV tuner API which indicate the audio content stream id and the
+ * synchronization id for the {@code AudioTrack}.
+ *
+ * @param tunerConfiguration obtained by {@link AudioTrack.TunerConfiguration.Builder}.
+ * @return the same Builder instance.
+ * @hide
+ */
+
+ /**
+ * @hide
+ * Sets the {@link AudioTrack} call redirection mode.
+ * Used when creating an AudioTrack to inject audio to call uplink path. The mode
+ * indicates if the call is a PSTN call or a VoIP call in which case a dynamic audio
+ * policy is created to use this track as the source for all capture with voice
+ * communication preset.
+ *
+ * @param callRedirectionMode one of
+ * {@link AudioManager#CALL_REDIRECT_NONE},
+ * {@link AudioManager#CALL_REDIRECT_PSTN},
+ * or {@link AAudioManager#CALL_REDIRECT_VOIP}.
+ * @return the same Builder instance.
+ * @throws IllegalArgumentException if {@code callRedirectionMode} is not valid.
+ */
+ public @NonNull Builder setCallRedirectionMode(
+ @AudioManager.CallRedirectionMode int callRedirectionMode) {
+ switch (callRedirectionMode) {
+ case AudioManager.CALL_REDIRECT_NONE:
+ case AudioManager.CALL_REDIRECT_PSTN:
+ case AudioManager.CALL_REDIRECT_VOIP:
+ mCallRedirectionMode = callRedirectionMode;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid call redirection mode " + callRedirectionMode);
+ }
+ return this;
+ }
+
+ private @NonNull AudioTrack buildCallInjectionTrack() {
+ AudioMixingRule audioMixingRule = new AudioMixingRule.Builder()
+ .addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET,
+ new AudioAttributes.Builder()
+ .setCapturePreset(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
+ .setForCallRedirection()
+ .build())
+ .setTargetMixRole(AudioMixingRule.MIX_ROLE_INJECTOR)
+ .build();
+ AudioMix audioMix = new AudioMix.Builder(audioMixingRule)
+ .setFormat(mFormat)
+ .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
+ .build();
+ AudioPolicy audioPolicy =
+ new AudioPolicy.Builder(/*context=*/ null).addMix(audioMix).build();
+ if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) {
+ throw new UnsupportedOperationException("Error: could not register audio policy");
+ }
+ AudioTrack track = audioPolicy.createAudioTrackSource(audioMix);
+ if (track == null) {
+ throw new UnsupportedOperationException("Cannot create injection AudioTrack");
+ }
+ track.unregisterAudioPolicyOnRelease(audioPolicy);
+ return track;
+ }
+
+ /**
* Builds an {@link AudioTrack} instance initialized with all the parameters set
* on this <code>Builder</code>.
* @return a new successfully initialized {@link AudioTrack} instance.
@@ -1270,6 +1344,14 @@ public class AudioTrack extends PlayerBase
.build();
}
+ if (mCallRedirectionMode == AudioManager.CALL_REDIRECT_VOIP) {
+ return buildCallInjectionTrack();
+ } else if (mCallRedirectionMode == AudioManager.CALL_REDIRECT_PSTN) {
+ mAttributes = new AudioAttributes.Builder(mAttributes)
+ .setForCallRedirection()
+ .build();
+ }
+
if (mOffload) {
if (mPerformanceMode == PERFORMANCE_MODE_LOW_LATENCY) {
throw new UnsupportedOperationException(
@@ -1315,6 +1397,16 @@ public class AudioTrack extends PlayerBase
}
/**
+ * Sets an {@link AudioPolicy} to automatically unregister when the track is released.
+ *
+ * <p>This is to prevent users of the call audio injection API from having to manually
+ * unregister the policy that was used to create the track.
+ */
+ private void unregisterAudioPolicyOnRelease(AudioPolicy audioPolicy) {
+ mAudioPolicy = audioPolicy;
+ }
+
+ /**
* Configures the delay and padding values for the current compressed stream playing
* in offload mode.
* This can only be used on a track successfully initialized with
@@ -1879,6 +1971,11 @@ public class AudioTrack extends PlayerBase
} catch(IllegalStateException ise) {
// don't raise an exception, we're releasing the resources.
}
+ if (mAudioPolicy != null) {
+ AudioManager.unregisterAudioPolicyAsyncStatic(mAudioPolicy);
+ mAudioPolicy = null;
+ }
+
baseRelease();
native_release();
synchronized (mPlayStateLock) {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 35191bde8ac4..f15f880a42a7 100755
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -445,4 +445,6 @@ interface IAudioService {
void unregisterSpatializerOutputCallback(in ISpatializerOutputCallback cb);
boolean isVolumeFixed();
+
+ boolean isPstnCallAudioInterceptable();
}
diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java
index 8080aad6dcc2..f85bdee18967 100644
--- a/media/java/android/media/audiopolicy/AudioMix.java
+++ b/media/java/android/media/audiopolicy/AudioMix.java
@@ -241,6 +241,11 @@ public class AudioMix {
}
/** @hide */
+ public boolean isForCallRedirection() {
+ return mRule.isForCallRedirection();
+ }
+
+ /** @hide */
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java
index 611229080e4c..c91275941200 100644
--- a/media/java/android/media/audiopolicy/AudioMixingRule.java
+++ b/media/java/android/media/audiopolicy/AudioMixingRule.java
@@ -23,6 +23,7 @@ import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.media.AudioAttributes;
+import android.media.MediaRecorder;
import android.os.Build;
import android.os.Parcel;
import android.util.Log;
@@ -262,6 +263,22 @@ public class AudioMixingRule {
}
/** @hide */
+ public boolean isForCallRedirection() {
+ for (AudioMixMatchCriterion criterion : mCriteria) {
+ if (criterion.mAttr != null
+ && (criterion.mRule == RULE_MATCH_ATTRIBUTE_USAGE
+ && criterion.mAttr.getUsage() == AudioAttributes.USAGE_VOICE_COMMUNICATION)
+ || (criterion.mRule == RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET
+ && criterion.mAttr.getCapturePreset()
+ == MediaRecorder.AudioSource.VOICE_COMMUNICATION)
+ && criterion.mAttr.isForCallRedirection()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** @hide */
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 3e8d76ac7551..0f08d79bf3bb 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -588,6 +588,10 @@ public class AudioPolicy {
boolean canModifyAudioRouting = PackageManager.PERMISSION_GRANTED
== checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING);
+ boolean canInterceptCallAudio = PackageManager.PERMISSION_GRANTED
+ == checkCallingOrSelfPermission(
+ android.Manifest.permission.CALL_AUDIO_INTERCEPTION);
+
boolean canProjectAudio;
try {
canProjectAudio = mProjection != null && mProjection.getProjection().canProjectAudio();
@@ -596,7 +600,9 @@ public class AudioPolicy {
throw e.rethrowFromSystemServer();
}
- if (!((isLoopbackRenderPolicy() && canProjectAudio) || canModifyAudioRouting)) {
+ if (!((isLoopbackRenderPolicy() && canProjectAudio)
+ || (isCallRedirectionPolicy() && canInterceptCallAudio)
+ || canModifyAudioRouting)) {
Slog.w(TAG, "Cannot use AudioPolicy for pid " + Binder.getCallingPid() + " / uid "
+ Binder.getCallingUid() + ", needs MODIFY_AUDIO_ROUTING or "
+ "MediaProjection that can project audio.");
@@ -612,6 +618,17 @@ public class AudioPolicy {
}
}
+ private boolean isCallRedirectionPolicy() {
+ synchronized (mLock) {
+ for (AudioMix mix : mConfig.mMixes) {
+ if (mix.isForCallRedirection()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
/**
* Returns {@link PackageManager#PERMISSION_GRANTED} if the caller has the given permission.
*/
@@ -728,13 +745,16 @@ public class AudioPolicy {
.setChannelMask(AudioFormat.inChannelMaskFromOutChannelMask(
mix.getFormat().getChannelMask()))
.build();
+
+ AudioAttributes.Builder ab = new AudioAttributes.Builder()
+ .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX)
+ .addTag(addressForTag(mix))
+ .addTag(AudioRecord.SUBMIX_FIXED_VOLUME);
+ if (mix.isForCallRedirection()) {
+ ab.setForCallRedirection();
+ }
// create the AudioRecord, configured for loop back, using the same format as the mix
- AudioRecord ar = new AudioRecord(
- new AudioAttributes.Builder()
- .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX)
- .addTag(addressForTag(mix))
- .addTag(AudioRecord.SUBMIX_FIXED_VOLUME)
- .build(),
+ AudioRecord ar = new AudioRecord(ab.build(),
mixFormat,
AudioRecord.getMinBufferSize(mix.getFormat().getSampleRate(),
// using stereo for buffer size to avoid the current poor support for masks
@@ -768,11 +788,13 @@ public class AudioPolicy {
}
checkMixReadyToUse(mix, true/*for an AudioTrack*/);
// create the AudioTrack, configured for loop back, using the same format as the mix
- AudioTrack at = new AudioTrack(
- new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_VIRTUAL_SOURCE)
- .addTag(addressForTag(mix))
- .build(),
+ AudioAttributes.Builder ab = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_VIRTUAL_SOURCE)
+ .addTag(addressForTag(mix));
+ if (mix.isForCallRedirection()) {
+ ab.setForCallRedirection();
+ }
+ AudioTrack at = new AudioTrack(ab.build(),
mix.getFormat(),
AudioTrack.getMinBufferSize(mix.getFormat().getSampleRate(),
mix.getFormat().getChannelMask(), mix.getFormat().getEncoding()),
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 0f3b08210d0b..9c8a663545b7 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1241,6 +1241,10 @@ public class AudioService extends IAudioService.Stub
initMinStreamVolumeWithoutModifyAudioSettings();
updateVibratorInfos();
+
+ synchronized (mSupportedSystemUsagesLock) {
+ AudioSystem.setSupportedSystemUsages(mSupportedSystemUsages);
+ }
}
//-----------------------------------------------------------------
@@ -3224,6 +3228,15 @@ public class AudioService extends IAudioService.Stub
}
}
+ private void enforceCallAudioInterceptionPermission() {
+ if (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.CALL_AUDIO_INTERCEPTION)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Missing CALL_AUDIO_INTERCEPTION permission");
+ }
+ }
+
+
/** @see AudioManager#setVolumeIndexForAttributes(attr, int, int) */
public void setVolumeIndexForAttributes(@NonNull AudioAttributes attr, int index, int flags,
String callingPackage, String attributionTag) {
@@ -4943,6 +4956,26 @@ public class AudioService extends IAudioService.Stub
mModeDispatchers.unregister(dispatcher);
}
+ /** @see AudioManager#isPstnCallAudioInterceptable() */
+ public boolean isPstnCallAudioInterceptable() {
+ enforceCallAudioInterceptionPermission();
+
+ boolean uplinkDeviceFound = false;
+ boolean downlinkDeviceFound = false;
+ AudioDeviceInfo[] devices = AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_ALL);
+ for (AudioDeviceInfo device : devices) {
+ if (device.getInternalType() == AudioSystem.DEVICE_OUT_TELEPHONY_TX) {
+ uplinkDeviceFound = true;
+ } else if (device.getInternalType() == AudioSystem.DEVICE_IN_TELEPHONY_RX) {
+ downlinkDeviceFound = true;
+ }
+ if (uplinkDeviceFound && downlinkDeviceFound) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/** @see AudioManager#setRttEnabled() */
@Override
public void setRttEnabled(boolean rttEnabled) {
@@ -8169,7 +8202,10 @@ public class AudioService extends IAudioService.Stub
private void validateAudioAttributesUsage(@NonNull AudioAttributes audioAttributes) {
@AudioAttributes.AttributeUsage int usage = audioAttributes.getSystemUsage();
if (AudioAttributes.isSystemUsage(usage)) {
- if (callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)) {
+ if ((usage == AudioAttributes.USAGE_CALL_ASSISTANT
+ && (audioAttributes.getAllFlags() & AudioAttributes.FLAG_CALL_REDIRECTION) != 0
+ && callerHasPermission(Manifest.permission.CALL_AUDIO_INTERCEPTION))
+ || callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)) {
if (!isSupportedSystemUsage(usage)) {
throw new IllegalArgumentException(
"Unsupported usage " + AudioAttributes.usageToString(usage));
@@ -8183,8 +8219,12 @@ public class AudioService extends IAudioService.Stub
private boolean isValidAudioAttributesUsage(@NonNull AudioAttributes audioAttributes) {
@AudioAttributes.AttributeUsage int usage = audioAttributes.getSystemUsage();
if (AudioAttributes.isSystemUsage(usage)) {
- return callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
- && isSupportedSystemUsage(usage);
+ return isSupportedSystemUsage(usage)
+ && ((usage == AudioAttributes.USAGE_CALL_ASSISTANT
+ && (audioAttributes.getAllFlags()
+ & AudioAttributes.FLAG_CALL_REDIRECTION) != 0
+ && callerHasPermission(Manifest.permission.CALL_AUDIO_INTERCEPTION))
+ || callerHasPermission(Manifest.permission.MODIFY_AUDIO_ROUTING));
}
return true;
}
@@ -9578,6 +9618,7 @@ public class AudioService extends IAudioService.Stub
boolean requireValidProjection = false;
boolean requireCaptureAudioOrMediaOutputPerm = false;
boolean requireModifyRouting = false;
+ boolean requireCallAudioInterception = false;
ArrayList<AudioMix> voiceCommunicationCaptureMixes = null;
@@ -9618,7 +9659,10 @@ public class AudioService extends IAudioService.Stub
// otherwise MODIFY_AUDIO_ROUTING permission is required
if (mix.getRouteFlags() == mix.ROUTE_FLAG_LOOP_BACK_RENDER && projection != null) {
requireValidProjection |= true;
- } else {
+ } else if (mix.isForCallRedirection()) {
+ requireCallAudioInterception |= true;
+ } else if (mix.containsMatchAttributeRuleForUsage(
+ AudioAttributes.USAGE_VOICE_COMMUNICATION)) {
requireModifyRouting |= true;
}
}
@@ -9655,6 +9699,12 @@ public class AudioService extends IAudioService.Stub
return false;
}
+ if (requireCallAudioInterception
+ && !callerHasPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION)) {
+ Log.e(TAG, "Can not capture audio without CALL_AUDIO_INTERCEPTION");
+ return false;
+ }
+
return true;
}