Merge "Don't complete in-call binding future for nonui" into rvc-qpr-dev
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/res/values/strings.xml b/res/values/strings.xml
index 07d6cc9..df08d7c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -75,13 +75,14 @@
         background. This app may be accessing and playing audio over the call.
     </string>
 
-    <!-- Crashed in call service notification label, used when the in call service has cranshed and
+    <!-- Crashed in call service notification label, used when the in call service has crashed and
          the system fall back to use system dialer. [CHAR LIMIT=NONE] -->
-    <string name="notification_crashedInCallService_title">Crashed phone app</string>
+    <string name="notification_incallservice_not_responding_title">
+        <xliff:g id="in_call_service_app_name">%s</xliff:g> stopped responding
+    </string>
     <!-- Body of the notification presented when an in call service crashed. [CHAR LIMIT=NONE] -->
-    <string name="notification_crashedInCallService_body">
-        Your phone app <xliff:g id="in_call_service_app_name">%s</xliff:g> has crashed.
-        You call was continued using the phone app that came with your device.
+    <string name="notification_incallservice_not_responding_body">
+        Your call used the phone app that came with your device
     </string>
 
     <!-- Content description of the call muted notification icon for
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/CallAudioModeStateMachine.java b/src/com/android/server/telecom/CallAudioModeStateMachine.java
index 2c435d7..32598be 100644
--- a/src/com/android/server/telecom/CallAudioModeStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioModeStateMachine.java
@@ -353,7 +353,17 @@
     }
 
     private class RingingFocusState extends BaseState {
+        // Keeps track of whether we're ringing with audio focus or if we've just entered the state
+        // without acquiring focus because of a silent ringtone or something.
+        private boolean mHasFocus = false;
+
         private void tryStartRinging() {
+            if (mHasFocus) {
+                Log.i(LOG_TAG, "RingingFocusState#tryStartRinging -- audio focus previously"
+                        + " acquired and ringtone already playing -- skipping.");
+                return;
+            }
+
             if (mCallAudioManager.startRinging()) {
                 mAudioManager.requestAudioFocusForCall(AudioManager.STREAM_RING,
                         AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
@@ -364,6 +374,7 @@
                 }
                 mCallAudioManager.setCallAudioRouteFocusState(
                         CallAudioRouteStateMachine.RINGING_FOCUS);
+                mHasFocus = true;
             } else {
                 Log.i(LOG_TAG, "RINGING state, try start ringing but not acquiring audio focus");
             }
@@ -380,6 +391,7 @@
         public void exit() {
             // Audio mode and audio stream will be set by the next state.
             mCallAudioManager.stopRinging();
+            mHasFocus = false;
         }
 
         @Override
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 0bfe27a..1c46209 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)),
@@ -2958,6 +2959,9 @@
                     call.setState(CallState.AUDIO_PROCESSING, "active set explicitly and adding");
                     addCall(call);
                 }
+                // Clear mPendingAudioProcessingCall so that future attempts to mark the call as
+                // active (e.g. coming off of hold) don't put the call into audio processing instead
+                mPendingAudioProcessingCall = null;
                 return;
             }
             setCallState(call, CallState.ACTIVE, "active set explicitly");
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 296d0b3..341c02e 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -1324,10 +1324,12 @@
                 (systemPackageName != null && systemPackageName.equals(packageName))
                 ? getInCallServiceComponent(packageName, IN_CALL_SERVICE_TYPE_SYSTEM_UI)
                 : getInCallServiceComponent(packageName, IN_CALL_SERVICE_TYPE_DIALER_UI);
-        if (packageName != null && defaultDialerComponent == null) {
-            // The in call service of default phone app is disabled, send notification.
-            sendCrashedInCallServiceNotification(packageName);
-        }
+        /* TODO: in Android 12 re-enable this an InCallService is required by the dialer role.
+            if (packageName != null && defaultDialerComponent == null) {
+                // The in call service of default phone app is disabled, send notification.
+                sendCrashedInCallServiceNotification(packageName);
+            }
+        */
         return defaultDialerComponent;
     }
 
@@ -1829,12 +1831,11 @@
         builder.setSmallIcon(R.drawable.ic_phone)
                 .setColor(mContext.getResources().getColor(R.color.theme_color))
                 .setContentTitle(
-                        mContext.getText(
-                                R.string.notification_crashedInCallService_title))
+                        mContext.getString(
+                                R.string.notification_incallservice_not_responding_title, appName))
                 .setStyle(new Notification.BigTextStyle()
-                        .bigText(mContext.getString(
-                                R.string.notification_crashedInCallService_body,
-                                appName)));
+                        .bigText(mContext.getText(
+                                R.string.notification_incallservice_not_responding_body)));
         notificationManager.notify(NOTIFICATION_TAG, IN_CALL_SERVICE_NOTIFICATION_ID,
                 builder.build());
     }
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/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
index 51608a0..1e52c5a 100644
--- a/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
+++ b/src/com/android/server/telecom/callfiltering/CallScreeningServiceFilter.java
@@ -34,6 +34,7 @@
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallScreeningServiceHelper;
 import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.LogUtils;
 import com.android.server.telecom.ParcelableCallUtils;
 
 import java.util.concurrent.CompletableFuture;
