Improve implementation of call recording tone
Instead of using a 15-second audio file to play the call recording tone,
clip out the silence in the audio and play it once every 15 seconds.
Also start using mockito-target-extended in Telecom's unit tests so we
can mock out MediaPlayer.
Test: atest CallRecordingTonePlayerTest
Bug: 157948346
Change-Id: I7c3429733bbc7d48e29be12a72c7f15b38699ae6
diff --git a/Android.bp b/Android.bp
index 94a6f9d..0d89b00 100644
--- a/Android.bp
+++ b/Android.bp
@@ -41,7 +41,7 @@
static_libs: [
"android-ex-camera2",
"guava",
- "mockito-target-inline",
+ "mockito-target-extended",
"androidx.test.rules",
"platform-test-annotations",
"androidx.legacy_legacy-support-core-ui",
@@ -70,7 +70,10 @@
"android.test.runner",
],
- jni_libs: ["libdexmakerjvmtiagent"],
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
aaptflags: [
"--auto-add-overlay",
diff --git a/res/raw/record.ogg b/res/raw/record.ogg
index a023e6d..732b42f 100644
--- a/res/raw/record.ogg
+++ b/res/raw/record.ogg
Binary files differ
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 491aed3..df6322c 100755
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -2569,7 +2569,8 @@
}
}
- boolean isActive() {
+ @VisibleForTesting
+ public boolean isActive() {
return mState == CallState.ACTIVE;
}
diff --git a/src/com/android/server/telecom/CallRecordingTonePlayer.java b/src/com/android/server/telecom/CallRecordingTonePlayer.java
index 999148c..1b522bc 100644
--- a/src/com/android/server/telecom/CallRecordingTonePlayer.java
+++ b/src/com/android/server/telecom/CallRecordingTonePlayer.java
@@ -24,6 +24,7 @@
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Looper;
+import android.os.Message;
import android.provider.MediaStore;
import android.telecom.Log;
@@ -61,19 +62,71 @@
}
};
+ 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 MediaPlayer mRecordingTonePlayer = null;
+ 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
@@ -163,7 +216,7 @@
*/
private void maybeStartCallAudioTone() {
if (mIsRecording && hasActiveCall()) {
- startCallRecordingTone(mContext);
+ startCallRecordingTone();
}
}
@@ -231,26 +284,15 @@
* 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) {
+ private void startCallRecordingTone() {
+ if (mLoopingTonePlayer != null) {
+ Log.w(this, "Tone is already playing");
return;
}
- AudioDeviceInfo telephonyDevice = getTelephonyDevice(mAudioManager);
- if (telephonyDevice != null) {
- Log.i(this ,"startCallRecordingTone: playing call recording tone to remote end.");
- mRecordingTonePlayer = MediaPlayer.create(context, R.raw.record);
- mRecordingTonePlayer.setLooping(true);
- mRecordingTonePlayer.setPreferredDevice(telephonyDevice);
- mRecordingTonePlayer.setVolume(0.1f);
- AudioAttributes audioAttributes = new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION).build();
- mRecordingTonePlayer.setAudioAttributes(audioAttributes);
- mRecordingTonePlayer.start();
- } else {
- Log.w(this ,"startCallRecordingTone: can't find telephony audio device.");
+ mLoopingTonePlayer = new LoopingTonePlayer();
+ if (!mLoopingTonePlayer.start()) {
+ mLoopingTonePlayer = null;
}
}
@@ -258,10 +300,10 @@
* 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;
+ if (mLoopingTonePlayer != null) {
+ Log.i(this, "stopCallRecordingTone: stopping call recording tone.");
+ mLoopingTonePlayer.stop();
+ mLoopingTonePlayer = null;
}
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 8fac4db..2211986 100755
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -543,7 +543,8 @@
mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer,
ringtoneFactory, systemVibrator,
new Ringer.VibrationEffectProxy(), mInCallController);
- mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager, mLock);
+ mCallRecordingTonePlayer = new CallRecordingTonePlayer(mContext, audioManager,
+ mTimeoutsAdapter, mLock);
mCallAudioManager = new CallAudioManager(callAudioRouteStateMachine,
this, callAudioModeStateMachineFactory.create(systemStateHelper,
(AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)),
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 37f9363..a701b88 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -29,7 +29,8 @@
*/
public final class Timeouts {
public static class Adapter {
- public Adapter() { }
+ public Adapter() {
+ }
public long getCallScreeningTimeoutMillis(ContentResolver cr) {
return Timeouts.getCallScreeningTimeoutMillis(cr);
@@ -62,20 +63,25 @@
public long getPhoneAccountSuggestionServiceTimeout(ContentResolver cr) {
return Timeouts.getPhoneAccountSuggestionServiceTimeout(cr);
}
+
+ public long getCallRecordingToneRepeatIntervalMillis(ContentResolver cr) {
+ return Timeouts.getCallRecordingToneRepeatIntervalMillis(cr);
+ }
}
/** A prefix to use for all keys so to not clobber the global namespace. */
private static final String PREFIX = "telecom.";
- private Timeouts() {}
+ private Timeouts() {
+ }
/**
* Returns the timeout value from Settings or the default value if it hasn't been changed. This
* method is safe to call from any thread, including the UI thread.
*
* @param contentResolver The content resolved.
- * @param key Settings key to retrieve.
- * @param defaultValue Default value, in milliseconds.
+ * @param key Settings key to retrieve.
+ * @param defaultValue Default value, in milliseconds.
* @return The timeout value from Settings or the default value if it hasn't been changed.
*/
private static long get(ContentResolver contentResolver, String key, long defaultValue) {
@@ -176,8 +182,8 @@
* as potential emergency callbacks.
*/
public static long getEmergencyCallbackWindowMillis(ContentResolver contentResolver) {
- return get(contentResolver, "emergency_callback_window_millis",
- TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
+ return get(contentResolver, "emergency_callback_window_millis",
+ TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES));
}
/**
@@ -187,7 +193,7 @@
*/
public static long getUserDefinedCallRedirectionTimeoutMillis(ContentResolver contentResolver) {
return get(contentResolver, "user_defined_call_redirection_timeout",
- 5000L /* 5 seconds */);
+ 5000L /* 5 seconds */);
}
/**
@@ -198,4 +204,11 @@
public static long getCarrierCallRedirectionTimeoutMillis(ContentResolver contentResolver) {
return get(contentResolver, "carrier_call_redirection_timeout", 5000L /* 5 seconds */);
}
+
+ /**
+ * Returns the number of milliseconds between two plays of the call recording tone.
+ */
+ public static long getCallRecordingToneRepeatIntervalMillis(ContentResolver contentResolver) {
+ return get(contentResolver, "call_recording_tone_repeat_interval", 15000L /* 15 seconds */);
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java b/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java
index 5151d4c..b5c6468 100644
--- a/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallRecordingTonePlayerTest.java
@@ -22,22 +22,37 @@
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecordingConfiguration;
+import android.media.MediaPlayer;
import android.media.MediaRecorder;
+import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.telecom.PhoneAccountHandle;
import android.test.suitebuilder.annotation.MediumTest;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallRecordingTonePlayer;
+import com.android.server.telecom.CallState;
import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.Timeouts;
import org.junit.After;
import org.junit.Before;
@@ -48,6 +63,7 @@
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
import java.util.ArrayList;
import java.util.List;
@@ -62,22 +78,29 @@
private static final String PHONE_ACCOUNT_CLASS = "MyFancyConnectionService";
private static final String PHONE_ACCOUNT_ID = "1";
private static final String RECORDING_APP_PACKAGE = "com.recording.app";
+ private static final long TEST_RECORDING_TONE_INTERVAL = 300L;
private static final PhoneAccountHandle TEST_PHONE_ACCOUNT = new PhoneAccountHandle(
new ComponentName(PHONE_ACCOUNT_PACKAGE, PHONE_ACCOUNT_CLASS), PHONE_ACCOUNT_ID);
private CallRecordingTonePlayer mCallRecordingTonePlayer;
- private TelecomSystem.SyncRoot mSyncRoot = new TelecomSystem.SyncRoot() { };
- @Mock private AudioManager mAudioManager;
+ private TelecomSystem.SyncRoot mSyncRoot = new TelecomSystem.SyncRoot() {
+ };
+ @Mock
+ private AudioManager mAudioManager;
+ @Mock
+ private Timeouts.Adapter mTimeouts;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
MockitoAnnotations.initMocks(this);
+ when(mTimeouts.getCallRecordingToneRepeatIntervalMillis(nullable(ContentResolver.class)))
+ .thenReturn(500L);
mCallRecordingTonePlayer = new CallRecordingTonePlayer(
mComponentContextFixture.getTestDouble().getApplicationContext(),
- mAudioManager, mSyncRoot);
+ mAudioManager, mTimeouts, mSyncRoot);
when(mAudioManager.getActiveRecordingConfigurations()).thenReturn(null);
}
@@ -87,6 +110,45 @@
super.tearDown();
}
+ @MediumTest
+ @Test
+ public void testToneLooping() throws Exception {
+ MediaPlayer mockMediaPlayer = mock(MediaPlayer.class);
+ MockitoSession session = ExtendedMockito.mockitoSession().mockStatic(MediaPlayer.class)
+ .startMocking();
+ ExtendedMockito.doReturn(mockMediaPlayer).when(() ->
+ MediaPlayer.create(nullable(Context.class), anyInt()));
+
+ when(mAudioManager.getActiveRecordingConfigurations()).thenReturn(
+ getAudioRecordingConfig(RECORDING_APP_PACKAGE));
+
+ AudioDeviceInfo mockAudioDeviceInfo = mock(AudioDeviceInfo.class);
+ when(mockAudioDeviceInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_TELEPHONY);
+ when(mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
+ .thenReturn(new AudioDeviceInfo[] { mockAudioDeviceInfo });
+
+ Call call = addValidCall();
+ when(call.isActive()).thenReturn(true);
+ mCallRecordingTonePlayer.onCallStateChanged(call, CallState.NEW, CallState.ACTIVE);
+
+ waitForHandlerAction(Handler.getMain(), TEST_TIMEOUT);
+ verify(mockMediaPlayer).start();
+
+ // Sleep for 4x the interval, then make sure it played more. No exact count,
+ // since timing can be tricky in tests.
+ Thread.sleep(TEST_RECORDING_TONE_INTERVAL * 4);
+ verify(mockMediaPlayer, atLeast(2)).start();
+ reset(mockMediaPlayer);
+
+ // Remove the call and verify that we're not starting the tone anymore.
+ mCallRecordingTonePlayer.onCallRemoved(call);
+ Thread.sleep(TEST_RECORDING_TONE_INTERVAL * 3 + 50);
+ verify(mockMediaPlayer, never()).start();
+ verify(mockMediaPlayer).release();
+
+ session.finishMocking();
+ }
+
/**
* Ensures that child calls are not tracked.
*/