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

Commit 5d66e1d5 authored by Tyler Gunn's avatar Tyler Gunn
Browse files

Add ability to generate remote call recording tone.

Add Telecom support to generate a call recording tone in the outgoing
telephony audio stream when a recording app is enabled on the device.

The new CallRecordingTonePlayer will play a call recording warning tone to
the remote user via outgoing telephony audio stream.  This functionality
if gated on device support and carrier config in the telephony stack.

Test: Added new unit tests.  Manually changed carrier config to enable
tones on local deice, installed audio recording app and verified while it
is recording the tone is audible on remote end.
Bug: 64138141
Change-Id: Ifcf5a49704af5a1352527e705e7d2a3bc16d7fdd
parent 7c031f22
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -36,6 +36,8 @@
    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
    <uses-permission android:name="android.permission.MANAGE_USERS" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <!-- Required to determine source of ongoing audio recordings. -->
    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
    <uses-permission android:name="android.permission.READ_CALL_LOG" />
    <uses-permission android:name="android.permission.STOP_APP_SWITCHES" />

res/raw/record.ogg

0 → 100644
+104 KiB

File added.

No diff preview for this file type.

+19 −2
Original line number Diff line number Diff line
@@ -434,6 +434,12 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,

    private boolean mIsWorkCall;

    /**
     * Tracks whether this {@link Call}'s {@link #getTargetPhoneAccount()} has
     * {@link PhoneAccount#EXTRA_PLAY_CALL_RECORDING_TONE} set.
     */
    private boolean mUseCallRecordingTone;

    // Set to true once the NewOutgoingCallIntentBroadcast comes back and is processed.
    private boolean mIsNewOutgoingCallIntentBroadcastDone = false;

@@ -1077,7 +1083,7 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
            for (Listener l : mListeners) {
                l.onTargetPhoneAccountChanged(this);
            }
            configureIsWorkCall();
            configureCallAttributes();
        }
        checkIfVideoCapable();
    }
@@ -1134,6 +1140,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
        return mIsWorkCall;
    }

    public boolean isUsingCallRecordingTone() {
        return mUseCallRecordingTone;
    }

    public boolean isVideoCallingSupported() {
        return mIsVideoCallingSupported;
    }
