Loading src/com/android/server/telecom/InCallTonePlayer.java +68 −67 Original line number Original line Diff line number Diff line Loading @@ -33,6 +33,7 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** /** * Play a call-related tone (ringback, busy signal, etc.) either through ToneGenerator, or using a * Play a call-related tone (ringback, busy signal, etc.) either through ToneGenerator, or using a Loading Loading @@ -186,7 +187,7 @@ public class InCallTonePlayer extends Thread { * when we need focus and when it can be release. This should only be manipulated from the main * when we need focus and when it can be release. This should only be manipulated from the main * thread. * thread. */ */ private static int sTonesPlaying = 0; private static AtomicInteger sTonesPlaying = new AtomicInteger(0); private final CallAudioManager mCallAudioManager; private final CallAudioManager mCallAudioManager; private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter; private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter; Loading @@ -212,6 +213,12 @@ public class InCallTonePlayer extends Thread { private final MediaPlayerFactory mMediaPlayerFactory; private final MediaPlayerFactory mMediaPlayerFactory; private final AudioManagerAdapter mAudioManagerAdapter; private final AudioManagerAdapter mAudioManagerAdapter; /** * Latch used for awaiting on playback, which may be interrupted if the tone is stopped from * outside the playback. */ private final CountDownLatch mPlaybackLatch = new CountDownLatch(1); /** /** * Initializes the tone player. Private; use the {@link Factory} to create tone players. * Initializes the tone player. Private; use the {@link Factory} to create tone players. * * Loading Loading @@ -388,26 +395,21 @@ public class InCallTonePlayer extends Thread { } } Log.i(this, "playToneGeneratorTone: toneType=%d", toneType); Log.i(this, "playToneGeneratorTone: toneType=%d", toneType); // TODO: Certain CDMA tones need to check the ringer-volume state before // playing. See CallNotifier.InCallTonePlayer. // TODO: Some tones play through the end of a call so we need to inform // CallAudioManager that we want focus the same way that Ringer does. synchronized (this) { if (mState != STATE_STOPPED) { mState = STATE_ON; mState = STATE_ON; toneGenerator.startTone(toneType); toneGenerator.startTone(toneType); try { try { Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId, Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId, toneLengthMillis + TIMEOUT_BUFFER_MILLIS); toneLengthMillis + TIMEOUT_BUFFER_MILLIS); wait(toneLengthMillis + TIMEOUT_BUFFER_MILLIS); if (mPlaybackLatch.await(toneLengthMillis + TIMEOUT_BUFFER_MILLIS, } catch (InterruptedException e) { TimeUnit.MILLISECONDS)) { Log.w(this, "wait interrupted", e); Log.i(this, "playToneGeneratorTone: tone playback stopped."); } } } } catch (InterruptedException e) { Log.w(this, "playToneGeneratorTone: wait interrupted", e); } } mState = STATE_OFF; // Redundant; don't want anyone re-using at this point. mState = STATE_STOPPED; } finally { } finally { if (toneGenerator != null) { if (toneGenerator != null) { toneGenerator.release(); toneGenerator.release(); Loading @@ -421,10 +423,7 @@ public class InCallTonePlayer extends Thread { * @param toneResourceId The resource ID of the tone to play. * @param toneResourceId The resource ID of the tone to play. */ */ private void playMediaTone(int stream, int toneResourceId) { private void playMediaTone(int stream, int toneResourceId) { synchronized (this) { if (mState != STATE_STOPPED) { mState = STATE_ON; mState = STATE_ON; } Log.i(this, "playMediaTone: toneResourceId=%d", toneResourceId); Log.i(this, "playMediaTone: toneResourceId=%d", toneResourceId); AudioAttributes attributes = new AudioAttributes.Builder() AudioAttributes attributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) Loading @@ -434,32 +433,32 @@ public class InCallTonePlayer extends Thread { mToneMediaPlayer = mMediaPlayerFactory.get(toneResourceId, attributes); mToneMediaPlayer = mMediaPlayerFactory.get(toneResourceId, attributes); mToneMediaPlayer.setLooping(false); mToneMediaPlayer.setLooping(false); int durationMillis = mToneMediaPlayer.getDuration(); int durationMillis = mToneMediaPlayer.getDuration(); final CountDownLatch toneLatch = new CountDownLatch(1); mToneMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { mToneMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override @Override public void onCompletion(MediaPlayer mp) { public void onCompletion(MediaPlayer mp) { Log.i(InCallTonePlayer.this, "playMediaTone: toneResourceId=%d completed.", Log.i(InCallTonePlayer.this, "playMediaTone: toneResourceId=%d completed.", toneResourceId); toneResourceId); synchronized (InCallTonePlayer.this) { mPlaybackLatch.countDown(); mState = STATE_OFF; } mToneMediaPlayer.release(); mToneMediaPlayer = null; toneLatch.countDown(); } } }); }); mToneMediaPlayer.start(); try { try { mToneMediaPlayer.start(); // Wait for the tone to stop playing; timeout at 2x the length of the file just to // Wait for the tone to stop playing; timeout at 2x the length of the file just to // be on the safe side. // be on the safe side. Playback can also be stopped via stopTone(). toneLatch.await(durationMillis * 2, TimeUnit.MILLISECONDS); if (mPlaybackLatch.await(durationMillis * 2, TimeUnit.MILLISECONDS)) { Log.i(this, "playMediaTone: tone playback stopped."); } } catch (InterruptedException ie) { } catch (InterruptedException ie) { Log.e(this, ie, "playMediaTone: tone playback interrupted."); Log.e(this, ie, "playMediaTone: tone playback interrupted."); } finally { // Redundant; don't want anyone re-using at this point. mState = STATE_STOPPED; mToneMediaPlayer.release(); mToneMediaPlayer = null; } } } } } @VisibleForTesting @VisibleForTesting public boolean startTone() { public boolean startTone() { // Skip playing the end call tone if the volume is silenced. // Skip playing the end call tone if the volume is silenced. Loading @@ -468,8 +467,12 @@ public class InCallTonePlayer extends Thread { return false; return false; } } sTonesPlaying++; // Tone already done; don't allow re-used if (sTonesPlaying == 1) { if (mState == STATE_STOPPED) { return false; } if (sTonesPlaying.incrementAndGet() == 1) { mCallAudioManager.setIsTonePlaying(true); mCallAudioManager.setIsTonePlaying(true); } } Loading @@ -494,18 +497,17 @@ public class InCallTonePlayer extends Thread { */ */ @VisibleForTesting @VisibleForTesting public void stopTone() { public void stopTone() { synchronized (this) { if (mState == STATE_ON) { if (mState == STATE_ON) { Log.d(this, "Stopping the tone %d.", mToneId); Log.i(this, "stopTone: Stopping the tone %d.", mToneId); notify(); // Notify the playback to end early. mPlaybackLatch.countDown(); } } mState = STATE_STOPPED; mState = STATE_STOPPED; } } } @VisibleForTesting @VisibleForTesting public void cleanup() { public void cleanup() { sTonesPlaying = 0; sTonesPlaying.set(0); } } private void cleanUpTonePlayer() { private void cleanUpTonePlayer() { Loading @@ -514,12 +516,11 @@ public class InCallTonePlayer extends Thread { mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) { mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) { @Override @Override public void loggedRun() { public void loggedRun() { if (sTonesPlaying == 0) { int newToneCount = sTonesPlaying.updateAndGet( t -> Math.min(0, t--)); Log.wtf(InCallTonePlayer.this, "cleanUpTonePlayer(): Over-releasing focus for tone player."); if (newToneCount == 0) { } else if (--sTonesPlaying == 0) { Log.i(InCallTonePlayer.this, Log.i(InCallTonePlayer.this, "cleanUpTonePlayer(): tonesPlaying=%d, tone completed", sTonesPlaying); "cleanUpTonePlayer(): tonesPlaying=%d, tone completed", newToneCount); if (mCallAudioManager != null) { if (mCallAudioManager != null) { mCallAudioManager.setIsTonePlaying(false); mCallAudioManager.setIsTonePlaying(false); } else { } else { Loading @@ -528,7 +529,7 @@ public class InCallTonePlayer extends Thread { } } } else { } else { Log.i(InCallTonePlayer.this, Log.i(InCallTonePlayer.this, "cleanUpTonePlayer(): tonesPlaying=%d; still playing", sTonesPlaying); "cleanUpTonePlayer(): tonesPlaying=%d; still playing", newToneCount); } } } } }.prepare()); }.prepare()); Loading tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java +37 −3 Original line number Original line Diff line number Diff line Loading @@ -87,7 +87,7 @@ public class InCallTonePlayerTest extends TelecomTestCase { @Override @Override public int getDuration() { public int getDuration() { return 0; return 1000; } } }; }; Loading Loading @@ -134,7 +134,41 @@ public class InCallTonePlayerTest extends TelecomTestCase { verify(mMediaPlayerFactory, never()).get(anyInt(), any()); verify(mMediaPlayerFactory, never()).get(anyInt(), any()); } } @FlakyTest @SmallTest @Test public void testInterruptMediaTone() { when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true); assertTrue(mInCallTonePlayer.startTone()); // Verify we did play a tone. verify(mMediaPlayerFactory, timeout(5000)).get(anyInt(), any()); verify(mCallAudioManager).setIsTonePlaying(eq(true)); mInCallTonePlayer.stopTone(); // Timeouts due to threads! verify(mCallAudioManager, timeout(5000)).setIsTonePlaying(eq(false)); // Correctness check: ensure we can't start the tone again. assertFalse(mInCallTonePlayer.startTone()); } @SmallTest @Test public void testInterruptToneGenerator() { mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK); when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true); assertTrue(mInCallTonePlayer.startTone()); verify(mToneGenerator, timeout(5000)).startTone(anyInt()); verify(mCallAudioManager).setIsTonePlaying(eq(true)); mInCallTonePlayer.stopTone(); // Timeouts due to threads! verify(mCallAudioManager, timeout(5000)).setIsTonePlaying(eq(false)); verify(mToneGenerator, timeout(5000)).release(); // Correctness check: ensure we can't start the tone again. assertFalse(mInCallTonePlayer.startTone()); } @SmallTest @SmallTest @Test @Test public void testEndCallToneWhenNotSilenced() { public void testEndCallToneWhenNotSilenced() { Loading @@ -143,6 +177,6 @@ public class InCallTonePlayerTest extends TelecomTestCase { // Verify we did play a tone. // Verify we did play a tone. verify(mMediaPlayerFactory, timeout(5000)).get(anyInt(), any()); verify(mMediaPlayerFactory, timeout(5000)).get(anyInt(), any()); verify(mCallAudioManager).setIsTonePlaying(eq(true)); verify(mCallAudioManager, timeout(5000)).setIsTonePlaying(eq(true)); } } } } Loading
src/com/android/server/telecom/InCallTonePlayer.java +68 −67 Original line number Original line Diff line number Diff line Loading @@ -33,6 +33,7 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** /** * Play a call-related tone (ringback, busy signal, etc.) either through ToneGenerator, or using a * Play a call-related tone (ringback, busy signal, etc.) either through ToneGenerator, or using a Loading Loading @@ -186,7 +187,7 @@ public class InCallTonePlayer extends Thread { * when we need focus and when it can be release. This should only be manipulated from the main * when we need focus and when it can be release. This should only be manipulated from the main * thread. * thread. */ */ private static int sTonesPlaying = 0; private static AtomicInteger sTonesPlaying = new AtomicInteger(0); private final CallAudioManager mCallAudioManager; private final CallAudioManager mCallAudioManager; private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter; private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter; Loading @@ -212,6 +213,12 @@ public class InCallTonePlayer extends Thread { private final MediaPlayerFactory mMediaPlayerFactory; private final MediaPlayerFactory mMediaPlayerFactory; private final AudioManagerAdapter mAudioManagerAdapter; private final AudioManagerAdapter mAudioManagerAdapter; /** * Latch used for awaiting on playback, which may be interrupted if the tone is stopped from * outside the playback. */ private final CountDownLatch mPlaybackLatch = new CountDownLatch(1); /** /** * Initializes the tone player. Private; use the {@link Factory} to create tone players. * Initializes the tone player. Private; use the {@link Factory} to create tone players. * * Loading Loading @@ -388,26 +395,21 @@ public class InCallTonePlayer extends Thread { } } Log.i(this, "playToneGeneratorTone: toneType=%d", toneType); Log.i(this, "playToneGeneratorTone: toneType=%d", toneType); // TODO: Certain CDMA tones need to check the ringer-volume state before // playing. See CallNotifier.InCallTonePlayer. // TODO: Some tones play through the end of a call so we need to inform // CallAudioManager that we want focus the same way that Ringer does. synchronized (this) { if (mState != STATE_STOPPED) { mState = STATE_ON; mState = STATE_ON; toneGenerator.startTone(toneType); toneGenerator.startTone(toneType); try { try { Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId, Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId, toneLengthMillis + TIMEOUT_BUFFER_MILLIS); toneLengthMillis + TIMEOUT_BUFFER_MILLIS); wait(toneLengthMillis + TIMEOUT_BUFFER_MILLIS); if (mPlaybackLatch.await(toneLengthMillis + TIMEOUT_BUFFER_MILLIS, } catch (InterruptedException e) { TimeUnit.MILLISECONDS)) { Log.w(this, "wait interrupted", e); Log.i(this, "playToneGeneratorTone: tone playback stopped."); } } } } catch (InterruptedException e) { Log.w(this, "playToneGeneratorTone: wait interrupted", e); } } mState = STATE_OFF; // Redundant; don't want anyone re-using at this point. mState = STATE_STOPPED; } finally { } finally { if (toneGenerator != null) { if (toneGenerator != null) { toneGenerator.release(); toneGenerator.release(); Loading @@ -421,10 +423,7 @@ public class InCallTonePlayer extends Thread { * @param toneResourceId The resource ID of the tone to play. * @param toneResourceId The resource ID of the tone to play. */ */ private void playMediaTone(int stream, int toneResourceId) { private void playMediaTone(int stream, int toneResourceId) { synchronized (this) { if (mState != STATE_STOPPED) { mState = STATE_ON; mState = STATE_ON; } Log.i(this, "playMediaTone: toneResourceId=%d", toneResourceId); Log.i(this, "playMediaTone: toneResourceId=%d", toneResourceId); AudioAttributes attributes = new AudioAttributes.Builder() AudioAttributes attributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) Loading @@ -434,32 +433,32 @@ public class InCallTonePlayer extends Thread { mToneMediaPlayer = mMediaPlayerFactory.get(toneResourceId, attributes); mToneMediaPlayer = mMediaPlayerFactory.get(toneResourceId, attributes); mToneMediaPlayer.setLooping(false); mToneMediaPlayer.setLooping(false); int durationMillis = mToneMediaPlayer.getDuration(); int durationMillis = mToneMediaPlayer.getDuration(); final CountDownLatch toneLatch = new CountDownLatch(1); mToneMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { mToneMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override @Override public void onCompletion(MediaPlayer mp) { public void onCompletion(MediaPlayer mp) { Log.i(InCallTonePlayer.this, "playMediaTone: toneResourceId=%d completed.", Log.i(InCallTonePlayer.this, "playMediaTone: toneResourceId=%d completed.", toneResourceId); toneResourceId); synchronized (InCallTonePlayer.this) { mPlaybackLatch.countDown(); mState = STATE_OFF; } mToneMediaPlayer.release(); mToneMediaPlayer = null; toneLatch.countDown(); } } }); }); mToneMediaPlayer.start(); try { try { mToneMediaPlayer.start(); // Wait for the tone to stop playing; timeout at 2x the length of the file just to // Wait for the tone to stop playing; timeout at 2x the length of the file just to // be on the safe side. // be on the safe side. Playback can also be stopped via stopTone(). toneLatch.await(durationMillis * 2, TimeUnit.MILLISECONDS); if (mPlaybackLatch.await(durationMillis * 2, TimeUnit.MILLISECONDS)) { Log.i(this, "playMediaTone: tone playback stopped."); } } catch (InterruptedException ie) { } catch (InterruptedException ie) { Log.e(this, ie, "playMediaTone: tone playback interrupted."); Log.e(this, ie, "playMediaTone: tone playback interrupted."); } finally { // Redundant; don't want anyone re-using at this point. mState = STATE_STOPPED; mToneMediaPlayer.release(); mToneMediaPlayer = null; } } } } } @VisibleForTesting @VisibleForTesting public boolean startTone() { public boolean startTone() { // Skip playing the end call tone if the volume is silenced. // Skip playing the end call tone if the volume is silenced. Loading @@ -468,8 +467,12 @@ public class InCallTonePlayer extends Thread { return false; return false; } } sTonesPlaying++; // Tone already done; don't allow re-used if (sTonesPlaying == 1) { if (mState == STATE_STOPPED) { return false; } if (sTonesPlaying.incrementAndGet() == 1) { mCallAudioManager.setIsTonePlaying(true); mCallAudioManager.setIsTonePlaying(true); } } Loading @@ -494,18 +497,17 @@ public class InCallTonePlayer extends Thread { */ */ @VisibleForTesting @VisibleForTesting public void stopTone() { public void stopTone() { synchronized (this) { if (mState == STATE_ON) { if (mState == STATE_ON) { Log.d(this, "Stopping the tone %d.", mToneId); Log.i(this, "stopTone: Stopping the tone %d.", mToneId); notify(); // Notify the playback to end early. mPlaybackLatch.countDown(); } } mState = STATE_STOPPED; mState = STATE_STOPPED; } } } @VisibleForTesting @VisibleForTesting public void cleanup() { public void cleanup() { sTonesPlaying = 0; sTonesPlaying.set(0); } } private void cleanUpTonePlayer() { private void cleanUpTonePlayer() { Loading @@ -514,12 +516,11 @@ public class InCallTonePlayer extends Thread { mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) { mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) { @Override @Override public void loggedRun() { public void loggedRun() { if (sTonesPlaying == 0) { int newToneCount = sTonesPlaying.updateAndGet( t -> Math.min(0, t--)); Log.wtf(InCallTonePlayer.this, "cleanUpTonePlayer(): Over-releasing focus for tone player."); if (newToneCount == 0) { } else if (--sTonesPlaying == 0) { Log.i(InCallTonePlayer.this, Log.i(InCallTonePlayer.this, "cleanUpTonePlayer(): tonesPlaying=%d, tone completed", sTonesPlaying); "cleanUpTonePlayer(): tonesPlaying=%d, tone completed", newToneCount); if (mCallAudioManager != null) { if (mCallAudioManager != null) { mCallAudioManager.setIsTonePlaying(false); mCallAudioManager.setIsTonePlaying(false); } else { } else { Loading @@ -528,7 +529,7 @@ public class InCallTonePlayer extends Thread { } } } else { } else { Log.i(InCallTonePlayer.this, Log.i(InCallTonePlayer.this, "cleanUpTonePlayer(): tonesPlaying=%d; still playing", sTonesPlaying); "cleanUpTonePlayer(): tonesPlaying=%d; still playing", newToneCount); } } } } }.prepare()); }.prepare()); Loading
tests/src/com/android/server/telecom/tests/InCallTonePlayerTest.java +37 −3 Original line number Original line Diff line number Diff line Loading @@ -87,7 +87,7 @@ public class InCallTonePlayerTest extends TelecomTestCase { @Override @Override public int getDuration() { public int getDuration() { return 0; return 1000; } } }; }; Loading Loading @@ -134,7 +134,41 @@ public class InCallTonePlayerTest extends TelecomTestCase { verify(mMediaPlayerFactory, never()).get(anyInt(), any()); verify(mMediaPlayerFactory, never()).get(anyInt(), any()); } } @FlakyTest @SmallTest @Test public void testInterruptMediaTone() { when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true); assertTrue(mInCallTonePlayer.startTone()); // Verify we did play a tone. verify(mMediaPlayerFactory, timeout(5000)).get(anyInt(), any()); verify(mCallAudioManager).setIsTonePlaying(eq(true)); mInCallTonePlayer.stopTone(); // Timeouts due to threads! verify(mCallAudioManager, timeout(5000)).setIsTonePlaying(eq(false)); // Correctness check: ensure we can't start the tone again. assertFalse(mInCallTonePlayer.startTone()); } @SmallTest @Test public void testInterruptToneGenerator() { mInCallTonePlayer = mFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK); when(mAudioManagerAdapter.isVolumeOverZero()).thenReturn(true); assertTrue(mInCallTonePlayer.startTone()); verify(mToneGenerator, timeout(5000)).startTone(anyInt()); verify(mCallAudioManager).setIsTonePlaying(eq(true)); mInCallTonePlayer.stopTone(); // Timeouts due to threads! verify(mCallAudioManager, timeout(5000)).setIsTonePlaying(eq(false)); verify(mToneGenerator, timeout(5000)).release(); // Correctness check: ensure we can't start the tone again. assertFalse(mInCallTonePlayer.startTone()); } @SmallTest @SmallTest @Test @Test public void testEndCallToneWhenNotSilenced() { public void testEndCallToneWhenNotSilenced() { Loading @@ -143,6 +177,6 @@ public class InCallTonePlayerTest extends TelecomTestCase { // Verify we did play a tone. // Verify we did play a tone. verify(mMediaPlayerFactory, timeout(5000)).get(anyInt(), any()); verify(mMediaPlayerFactory, timeout(5000)).get(anyInt(), any()); verify(mCallAudioManager).setIsTonePlaying(eq(true)); verify(mCallAudioManager, timeout(5000)).setIsTonePlaying(eq(true)); } } } }