Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 78eef3ab authored by Eric Laurent's avatar Eric Laurent
Browse files

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
parent c86875fb
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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);
+3 −0
Original line number Diff line number Diff line
@@ -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);
  }

+2 −0
Original line number Diff line number Diff line
@@ -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" />
+41 −1
Original line number Diff line number Diff line
@@ -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;
            }
        }

        /**
@@ -1070,6 +1089,17 @@ public final class AudioAttributes implements Parcelable {
            return this;
        }

        /**
         * @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
@@ -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
+277 −2
Original line number Diff line number Diff line
@@ -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.
@@ -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
    //--------------------
Loading