@@ -1201,9 +1211,10 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
        return mHandoverState;
    }

    private void configureIsWorkCall() {
    private void configureCallAttributes() {
        PhoneAccountRegistrar phoneAccountRegistrar = mCallsManager.getPhoneAccountRegistrar();
        boolean isWorkCall = false;
        boolean isCallRecordingToneSupported = false;
        PhoneAccount phoneAccount =
                phoneAccountRegistrar.getPhoneAccountUnchecked(mTargetPhoneAccountHandle);
        if (phoneAccount != null) {
@@ -1216,8 +1227,14 @@ public class Call implements CreateConnectionResponse, EventManager.Loggable,
            if (userHandle != null) {
                isWorkCall = UserUtil.isManagedProfile(mContext, userHandle);
            }

            isCallRecordingToneSupported = (phoneAccount.hasCapabilities(
                    PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) && phoneAccount.getExtras() != null
                    && phoneAccount.getExtras().getBoolean(
                    PhoneAccount.EXTRA_PLAY_CALL_RECORDING_TONE, false));
        }
        mIsWorkCall = isWorkCall;
        mUseCallRecordingTone = isCallRecordingToneSupported;
    }

    /**
+309 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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 com.android.server.telecom;

import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.AudioRecordingConfiguration;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Looper;
import android.telecom.Log;

import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Plays a periodic, repeating tone to the remote party when an app on the device is recording
 * a call.  A call recording tone is played on the called party's audio if an app begins recording.
 * This ensures that the remote party is aware of the fact call recording is in progress.
 */
public class CallRecordingTonePlayer extends CallsManagerListenerBase {
    /**
     * Callback registered with {@link AudioManager} to track apps which are recording audio.
     * Registered when a SIM call is added and unregistered when it ends.
     */
    private AudioManager.AudioRecordingCallback mAudioRecordingCallback =
            new AudioManager.AudioRecordingCallback() {
                @Override
                public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
                    synchronized (mLock) {
                        try {
                            Log.startSession("CRTP.oRCC");
                            handleRecordingConfigurationChange(configs);
                            maybeStartCallAudioTone();
                            maybeStopCallAudioTone();
                        } finally {
                            Log.endSession();
                        }
                    }
                }
    };

    private final AudioManager mAudioManager;
    private final Context mContext;
    private final TelecomSystem.SyncRoot mLock;
    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
    private boolean mIsRecording = false;
    private MediaPlayer mRecordingTonePlayer = null;
    private List<Call> mCalls = new ArrayList<>();

    public CallRecordingTonePlayer(Context context, AudioManager audioManager,
            TelecomSystem.SyncRoot lock) {
        mContext = context;
        mAudioManager = audioManager;
        mLock = lock;
    }

    @Override
    public void onCallAdded(Call call) {
        if (!shouldUseRecordingTone(call)) {
            return; // Ignore calls which don't use the recording tone.
        }

        addCall(call);
    }

    @Override
    public void onCallRemoved(Call call) {
        if (!shouldUseRecordingTone(call)) {
            return; // Ignore calls which don't use the recording tone.
        }

        removeCall(call);
    }

    @Override
    public void onCallStateChanged(Call call, int oldState, int newState) {
        if (!shouldUseRecordingTone(call)) {
            return; // Ignore calls which don't use the recording tone.
        }

        if (mIsRecording) {
            // Handle start and stop now; could be stopping if we held a call.
            maybeStartCallAudioTone();
            maybeStopCallAudioTone();
        }
    }

    /**
     * Handles addition of a new call by:
     * 1. Registering an audio manager listener to track changes to recording state.
     * 2. Checking if there is recording in progress.
     * 3. Potentially starting the call recording tone.
     *
     * @param toAdd The call to start tracking.
     */
    private void addCall(Call toAdd) {
        boolean isFirstCall = mCalls.isEmpty();

        mCalls.add(toAdd);
        if (isFirstCall) {
            // First call, so register the recording callback.  Also check for recordings which
            // started before we registered the callback (we don't receive a callback for those).
            handleRecordingConfigurationChange(mAudioManager.getActiveRecordingConfigurations());
            mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback,
                    mMainThreadHandler);
        }

        maybeStartCallAudioTone();
    }

    /**
     * Handles removal of tracked call by unregistering the audio recording callback and stopping
     * the recording tone if this is the last call.
     * @param toRemove The call to stop tracking.
     */
    private void removeCall(Call toRemove) {
        mCalls.remove(toRemove);
        boolean isLastCall = mCalls.isEmpty();

        if (isLastCall) {
            mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback);
            maybeStopCallAudioTone();
        }
    }

    /**
     * Determines whether a call is applicable for call recording tone generation.
     * Only top level sim calls are considered which have
     * {@link android.telecom.PhoneAccount#EXTRA_PLAY_CALL_RECORDING_TONE} set on their target
     * {@link android.telecom.PhoneAccount}.
     * @param call The call to check.
     * @return {@code true} if the call is should use the recording tone, {@code false} otherwise.
     */
    private boolean shouldUseRecordingTone(Call call) {
        return call.getParentCall() == null && !call.isExternalCall() &&
                !call.isEmergencyCall() && call.isUsingCallRecordingTone();
    }

    /**
     * Starts the call recording tone if recording has started and there are calls.
     */
    private void maybeStartCallAudioTone() {
        if (mIsRecording && hasActiveCall()) {
            startCallRecordingTone(mContext);
        }
    }

    /**
     * Stops the call recording tone if recording has stopped or there are no longer any calls.
     */
    private void maybeStopCallAudioTone() {
        if (!mIsRecording || !hasActiveCall()) {
            stopCallRecordingTone();
        }
    }

    /**
     * Determines if any of the calls tracked are active.
     * @return {@code true} if there is an active call, {@code false} otherwise.
     */
    private boolean hasActiveCall() {
        return !mCalls.isEmpty() && mCalls.stream()
                .filter(call -> call.isActive())
                .count() > 0;
    }

    /**
     * Handles changes to recording configuration changes.
     * @param configs the recording configurations.
     */
    private void handleRecordingConfigurationChange(List<AudioRecordingConfiguration> configs) {
        if (configs == null) {
            configs = Collections.emptyList();
        }
        boolean wasRecording = mIsRecording;
        boolean isRecording = isRecordingInProgress(configs);
        if (wasRecording != isRecording) {
            mIsRecording = isRecording;
            if (isRecording) {
                Log.i(this, "handleRecordingConfigurationChange: recording started");
            } else {
                Log.i(this, "handleRecordingConfigurationChange: recording stopped");
            }
        }
    }

    /**
     * Determines if call recording is potentially in progress.
     * Excludes from consideration any recordings from packages which have active calls themselves.
     * Presumably a call with an active recording session is doing so in order to capture the audio
     * for the purpose of making a call.  In practice Telephony calls don't show up in the
     * recording configurations, but it is reasonable to consider Connection Managers which are
     * using an over the top voip solution for calling.
     * @param configs the ongoing recording configurations.
     * @return {@code true} if there are active audio recordings for which we want to generate a
     * call recording tone, {@code false} otherwise.
     */
    private boolean isRecordingInProgress(List<AudioRecordingConfiguration> configs) {
        String recordingPackages = configs.stream()
                .map(config -> config.getClientPackageName())
                .collect(Collectors.joining(", "));
        Log.i(this, "isRecordingInProgress: recordingPackages=%s", recordingPackages);
        return configs.stream()
                .filter(config -> !hasCallForPackage(config.getClientPackageName()))
                .count() > 0;
    }

    /**
     * Begins playing the call recording tone to the remote end of the call.
     * The call recording tone is played via the telephony audio output device; this means that it
     * will only be audible to the remote end of the call, not the local side.
     *
     * @param context required for obtaining media player.
     */
    private void startCallRecordingTone(Context context) {
        if (mRecordingTonePlayer != null) {
            return;
        }
        AudioDeviceInfo telephonyDevice = getTelephonyDevice(mAudioManager);
        if (telephonyDevice != null) {
            Log.i(this ,"startCallRecordingTone: playing call recording tone to remote end.");
            /**
             TODO: uncomment this in P release; API dependencies exist which are not met in AOSP.
            mRecordingTonePlayer = MediaPlayer.create(context, R.raw.record);
            mRecordingTonePlayer.setLooping(true);
            mRecordingTonePlayer.setPreferredDevice(telephonyDevice);
            mRecordingTonePlayer.setVolume(0.1f);
            mRecordingTonePlayer.start();
             */
        } else {
            Log.w(this ,"startCallRecordingTone: can't find telephony audio device.");
        }
    }

    /**
     * Attempts to stop the call recording tone if it is playing.
     */
    private void stopCallRecordingTone() {
        if (mRecordingTonePlayer != null) {
            Log.i(this ,"stopCallRecordingTone: stopping call recording tone.");
            mRecordingTonePlayer.stop();
            mRecordingTonePlayer = null;
        }
    }

    /**
     * Finds the the output device of type {@link AudioDeviceInfo#TYPE_TELEPHONY}.  This device is
     * the one on which outgoing audio for SIM calls is played.
     * @param audioManager the audio manage.
     * @return the {@link AudioDeviceInfo} corresponding to the telephony device, or {@code null}
     * if none can be found.
     */
    private AudioDeviceInfo getTelephonyDevice(AudioManager audioManager) {
        AudioDeviceInfo[] deviceList = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
        for (AudioDeviceInfo device: deviceList) {
            if (device.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
                return device;
            }
        }
        return null;
    }

    /**
     * Determines if any of the known calls belongs to a {@link android.telecom.PhoneAccount} with
     * the specified package name.
     * @param packageName The package name.
     * @return {@code true} if a call exists for this package, {@code false} otherwise.
     */
    private boolean hasCallForPackage(String packageName) {
        return mCalls.stream()
                .filter(call -> (call.getTargetPhoneAccount() != null &&
                        call.getTargetPhoneAccount()
                                .getComponentName().getPackageName().equals(packageName)) ||
                        (call.getConnectionManagerPhoneAccount() != null &&
                                call.getConnectionManagerPhoneAccount()
                                        .getComponentName().getPackageName().equals(packageName)))
                .count() >= 1;
    }

    @VisibleForTesting
    public boolean hasCalls() {
        return mCalls.size() > 0;
    }

    @VisibleForTesting
    public boolean isRecording() {
        return mIsRecording;
    }
}
+4 −2
Original line number Diff line number Diff line
@@ -238,6 +238,7 @@ public class CallsManager extends Call.ListenerBase
    private final DtmfLocalTonePlayer mDtmfLocalTonePlayer;
    private final InCallController mInCallController;
    private final CallAudioManager mCallAudioManager;
    private final CallRecordingTonePlayer mCallRecordingTonePlayer;
    private RespondViaSmsManager mRespondViaSmsManager;
    private final Ringer mRinger;
    private final InCallWakeLockController mInCallWakeLockController;
@@ -386,7 +387,8 @@ public class CallsManager extends Call.ListenerBase
                emergencyCallHelper);
        mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
                ringtoneFactory, systemVibrator, mInCallController);

        mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext,
                (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE), mLock);
        mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
                this,new CallAudioModeStateMachine((AudioManager)
                        mContext.getSystemService(Context.AUDIO_SERVICE)),
@@ -394,7 +396,6 @@ public class CallsManager extends Call.ListenerBase

        mConnectionSvrFocusMgr = connectionServiceFocusManagerFactory.create(
                mRequester, Looper.getMainLooper());

        mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock);
        mTtyManager = new TtyManager(context, mWiredHeadsetManager);
        mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
@@ -411,6 +412,7 @@ public class CallsManager extends Call.ListenerBase
        mListeners.add(mPhoneStateBroadcaster);
        mListeners.add(mInCallController);
        mListeners.add(mCallAudioManager);
        mListeners.add(mCallRecordingTonePlayer);
        mListeners.add(missedCallNotifier);
        mListeners.add(mHeadsetMediaButton);
        mListeners.add(mProximitySensorManager);
Loading