diff options
author | Brandon Maxwell <maxwelb@google.com> | 2016-02-12 23:28:14 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2016-02-12 23:28:14 +0000 |
commit | a37612a50a30eace3f6d8faeaf6a35bc58f6bfbb (patch) | |
tree | 6584bf36540eba8017d5939f93c18d907d3b939b | |
parent | f4b665e44b18614dbdcbbffbb59dc165b349eae5 (diff) | |
parent | f0bba5cb2775daa2acf2a0de950f7249ed9e56a1 (diff) |
Merge "Implementing class to play tones" into nyc-dev
3 files changed, 347 insertions, 1 deletions
diff --git a/InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java b/InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java new file mode 100644 index 000000000..2a94f226f --- /dev/null +++ b/InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.incallui.ringtone; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; + +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.provider.MediaStore.Audio; +import android.support.annotation.Nullable; + +import com.android.contacts.common.testing.NeededForTesting; +import com.android.dialer.compat.CallAudioStateCompat; +import com.android.incallui.AudioModeProvider; +import com.android.incallui.Log; +import com.android.incallui.async.PausableExecutor; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.annotation.concurrent.NotThreadSafe; + +/** + * Class responsible for playing in-call related tones in a background thread. This class only + * allows one tone to be played at a time. + */ +@NeededForTesting +public class InCallTonePlayer { + + public static final int TONE_CALL_WAITING = 4; + + public static final int VOLUME_RELATIVE_HIGH_PRIORITY = 80; + + private final AudioModeProvider mAudioModeProvider; + private final ToneGeneratorFactory mToneGeneratorFactory; + private final PausableExecutor mExecutor; + private @Nullable CountDownLatch mNumPlayingTones; + + /** + * Creates a new InCallTonePlayer. + * + * @param audioModeProvider the {@link AudioModeProvider} used to determine through which stream + * to play tones. + * @param toneGeneratorFactory the {@link ToneGeneratorFactory} used to create + * {@link ToneGenerator}s. + * @param executor the {@link PausableExecutor} used to play tones in a background thread. + * @throws NullPointerException if audioModeProvider, toneGeneratorFactory, or executor are + * {@code null}. + */ + @NeededForTesting + public InCallTonePlayer(AudioModeProvider audioModeProvider, + ToneGeneratorFactory toneGeneratorFactory, PausableExecutor executor) { + mAudioModeProvider = Preconditions.checkNotNull(audioModeProvider); + mToneGeneratorFactory = Preconditions.checkNotNull(toneGeneratorFactory); + mExecutor = Preconditions.checkNotNull(executor); + } + + /** + * @return {@code true} if a tone is currently playing, {@code false} otherwise + */ + @NeededForTesting + public boolean isPlayingTone() { + return mNumPlayingTones != null && mNumPlayingTones.getCount() > 0; + } + + /** + * Plays the given tone in a background thread. + * + * @param tone the tone to play. + * @throws IllegalStateException if a tone is already playing + * @throws IllegalArgumentException if the tone is invalid + */ + @NeededForTesting + public void play(int tone) { + if (isPlayingTone()) { + throw new IllegalStateException("Tone already playing"); + } + final ToneGeneratorInfo info = getToneGeneratorInfo(tone); + mNumPlayingTones = new CountDownLatch(1); + mExecutor.execute(new Runnable() { + @Override + public void run() { + playOnBackgroundThread(info); + } + }); + } + + private ToneGeneratorInfo getToneGeneratorInfo(int tone) { + int stream = getPlaybackStream(); + switch (tone) { + case TONE_CALL_WAITING: + return new ToneGeneratorInfo(ToneGenerator.TONE_SUP_CALL_WAITING, + VOLUME_RELATIVE_HIGH_PRIORITY, + Integer.MAX_VALUE, + stream); + default: + throw new IllegalArgumentException("Bad tone: " + tone); + } + } + + private int getPlaybackStream() { + if (mAudioModeProvider.getAudioMode() == CallAudioStateCompat.ROUTE_BLUETOOTH) { + // TODO (maxwelb): b/26932998 play through bluetooth + // return AudioManager.STREAM_BLUETOOTH_SCO; + } + return AudioManager.STREAM_VOICE_CALL; + } + + private void playOnBackgroundThread(ToneGeneratorInfo info) { + // TODO (maxwelb): b/26936902 respect Do Not Disturb setting + ToneGenerator toneGenerator = null; + try { + Log.v(this, "Starting tone " + info); + toneGenerator = mToneGeneratorFactory.newInCallToneGenerator(info.stream, info.volume); + toneGenerator.startTone(info.tone); + /* + * During tests, this will block until the tests call mExecutor.ackMilestone. This call + * allows for synchronization to the point where the tone has started playing. + */ + mExecutor.milestone(); + if (mNumPlayingTones != null) { + mNumPlayingTones.await(info.toneLengthMillis, TimeUnit.MILLISECONDS); + // Allows for synchronization to the point where the tone has completed playing. + mExecutor.milestone(); + } + } catch (InterruptedException e) { + Log.w(this, "Interrupted while playing in-call tone."); + } finally { + if (toneGenerator != null) { + toneGenerator.release(); + } + if (mNumPlayingTones != null) { + mNumPlayingTones.countDown(); + } + // Allows for synchronization to the point where this background thread has cleaned up. + mExecutor.milestone(); + } + } + + /** + * Stops playback of the current tone. + */ + @NeededForTesting + public void stop() { + if (mNumPlayingTones != null) { + mNumPlayingTones.countDown(); + } + } + + private static class ToneGeneratorInfo { + public final int tone; + public final int volume; + public final int toneLengthMillis; + public final int stream; + + public ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, + int stream) { + this.tone = toneGeneratorType; + this.volume = volume; + this.toneLengthMillis = toneLengthMillis; + this.stream = stream; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("tone", tone) + .add("volume", volume) + .add("toneLengthMillis", toneLengthMillis).toString(); + } + } +} diff --git a/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java b/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java index ee27862b3..839bb2e96 100644 --- a/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java +++ b/InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java @@ -22,7 +22,8 @@ import javax.annotation.concurrent.ThreadSafe; /** * {@link PausableExecutor} for use in tests. It is intended to be used between one test thread - * and one prod thread. + * and one prod thread. See {@link com.android.incallui.ringtone.InCallTonePlayerTest} for example + * usage. */ @ThreadSafe public final class SingleProdThreadExecutor implements PausableExecutor { diff --git a/InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java b/InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java new file mode 100644 index 000000000..096d21122 --- /dev/null +++ b/InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.incallui.ringtone; + +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.incallui.AudioModeProvider; +import com.android.incallui.async.PausableExecutor; +import com.android.incallui.async.SingleProdThreadExecutor; + +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +@SmallTest +public class InCallTonePlayerTest extends AndroidTestCase { + + @Mock private AudioModeProvider mAudioModeProvider; + @Mock private ToneGeneratorFactory mToneGeneratorFactory; + @Mock private ToneGenerator mToneGenerator; + private InCallTonePlayer mInCallTonePlayer; + + /* + * InCallTonePlayer milestones: + * 1) After tone starts playing + * 2) After tone finishes waiting (could have timed out) + * 3) After cleaning up state to allow new tone to play + */ + private PausableExecutor mExecutor; + + @Override + public void setUp() throws Exception { + super.setUp(); + MockitoAnnotations.initMocks(this); + Mockito.when(mToneGeneratorFactory.newInCallToneGenerator(Mockito.anyInt(), + Mockito.anyInt())).thenReturn(mToneGenerator); + mExecutor = new SingleProdThreadExecutor(); + mInCallTonePlayer = new InCallTonePlayer(mAudioModeProvider, mToneGeneratorFactory, + mExecutor); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + // Stop any playing so the InCallTonePlayer isn't stuck waiting for the tone to complete + mInCallTonePlayer.stop(); + // 3 milestones in InCallTonePlayer, ack them to ensure that the prod thread doesn't block + // forever. It's fine to ack for more milestones than are hit + mExecutor.ackMilestoneForTesting(); + mExecutor.ackMilestoneForTesting(); + mExecutor.ackMilestoneForTesting(); + } + + public void testIsPlayingTone_False() { + assertFalse(mInCallTonePlayer.isPlayingTone()); + } + + public void testIsPlayingTone_True() throws InterruptedException { + mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); + mExecutor.awaitMilestoneForTesting(); + + assertTrue(mInCallTonePlayer.isPlayingTone()); + } + + public void testPlay_InvalidTone() { + try { + mInCallTonePlayer.play(Integer.MIN_VALUE); + fail(); + } catch (IllegalArgumentException e) {} + } + + public void testPlay_CurrentlyPlaying() throws InterruptedException { + mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); + mExecutor.awaitMilestoneForTesting(); + try { + mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); + fail(); + } catch (IllegalStateException e) {} + } + + public void testPlay_BlueToothStream() { + // TODO (maxwelb): b/26932998 play through bluetooth + } + + public void testPlay_VoiceCallStream() throws InterruptedException { + mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); + mExecutor.awaitMilestoneForTesting(); + Mockito.verify(mToneGeneratorFactory).newInCallToneGenerator(AudioManager.STREAM_VOICE_CALL, + InCallTonePlayer.VOLUME_RELATIVE_HIGH_PRIORITY); + } + + public void testPlay_Single() throws InterruptedException { + mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); + mExecutor.awaitMilestoneForTesting(); + mExecutor.ackMilestoneForTesting(); + mInCallTonePlayer.stop(); + mExecutor.ackMilestoneForTesting(); + mExecutor.awaitMilestoneForTesting(); + mExecutor.ackMilestoneForTesting(); + + Mockito.verify(mToneGenerator).startTone(ToneGenerator.TONE_SUP_CALL_WAITING); + } + + public void testPlay_Consecutive() throws InterruptedException { + mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); + mExecutor.awaitMilestoneForTesting(); + mExecutor.ackMilestoneForTesting(); + // Prevent waiting forever + mInCallTonePlayer.stop(); + mExecutor.ackMilestoneForTesting(); + mExecutor.awaitMilestoneForTesting(); + mExecutor.ackMilestoneForTesting(); + + mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); + mExecutor.awaitMilestoneForTesting(); + mExecutor.ackMilestoneForTesting(); + mInCallTonePlayer.stop(); + mExecutor.ackMilestoneForTesting(); + mExecutor.awaitMilestoneForTesting(); + mExecutor.ackMilestoneForTesting(); + + Mockito.verify(mToneGenerator, Mockito.times(2)) + .startTone(ToneGenerator.TONE_SUP_CALL_WAITING); + } + + public void testStop_NotPlaying() { + // No crash + mInCallTonePlayer.stop(); + } + + public void testStop() throws InterruptedException { + mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING); + mExecutor.awaitMilestoneForTesting(); + + mInCallTonePlayer.stop(); + mExecutor.ackMilestoneForTesting(); + mExecutor.awaitMilestoneForTesting(); + + assertFalse(mInCallTonePlayer.isPlayingTone()); + } +} |