summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrandon Maxwell <maxwelb@google.com>2016-02-12 23:28:14 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2016-02-12 23:28:14 +0000
commita37612a50a30eace3f6d8faeaf6a35bc58f6bfbb (patch)
tree6584bf36540eba8017d5939f93c18d907d3b939b
parentf4b665e44b18614dbdcbbffbb59dc165b349eae5 (diff)
parentf0bba5cb2775daa2acf2a0de950f7249ed9e56a1 (diff)
Merge "Implementing class to play tones" into nyc-dev
-rw-r--r--InCallUI/src/com/android/incallui/ringtone/InCallTonePlayer.java187
-rw-r--r--InCallUI/tests/src/com/android/incallui/async/SingleProdThreadExecutor.java3
-rw-r--r--InCallUI/tests/src/com/android/incallui/ringtone/InCallTonePlayerTest.java158
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());
+ }
+}