Fix concurrency issues with ringtone playback.

CallAudioManager has a number of calls into Ringer.java; this code can
come from CallAudioModeStateMachine which is in turn called into via
Bluetooth callbacks.  Added synchronization using the Telecom lock on these
calls to ensure ringer operations don't have concurrency issues.

Also, in AsyncRingtonePlayer add some null checks to ensure that it is
not possible to run into the NPE reported in the bug, should concurrency
issues persist.

Test: Ran unit tests to confirm no regressions in behavior.
Test: Manual test with bluetooth headset; verify I'm seeing the session
logging for ringtone playing now.
Bug: 135997076

Change-Id: I9a119b15d2853a44d5850d040bd31340d1e34127
Merged-In: I9a119b15d2853a44d5850d040bd31340d1e34127
(cherry picked from commit bbe5d15064ecad396d11bc849805d25fa4cb829a)
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 3745bc5..8f11882 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -26,6 +26,7 @@
 import android.os.HandlerThread;
 import android.os.Message;
 import android.telecom.Log;
+import android.telecom.Logging.Session;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.SomeArgs;
@@ -107,6 +108,7 @@
         args.arg2 = incomingCall;
         args.arg3 = volumeShaperConfig;
         args.arg4 = isVibrationEnabled;
+        args.arg5 = Log.createSubsession();
         postMessage(EVENT_PLAY, true /* shouldCreateHandler */, args);
         return mHapticsFuture;
     }
@@ -173,73 +175,87 @@
         Call incomingCall = (Call) args.arg2;
         VolumeShaper.Configuration volumeShaperConfig = (VolumeShaper.Configuration) args.arg3;
         boolean isVibrationEnabled = (boolean) args.arg4;
+        Session session = (Session) args.arg5;
         args.recycle();
-        // don't bother with any of this if there is an EVENT_STOP waiting.
-        if (mHandler.hasMessages(EVENT_STOP)) {
-            mHapticsFuture.complete(false /* ringtoneHasHaptics */);
-            mHapticsFuture = null;
-            return;
-        }
 
