Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a37612a5 authored by Brandon Maxwell's avatar Brandon Maxwell Committed by Android (Google) Code Review
Browse files

Merge "Implementing class to play tones" into nyc-dev

parents f4b665e4 f0bba5cb
Loading
Loading
Loading
Loading
+187 −0
Original line number Diff line number Diff line
/*
 * 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();
        }
    }
}
+2 −1
Original line number Diff line number Diff line
@@ -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 {
+158 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}