@@ -70,6 +71,7 @@
                 if (mCall == null || (!mCall.getId().equals(callId))) {
                     Log.w(this, "allowCall, unknown call id: %s", callId);
                 }
+                Log.addEvent(mCall, LogUtils.Events.SCREENING_COMPLETED, mPriorStageResult);
                 mResultFuture.complete(mPriorStageResult);
             } finally {
                 unbindCallScreeningService();
@@ -86,7 +88,7 @@
             Log.startSession("NCSSF.dC");
             try {
                 if (mCall != null && mCall.getId().equals(callId)) {
-                    mResultFuture.complete(new CallFilteringResult.Builder()
+                    CallFilteringResult result = new CallFilteringResult.Builder()
                             .setShouldAllowCall(false)
                             .setShouldReject(shouldReject)
                             .setShouldSilence(false)
@@ -97,7 +99,9 @@
                             .setCallScreeningAppName(mAppName)
                             .setCallScreeningComponentName(componentName.flattenToString())
                             .setContactExists(mPriorStageResult.contactExists)
-                            .build());
+                            .build();
+                    Log.addEvent(mCall, LogUtils.Events.SCREENING_COMPLETED, result);
+                    mResultFuture.complete(result);
                 } else {
                     Log.w(this, "disallowCall, unknown call id: %s", callId);
                     mResultFuture.complete(mPriorStageResult);
@@ -115,14 +119,16 @@
             Log.startSession("NCSSF.sC");
             try {
                 if (mCall != null && mCall.getId().equals(callId)) {
-                    mResultFuture.complete(new CallFilteringResult.Builder()
+                    CallFilteringResult result = new CallFilteringResult.Builder()
                             .setShouldAllowCall(true)
                             .setShouldReject(false)
                             .setShouldSilence(true)
                             .setShouldAddToCallLog(true)
                             .setShouldShowNotification(true)
                             .setContactExists(mPriorStageResult.contactExists)
-                            .build());
+                            .build();
+                    Log.addEvent(mCall, LogUtils.Events.SCREENING_COMPLETED, result);
+                    mResultFuture.complete(result);
                 } else {
                     Log.w(this, "silenceCall, unknown call id: %s", callId);
                     mResultFuture.complete(mPriorStageResult);
@@ -146,14 +152,16 @@
 
             try {
                 if (mCall != null && mCall.getId().equals(callId)) {
-                    mResultFuture.complete(new CallFilteringResult.Builder()
+                    CallFilteringResult result = new CallFilteringResult.Builder()
                             .setShouldAllowCall(true)
                             .setShouldReject(false)
                             .setShouldSilence(false)
                             .setShouldScreenViaAudio(true)
                             .setCallScreeningAppName(mAppName)
                             .setContactExists(mPriorStageResult.contactExists)
-                            .build());
+                            .build();
+                    Log.addEvent(mCall, LogUtils.Events.SCREENING_COMPLETED, result);
+                    mResultFuture.complete(result);
                 } else {
                     Log.w(this, "screenCallFurther, unknown call id: %s", callId);
                     mResultFuture.complete(mPriorStageResult);
@@ -185,6 +193,7 @@
                 Log.e(this, e, "Failed to set the call screening adapter");
                 mResultFuture.complete(mPriorStageResult);
             }
+            Log.addEvent(mCall, LogUtils.Events.SCREENING_BOUND, componentName);
             Log.i(this, "Binding completed.");
         }
 
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
index ca84c4c..4fc4358 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioModeStateMachineTest.java
@@ -180,6 +180,42 @@
                 CallAudioRouteStateMachine.RINGING_FOCUS);
     }
 
+    @SmallTest
+    @Test
+    public void testDoNotRingTwiceWhenHfpConnected() {
+        CallAudioModeStateMachine sm = new CallAudioModeStateMachine(mSystemStateHelper,
+                mAudioManager, mTestThread.getLooper());
+        sm.setCallAudioManager(mCallAudioManager);
+        sm.sendMessage(CallAudioModeStateMachine.ABANDON_FOCUS_FOR_TESTING);
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+
+        resetMocks();
+        when(mCallAudioManager.startRinging()).thenReturn(true);
+
+        sm.sendMessage(CallAudioModeStateMachine.NEW_RINGING_CALL, new Builder()
+                .setHasActiveOrDialingCalls(false)
+                .setHasRingingCalls(true)
+                .setHasHoldingCalls(false)
+                .setIsTonePlaying(false)
+                .setForegroundCallIsVoip(false)
+                .setSession(null)
+                .build());
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+
+        assertEquals(CallAudioModeStateMachine.RING_STATE_NAME, sm.getCurrentStateName());
+
+        verify(mAudioManager).requestAudioFocusForCall(AudioManager.STREAM_RING,
+                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        verify(mAudioManager).setMode(AudioManager.MODE_RINGTONE);
+        verify(mCallAudioManager).setCallAudioRouteFocusState(
+                CallAudioRouteStateMachine.RINGING_FOCUS);
+
+        sm.sendMessage(CallAudioModeStateMachine.RINGER_MODE_CHANGE);
+        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
+
+        // Make sure we don't try and start ringing again.
+        verify(mCallAudioManager, times(1)).startRinging();
+    }
 
     private void resetMocks() {
         clearInvocations(mCallAudioManager, mAudioManager);
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.
      */