-        // If the Ringtone Uri is EMPTY, then the "None" Ringtone has been selected. Do not play
-        // anything.
-        if(Uri.EMPTY.equals(incomingCall.getRingtone())) {
-            mRingtone = null;
-            mHapticsFuture.complete(false /* ringtoneHasHaptics */);
-            mHapticsFuture = null;
-            return;
-        }
-
-        ThreadUtil.checkNotOnMainThread();
-        Log.i(this, "handlePlay: Play ringtone.");
-
-        if (mRingtone == null) {
-            mRingtone = factory.getRingtone(incomingCall, volumeShaperConfig);
-            if (mRingtone == null) {
-                Uri ringtoneUri = incomingCall.getRingtone();
-                String ringtoneUriString = (ringtoneUri == null) ? "null" :
-                        ringtoneUri.toSafeString();
-                Log.addEvent(null, LogUtils.Events.ERROR_LOG, "Failed to get ringtone from " +
-                        "factory. Skipping ringing. Uri was: " + ringtoneUriString);
-                mHapticsFuture.complete(false /* ringtoneHasHaptics */);
-                mHapticsFuture = null;
-                return;
-            }
-
-            // With the ringtone to play now known, we can determine if it has haptic channels or
-            // not; we will complete the haptics future so the default vibration code in Ringer
-            // can know whether to trigger the vibrator.
-            if (mHapticsFuture != null && !mHapticsFuture.isDone()) {
-                boolean hasHaptics = factory.hasHapticChannels(mRingtone);
-
-                Log.i(this, "handlePlay: hasHaptics=%b, isVibrationEnabled=%b", hasHaptics,
-                        isVibrationEnabled);
-                if (hasHaptics) {
-                    AudioAttributes attributes = mRingtone.getAudioAttributes();
-                    Log.d(this, "handlePlay: %s haptic channel",
-                            (isVibrationEnabled ? "unmuting" : "muting"));
-                    mRingtone.setAudioAttributes(
-                            new AudioAttributes.Builder(attributes)
-                                    .setHapticChannelsMuted(!isVibrationEnabled)
-                                    .build());
+        Log.continueSession(session, "ARP.hP");
+        try {
+            // don't bother with any of this if there is an EVENT_STOP waiting.
+            if (mHandler.hasMessages(EVENT_STOP)) {
+                if (mHapticsFuture != null) {
+                    mHapticsFuture.complete(false /* ringtoneHasHaptics */);
+                    mHapticsFuture = null;
                 }
-                mHapticsFuture.complete(hasHaptics);
-                mHapticsFuture = null;
-            }
-        }
-
-        if (mShouldPauseBetweenRepeat) {
-            // We're trying to pause between repeats, so the ringtone will not intentionally loop.
-            // Instead, we'll use a handler message to perform repeats.
-            handleRepeat();
-        } else {
-            mRingtone.setLooping(true);
-            if (mRingtone.isPlaying()) {
-                Log.d(this, "Ringtone already playing.");
                 return;
             }
-            mRingtone.play();
-            Log.i(this, "Play ringtone, looping.");
+
+            // If the Ringtone Uri is EMPTY, then the "None" Ringtone has been selected. Do not play
+            // anything.
+            if (Uri.EMPTY.equals(incomingCall.getRingtone())) {
+                mRingtone = null;
+                if (mHapticsFuture != null) {
+                    mHapticsFuture.complete(false /* ringtoneHasHaptics */);
+                    mHapticsFuture = null;
+                }
+
+                return;
+            }
+
+            ThreadUtil.checkNotOnMainThread();
+            Log.i(this, "handlePlay: Play ringtone.");
+
+            if (mRingtone == null) {
+                mRingtone = factory.getRingtone(incomingCall, volumeShaperConfig);
+                if (mRingtone == null) {
+                    Uri ringtoneUri = incomingCall.getRingtone();
+                    String ringtoneUriString = (ringtoneUri == null) ? "null" :
+                            ringtoneUri.toSafeString();
+                    Log.addEvent(null, LogUtils.Events.ERROR_LOG, "Failed to get ringtone from " +
+                            "factory. Skipping ringing. Uri was: " + ringtoneUriString);
+                    if (mHapticsFuture != null) {
+                        mHapticsFuture.complete(false /* ringtoneHasHaptics */);
+                        mHapticsFuture = null;
+                    }
+                    return;
+                }
+
+                // With the ringtone to play now known, we can determine if it has haptic channels or
+                // not; we will complete the haptics future so the default vibration code in Ringer
+                // can know whether to trigger the vibrator.
+                if (mHapticsFuture != null && !mHapticsFuture.isDone()) {
+                    boolean hasHaptics = factory.hasHapticChannels(mRingtone);
+
+                    Log.i(this, "handlePlay: hasHaptics=%b, isVibrationEnabled=%b", hasHaptics,
+                            isVibrationEnabled);
+                    if (hasHaptics) {
+                        AudioAttributes attributes = mRingtone.getAudioAttributes();
+                        Log.d(this, "handlePlay: %s haptic channel",
+                                (isVibrationEnabled ? "unmuting" : "muting"));
+                        mRingtone.setAudioAttributes(
+                                new AudioAttributes.Builder(attributes)
+                                        .setHapticChannelsMuted(!isVibrationEnabled)
+                                        .build());
+                    }
+                    mHapticsFuture.complete(hasHaptics);
+                    mHapticsFuture = null;
+                }
+            }
+
+            if (mShouldPauseBetweenRepeat) {
+                // We're trying to pause between repeats, so the ringtone will not intentionally loop.
+                // Instead, we'll use a handler message to perform repeats.
+                handleRepeat();
+            } else {
+                mRingtone.setLooping(true);
+                if (mRingtone.isPlaying()) {
+                    Log.d(this, "Ringtone already playing.");
+                    return;
+                }
+                mRingtone.play();
+                Log.i(this, "Play ringtone, looping.");
+            }
+        } finally {
+            Log.cancelSubsession(session);
         }
     }
 
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 7e4c3ba..3345fa4 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -431,35 +431,45 @@
     }
 
     void silenceRingers() {
-        for (Call call : mRingingCalls) {
-            call.silence();
-        }
+        synchronized (mCallsManager.getLock()) {
+            for (Call call : mRingingCalls) {
+                call.silence();
+            }
 
-        mRinger.stopRinging();
-        mRinger.stopCallWaiting();
+            mRinger.stopRinging();
+            mRinger.stopCallWaiting();
+        }
     }
 
     @VisibleForTesting
     public boolean startRinging() {
-        return mRinger.startRinging(mForegroundCall,
-                mCallAudioRouteStateMachine.isHfpDeviceAvailable());
+        synchronized (mCallsManager.getLock()) {
+            return mRinger.startRinging(mForegroundCall,
+                    mCallAudioRouteStateMachine.isHfpDeviceAvailable());
+        }
     }
 
     @VisibleForTesting
     public void startCallWaiting(String reason) {
-        if (mRingingCalls.size() == 1) {
-            mRinger.startCallWaiting(mRingingCalls.iterator().next(), reason);
+        synchronized (mCallsManager.getLock()) {
+            if (mRingingCalls.size() == 1) {
+                mRinger.startCallWaiting(mRingingCalls.iterator().next(), reason);
+            }
         }
     }
 
     @VisibleForTesting
     public void stopRinging() {
-        mRinger.stopRinging();
+        synchronized (mCallsManager.getLock()) {
+            mRinger.stopRinging();
+        }
     }
 
     @VisibleForTesting
     public void stopCallWaiting() {
-        mRinger.stopCallWaiting();
+        synchronized (mCallsManager.getLock()) {
+            mRinger.stopCallWaiting();
+        }
     }
 
     @VisibleForTesting
@@ -801,10 +811,12 @@
     private void maybeStopRingingAndCallWaitingForAnsweredOrRejectedCall(Call call) {
         // Check to see if the call being answered/rejected is the only ringing call, since this
         // will be called before the connection service acknowledges the state change.
-        if (mRingingCalls.size() == 0 ||
-                (mRingingCalls.size() == 1 && call == mRingingCalls.iterator().next())) {
-            mRinger.stopRinging();
-            mRinger.stopCallWaiting();
+        synchronized (mCallsManager.getLock()) {
+            if (mRingingCalls.size() == 0 ||
+                    (mRingingCalls.size() == 1 && call == mRingingCalls.iterator().next())) {
+                mRinger.stopRinging();
+                mRinger.stopCallWaiting();
+            }
         }
     }
 
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 2909b72..1dceb01 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -326,7 +326,7 @@
         }
 
         if (hapticsFuture != null) {
-           mVibrateFuture = hapticsFuture.thenAccept(isUsingAudioCoupledHaptics -> {
+            mVibrateFuture = hapticsFuture.thenAccept(isUsingAudioCoupledHaptics -> {
                 if (!isUsingAudioCoupledHaptics || !mIsHapticPlaybackSupportedByDevice) {
                     Log.i(this, "startRinging: fileHasHaptics=%b, hapticsSupported=%b",
                             isUsingAudioCoupledHaptics, mIsHapticPlaybackSupportedByDevice);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
index 01add22..3f70453 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioManagerTest.java
@@ -31,6 +31,7 @@
 import com.android.server.telecom.InCallTonePlayer;
 import com.android.server.telecom.RingbackPlayer;
 import com.android.server.telecom.Ringer;
+import com.android.server.telecom.TelecomSystem;
 import com.android.server.telecom.bluetooth.BluetoothStateReceiver;
 
 import org.junit.Before;
@@ -66,6 +67,7 @@
     @Mock private RingbackPlayer mRingbackPlayer;
     @Mock private DtmfLocalTonePlayer mDtmfLocalTonePlayer;
     @Mock private BluetoothStateReceiver mBluetoothStateReceiver;
+    @Mock private TelecomSystem.SyncRoot mLock;
 
     private CallAudioManager mCallAudioManager;
 
@@ -81,6 +83,7 @@
             }).when(mockInCallTonePlayer).startTone();
             return mockInCallTonePlayer;
         }).when(mPlayerFactory).createPlayer(anyInt());
+        when(mCallsManager.getLock()).thenReturn(mLock);
         mCallAudioManager = new CallAudioManager(
                 mCallAudioRouteStateMachine,
                 mCallsManager,