blob: 1b522bc19d06fc9cfe6e4c88a48fa80b27d39a87 [file] [log] [blame]
/*
* 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.AudioAttributes;
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.os.Message;
import android.provider.MediaStore;
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 class LoopingTonePlayer extends Handler {
private Runnable mPlayToneRunnable = new Runnable() {
@Override
public void run() {
if (mRecordingTonePlayer != null) {
mRecordingTonePlayer.start();
postDelayed(this, mRepeatInterval);
}
}
};
private MediaPlayer mRecordingTonePlayer = null;
LoopingTonePlayer() {
// We're using the main looper here to avoid creating more threads and risking a thread
// leak. The actual playing of the tone doesn't take up much time on the calling
// thread, so it's okay to use the main thread for this.
super(Looper.getMainLooper());
}
private boolean start() {
if (mRecordingTonePlayer != null) {
Log.w(CallRecordingTonePlayer.this, "Can't start looping tone player more than"
+ " once");
return false;
}
AudioDeviceInfo telephonyDevice = getTelephonyDevice(mAudioManager);
if (telephonyDevice != null) {
mRecordingTonePlayer = MediaPlayer.create(mContext, R.raw.record);
mRecordingTonePlayer.setPreferredDevice(telephonyDevice);
mRecordingTonePlayer.setVolume(0.1f);
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION).build();
mRecordingTonePlayer.setAudioAttributes(audioAttributes);
post(mPlayToneRunnable);
return true;
} else {
Log.w(this ,"startCallRecordingTone: can't find telephony audio device.");
return false;
}
}
private void stop() {
mRecordingTonePlayer.release();
mRecordingTonePlayer = null;
}
}
private final AudioManager mAudioManager;
private final Context mContext;
private final TelecomSystem.SyncRoot mLock;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
private final long mRepeatInterval;
private boolean mIsRecording = false;
private LoopingTonePlayer mLoopingTonePlayer;
private List<Call> mCalls = new ArrayList<>();
public CallRecordingTonePlayer(Context context, AudioManager audioManager,
Timeouts.Adapter timeouts,
TelecomSystem.SyncRoot lock) {
mContext = context;
mAudioManager = audioManager;
mLock = lock;
mRepeatInterval = timeouts.getCallRecordingToneRepeatIntervalMillis(
context.getContentResolver());
}
@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();
}
}
/**
* 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.
*/
private void startCallRecordingTone() {
if (mLoopingTonePlayer != null) {
Log.w(this, "Tone is already playing");
return;
}
mLoopingTonePlayer = new LoopingTonePlayer();
if (!mLoopingTonePlayer.start()) {
mLoopingTonePlayer = null;
}
}
/**
* Attempts to stop the call recording tone if it is playing.
*/
private void stopCallRecordingTone() {
if (mLoopingTonePlayer != null) {
Log.i(this, "stopCallRecordingTone: stopping call recording tone.");
mLoopingTonePlayer.stop();
mLoopingTonePlayer = 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;
}
}