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 android.media.AudioManager;
20 import android.media.ToneGenerator;
21 import android.support.annotation.NonNull;
22 import android.support.annotation.Nullable;
23 import com.android.incallui.Log;
24 import com.android.incallui.async.PausableExecutor;
25 import java.util.Objects;
26 import java.util.concurrent.CountDownLatch;
27 import java.util.concurrent.TimeUnit;
28 
29 /**
30  * Class responsible for playing in-call related tones in a background thread. This class only
31  * allows one tone to be played at a time.
32  */
33 public class InCallTonePlayer {
34 
35   public static final int TONE_CALL_WAITING = 4;
36 
37   public static final int VOLUME_RELATIVE_HIGH_PRIORITY = 80;
38 
39   @NonNull private final ToneGeneratorFactory mToneGeneratorFactory;
40   @NonNull private final PausableExecutor mExecutor;
41   private @Nullable CountDownLatch mNumPlayingTones;
42 
43   /**
44    * Creates a new InCallTonePlayer.
45    *
46    * @param toneGeneratorFactory the {@link ToneGeneratorFactory} used to create {@link
47    *     ToneGenerator}s.
48    * @param executor the {@link PausableExecutor} used to play tones in a background thread.
49    * @throws NullPointerException if audioModeProvider, toneGeneratorFactory, or executor are {@code
50    *     null}.
51    */
InCallTonePlayer( @onNull ToneGeneratorFactory toneGeneratorFactory, @NonNull PausableExecutor executor)52   public InCallTonePlayer(
53       @NonNull ToneGeneratorFactory toneGeneratorFactory, @NonNull PausableExecutor executor) {
54     mToneGeneratorFactory = Objects.requireNonNull(toneGeneratorFactory);
55     mExecutor = Objects.requireNonNull(executor);
56   }
57 
58   /** @return {@code true} if a tone is currently playing, {@code false} otherwise. */
isPlayingTone()59   public boolean isPlayingTone() {
60     return mNumPlayingTones != null && mNumPlayingTones.getCount() > 0;
61   }
62 
63   /**
64    * Plays the given tone in a background thread.
65    *
66    * @param tone the tone to play.
67    * @throws IllegalStateException if a tone is already playing.
68    * @throws IllegalArgumentException if the tone is invalid.
69    */
play(int tone)70   public void play(int tone) {
71     if (isPlayingTone()) {
72       throw new IllegalStateException("Tone already playing");
73     }
74     final ToneGeneratorInfo info = getToneGeneratorInfo(tone);
75     mNumPlayingTones = new CountDownLatch(1);
76     mExecutor.execute(
77         new Runnable() {
78           @Override
79           public void run() {
80             playOnBackgroundThread(info);
81           }
82         });
83   }
84 
getToneGeneratorInfo(int tone)85   private ToneGeneratorInfo getToneGeneratorInfo(int tone) {
86     switch (tone) {
87       case TONE_CALL_WAITING:
88         /*
89          * DialerCall waiting tones play until they're stopped either by the user accepting or
90          * declining the call so the tone length is set at what's effectively forever. The
91          * tone is played at a high priority volume and through STREAM_VOICE_CALL since it's
92          * call related and using that stream will route it through bluetooth devices
93          * appropriately.
94          */
95         return new ToneGeneratorInfo(
96             ToneGenerator.TONE_SUP_CALL_WAITING,
97             VOLUME_RELATIVE_HIGH_PRIORITY,
98             Integer.MAX_VALUE,
99             AudioManager.STREAM_VOICE_CALL);
100       default:
101         throw new IllegalArgumentException("Bad tone: " + tone);
102     }
103   }
104 
playOnBackgroundThread(ToneGeneratorInfo info)105   private void playOnBackgroundThread(ToneGeneratorInfo info) {
106     ToneGenerator toneGenerator = null;
107     try {
108       Log.v(this, "Starting tone " + info);
109       toneGenerator = mToneGeneratorFactory.newInCallToneGenerator(info.stream, info.volume);
110       toneGenerator.startTone(info.tone);
111       /*
112        * During tests, this will block until the tests call mExecutor.ackMilestone. This call
113        * allows for synchronization to the point where the tone has started playing.
114        */
115       mExecutor.milestone();
116       if (mNumPlayingTones != null) {
117         mNumPlayingTones.await(info.toneLengthMillis, TimeUnit.MILLISECONDS);
118         // Allows for synchronization to the point where the tone has completed playing.
119         mExecutor.milestone();
120       }
121     } catch (InterruptedException e) {
122       Log.w(this, "Interrupted while playing in-call tone.");
123     } finally {
124       if (toneGenerator != null) {
125         toneGenerator.release();
126       }
127       if (mNumPlayingTones != null) {
128         mNumPlayingTones.countDown();
129       }
130       // Allows for synchronization to the point where this background thread has cleaned up.
131       mExecutor.milestone();
132     }
133   }
134 
135   /** Stops playback of the current tone. */
stop()136   public void stop() {
137     if (mNumPlayingTones != null) {
138       mNumPlayingTones.countDown();
139     }
140   }
141 
142   private static class ToneGeneratorInfo {
143 
144     public final int tone;
145     public final int volume;
146     public final int toneLengthMillis;
147     public final int stream;
148 
ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, int stream)149     public ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, int stream) {
150       this.tone = toneGeneratorType;
151       this.volume = volume;
152       this.toneLengthMillis = toneLengthMillis;
153       this.stream = stream;
154     }
155 
156     @Override
toString()157     public String toString() {
158       return "ToneGeneratorInfo{"
159           + "toneLengthMillis="
160           + toneLengthMillis
161           + ", tone="
162           + tone
163           + ", volume="
164           + volume
165           + '}';
166     }
167   }
168 }
169