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.
      */