1 /*
2  * Copyright 2014, 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.server.telecom;
18 
19 import android.content.Context;
20 import android.media.AudioManager;
21 import android.media.ToneGenerator;
22 import android.os.Handler;
23 import android.os.HandlerThread;
24 import android.os.Looper;
25 import android.os.Message;
26 import android.provider.Settings;
27 import android.telecom.Log;
28 import android.telecom.Logging.Session;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.internal.util.Preconditions;
32 
33 // TODO: Needed for move to system service: import com.android.internal.R;
34 
35 /**
36  * Plays DTMF tones locally for the caller to hear. In order to reduce (1) the amount of times we
37  * check the "play local tones" setting and (2) the length of time we keep the tone generator, this
38  * class employs a concept of a call "session" that starts and stops when the foreground call
39  * changes.
40  */
41 public class DtmfLocalTonePlayer {
42     public static class ToneGeneratorProxy {
43         /** Generator used to actually play the tone. */
44         private ToneGenerator mToneGenerator;
45 
create()46         public void create() {
47             if (mToneGenerator == null) {
48                 try {
49                     mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, 80);
50                 } catch (RuntimeException e) {
51                     Log.e(this, e, "Error creating local tone generator.");
52                     mToneGenerator = null;
53                 }
54             }
55         }
56 
release()57         public void release() {
58             if (mToneGenerator != null) {
59                 mToneGenerator.release();
60                 mToneGenerator = null;
61             }
62         }
63 
isPresent()64         public boolean isPresent() {
65             return mToneGenerator != null;
66         }
67 
startTone(int toneType, int durationMs)68         public void startTone(int toneType, int durationMs) {
69             if (mToneGenerator != null) {
70                 mToneGenerator.startTone(toneType, durationMs);
71             }
72         }
73 
stopTone()74         public void stopTone() {
75             if (mToneGenerator != null) {
76                 mToneGenerator.stopTone();
77             }
78         }
79     }
80 
81     private final class ToneHandler extends Handler {
82 
ToneHandler(Looper looper)83         public ToneHandler(Looper looper) {
84             super(looper);
85         }
86 
87         @Override
handleMessage(Message msg)88         public void handleMessage(Message msg) {
89             if (msg.obj instanceof Session) {
90                 Log.continueSession((Session) msg.obj, "DLTP.TH");
91             }
92 
93             switch(msg.what) {
94                 case EVENT_START_SESSION:
95                     mToneGeneratorProxy.create();
96                     break;
97                 case EVENT_END_SESSION:
98                     mToneGeneratorProxy.release();
99                     break;
100                 case EVENT_PLAY_TONE:
101                     char c = (char) msg.arg1;
102                     if (!mToneGeneratorProxy.isPresent()) {
103                         Log.d(this, "playTone: no tone generator, %c.", c);
104                     } else {
105                         Log.d(this, "starting local tone: %c.", c);
106                         int tone = getMappedTone(c);
107                         if (tone != ToneGenerator.TONE_UNKNOWN) {
108                             mToneGeneratorProxy.startTone(tone, -1 /* toneDuration */);
109                         }
110                     }
111                     break;
112                 case EVENT_STOP_TONE:
113                     if (mToneGeneratorProxy.isPresent()) {
114                         mToneGeneratorProxy.stopTone();
115                     }
116                     break;
117                 default:
118                     Log.w(this, "Unknown message: %d", msg.what);
119                     break;
120             }
121         }
122     }
123 
124     /** The current call associated with an existing dtmf session. */
125     private Call mCall;
126 
127     /**
128      * Message codes to be used for creating and deleting ToneGenerator object in the tonegenerator
129      * thread, as well as for actually playing the tones.
130      */
131     private static final int EVENT_START_SESSION = 1;
132     private static final int EVENT_END_SESSION = 2;
133     private static final int EVENT_PLAY_TONE = 3;
134     private static final int EVENT_STOP_TONE = 4;
135 
136     /** Handler running on the tonegenerator thread. */
137     private ToneHandler mHandler;
138 
139     private final ToneGeneratorProxy mToneGeneratorProxy;
140 
DtmfLocalTonePlayer(ToneGeneratorProxy toneGeneratorProxy)141     public DtmfLocalTonePlayer(ToneGeneratorProxy toneGeneratorProxy) {
142         mToneGeneratorProxy = toneGeneratorProxy;
143     }
144 
onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall)145     public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
146         endDtmfSession(oldForegroundCall);
147         startDtmfSession(newForegroundCall);
148     }
149 
150     /**
151      * Starts playing the dtmf tone specified by c.
152      *
153      * @param call The associated call.
154      * @param c The digit to play.
155      */
playTone(Call call, char c)156     public void playTone(Call call, char c) {
157         // Do nothing if it is not the right call.
158         if (mCall != call) {
159             return;
160         }
161 
162         getHandler().sendMessage(
163                 getHandler().obtainMessage(EVENT_PLAY_TONE, (int) c, 0, Log.createSubsession()));
164     }
165 
166     /**
167      * Stops any currently playing dtmf tone.
168      *
169      * @param call The associated call.
170      */
stopTone(Call call)171     public void stopTone(Call call) {
172         // Do nothing if it's not the right call.
173         if (mCall != call) {
174             return;
175         }
176 
177         getHandler().sendMessage(
178                 getHandler().obtainMessage(EVENT_STOP_TONE, Log.createSubsession()));
179     }
180 
181     /**
182      * Runs initialization requires to play local tones during a call.
183      *
184      * @param call The call associated with this dtmf session.
185      */
startDtmfSession(Call call)186     private void startDtmfSession(Call call) {
187         if (call == null) {
188             return;
189         }
190         final Context context = call.getContext();
191         final boolean areLocalTonesEnabled;
192         if (context.getResources().getBoolean(R.bool.allow_local_dtmf_tones)) {
193             areLocalTonesEnabled = Settings.System.getInt(
194                     context.getContentResolver(), Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
195         } else {
196             areLocalTonesEnabled = false;
197         }
198 
199         mCall = call;
200 
201         if (areLocalTonesEnabled) {
202             Log.d(this, "Posting create.");
203             getHandler().sendMessage(
204                     getHandler().obtainMessage(EVENT_START_SESSION, Log.createSubsession()));
205         }
206     }
207 
208     /**
209      * Releases resources needed for playing local dtmf tones.
210      *
211      * @param call The call associated with the session to end.
212      */
endDtmfSession(Call call)213     private void endDtmfSession(Call call) {
214         if (call != null && mCall == call) {
215             // Do a stopTone() in case the sessions ends before we are told to stop the tone.
216             stopTone(call);
217 
218             mCall = null;
219             Log.d(this, "Posting delete.");
220             getHandler().sendMessage(
221                     getHandler().obtainMessage(EVENT_END_SESSION, Log.createSubsession()));
222         }
223     }
224 
225     /**
226      * Creates a new ToneHandler on a separate thread if none exists, and returns it.
227      * No need for locking, since everything that calls this is protected by the Telecom lock.
228      */
229     @VisibleForTesting
getHandler()230     public ToneHandler getHandler() {
231         if (mHandler == null) {
232             HandlerThread thread = new HandlerThread("tonegenerator-dtmf");
233             thread.start();
234             mHandler = new ToneHandler(thread.getLooper());
235         }
236         return mHandler;
237     }
238 
getMappedTone(char digit)239     private static int getMappedTone(char digit) {
240         if (digit >= '0' && digit <= '9') {
241             return ToneGenerator.TONE_DTMF_0 + digit - '0';
242         } else if (digit == '#') {
243             return ToneGenerator.TONE_DTMF_P;
244         } else if (digit == '*') {
245             return ToneGenerator.TONE_DTMF_S;
246         }
247         return ToneGenerator.TONE_UNKNOWN;
248     }
249 }
250