1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.incallui.ringtone;
18 
19 import com.google.common.base.MoreObjects;
20 import com.google.common.base.Preconditions;
21 
22 import android.media.AudioManager;
23 import android.media.ToneGenerator;
24 import android.support.annotation.Nullable;
25 
26 import com.android.incallui.Log;
27 import com.android.incallui.async.PausableExecutor;
28 
29 import java.util.concurrent.CountDownLatch;
30 import java.util.concurrent.TimeUnit;
31 
32 /**
33  * Class responsible for playing in-call related tones in a background thread. This class only
34  * allows one tone to be played at a time.
35  */
36 public class InCallTonePlayer {
37 
38     public static final int TONE_CALL_WAITING = 4;
39 
40     public static final int VOLUME_RELATIVE_HIGH_PRIORITY = 80;
41 
42     private final ToneGeneratorFactory mToneGeneratorFactory;
43     private final PausableExecutor mExecutor;
44     private @Nullable CountDownLatch mNumPlayingTones;
45 
46     /**
47      * Creates a new InCallTonePlayer.
48      *
49      * @param toneGeneratorFactory the {@link ToneGeneratorFactory} used to create
50      * {@link ToneGenerator}s.
51      * @param executor the {@link PausableExecutor} used to play tones in a background thread.
52      * @throws NullPointerException if audioModeProvider, toneGeneratorFactory, or executor are
53      * {@code null}.
54      */
InCallTonePlayer(ToneGeneratorFactory toneGeneratorFactory, PausableExecutor executor)55     public InCallTonePlayer(ToneGeneratorFactory toneGeneratorFactory, PausableExecutor executor) {
56         mToneGeneratorFactory = Preconditions.checkNotNull(toneGeneratorFactory);
57         mExecutor = Preconditions.checkNotNull(executor);
58     }
59 
60     /**
61      * @return {@code true} if a tone is currently playing, {@code false} otherwise.
62      */
isPlayingTone()63     public boolean isPlayingTone() {
64         return mNumPlayingTones != null && mNumPlayingTones.getCount() > 0;
65     }
66 
67     /**
68      * Plays the given tone in a background thread.
69      *
70      * @param tone the tone to play.
71      * @throws IllegalStateException if a tone is already playing.
72      * @throws IllegalArgumentException if the tone is invalid.
73      */
play(int tone)74     public void play(int tone) {
75         if (isPlayingTone()) {
76             throw new IllegalStateException("Tone already playing");
77         }
78         final ToneGeneratorInfo info = getToneGeneratorInfo(tone);
79         mNumPlayingTones = new CountDownLatch(1);
80         mExecutor.execute(new Runnable() {
81             @Override
82             public void run() {
83                 playOnBackgroundThread(info);
84             }
85         });
86     }
87 
getToneGeneratorInfo(int tone)88     private ToneGeneratorInfo getToneGeneratorInfo(int tone) {
89         switch (tone) {
90             case TONE_CALL_WAITING:
91                 /*
92                  * Call waiting tones play until they're stopped either by the user accepting or
93                  * declining the call so the tone length is set at what's effectively forever. The
94                  * tone is played at a high priority volume and through STREAM_VOICE_CALL since it's
95                  * call related and using that stream will route it through bluetooth devices
96                  * appropriately.
97                  */
98                 return new ToneGeneratorInfo(ToneGenerator.TONE_SUP_CALL_WAITING,
99                         VOLUME_RELATIVE_HIGH_PRIORITY,
100                         Integer.MAX_VALUE,
101                         AudioManager.STREAM_VOICE_CALL);
102             default:
103                 throw new IllegalArgumentException("Bad tone: " + tone);
104         }
105     }
106 
playOnBackgroundThread(ToneGeneratorInfo info)107     private void playOnBackgroundThread(ToneGeneratorInfo info) {
108         ToneGenerator toneGenerator = null;
109         try {
110             Log.v(this, "Starting tone " + info);
111             toneGenerator = mToneGeneratorFactory.newInCallToneGenerator(info.stream, info.volume);
112             toneGenerator.startTone(info.tone);
113             /*
114              * During tests, this will block until the tests call mExecutor.ackMilestone. This call
115              * allows for synchronization to the point where the tone has started playing.
116              */
117             mExecutor.milestone();
118             if (mNumPlayingTones != null) {
119                 mNumPlayingTones.await(info.toneLengthMillis, TimeUnit.MILLISECONDS);
120                 // Allows for synchronization to the point where the tone has completed playing.
121                 mExecutor.milestone();
122             }
123         } catch (InterruptedException e) {
124             Log.w(this, "Interrupted while playing in-call tone.");
125         } finally {
126             if (toneGenerator != null) {
127                 toneGenerator.release();
128             }
129             if (mNumPlayingTones != null) {
130                 mNumPlayingTones.countDown();
131             }
132             // Allows for synchronization to the point where this background thread has cleaned up.
133             mExecutor.milestone();
134         }
135     }
136 
137     /**
138      * Stops playback of the current tone.
139      */
stop()140     public void stop() {
141         if (mNumPlayingTones != null) {
142             mNumPlayingTones.countDown();
143         }
144     }
145 
146     private static class ToneGeneratorInfo {
147         public final int tone;
148         public final int volume;
149         public final int toneLengthMillis;
150         public final int stream;
151 
ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, int stream)152         public ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis,
153                 int stream) {
154             this.tone = toneGeneratorType;
155             this.volume = volume;
156             this.toneLengthMillis = toneLengthMillis;
157             this.stream = stream;
158         }
159 
160         @Override
toString()161         public String toString() {
162             return MoreObjects.toStringHelper(this)
163                     .add("tone", tone)
164                     .add("volume", volume)
165                     .add("toneLengthMillis", toneLengthMillis).toString();
166         }
167     }
168 }
169