blob: 0acd11457868136ff1cea6dada81026f8639d41c [file] [log] [blame]
Santos Cordon2eaff902013-08-05 04:37:55 -07001/*
2 * Copyright (C) 2013 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
17package com.android.phone;
18
19import com.google.common.collect.ImmutableMap;
20
Christine Chendaf7bf62013-08-05 19:12:31 -070021import android.content.Context;
Santos Cordon2eaff902013-08-05 04:37:55 -070022import android.media.AudioManager;
23import android.media.ToneGenerator;
24import android.os.Handler;
25import android.os.Message;
26import android.provider.Settings;
27import android.util.Log;
28
29import com.android.internal.telephony.CallManager;
30import com.android.internal.telephony.Phone;
Christine Chendaf7bf62013-08-05 19:12:31 -070031import com.android.internal.telephony.PhoneConstants;
Santos Cordon2eaff902013-08-05 04:37:55 -070032import com.android.services.telephony.common.Call;
33
Christine Chendaf7bf62013-08-05 19:12:31 -070034import java.util.LinkedList;
Santos Cordon2eaff902013-08-05 04:37:55 -070035import java.util.List;
36import java.util.Map;
Christine Chendaf7bf62013-08-05 19:12:31 -070037import java.util.Queue;
Santos Cordon2eaff902013-08-05 04:37:55 -070038
39/**
40 * Playing DTMF tones through the CallManager.
41 */
42public class DTMFTonePlayer implements CallModeler.Listener {
43 private static final String LOG_TAG = DTMFTonePlayer.class.getSimpleName();
44 private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
45
Christine Chendaf7bf62013-08-05 19:12:31 -070046 private static final int DTMF_SEND_CNF = 100;
47 private static final int DTMF_STOP = 101;
Santos Cordon2eaff902013-08-05 04:37:55 -070048
49 /** Hash Map to map a character to a tone*/
50 private static final Map<Character, Integer> mToneMap =
51 ImmutableMap.<Character, Integer>builder()
52 .put('1', ToneGenerator.TONE_DTMF_1)
53 .put('2', ToneGenerator.TONE_DTMF_2)
54 .put('3', ToneGenerator.TONE_DTMF_3)
55 .put('4', ToneGenerator.TONE_DTMF_4)
56 .put('5', ToneGenerator.TONE_DTMF_5)
57 .put('6', ToneGenerator.TONE_DTMF_6)
58 .put('7', ToneGenerator.TONE_DTMF_7)
59 .put('8', ToneGenerator.TONE_DTMF_8)
60 .put('9', ToneGenerator.TONE_DTMF_9)
61 .put('0', ToneGenerator.TONE_DTMF_0)
62 .put('#', ToneGenerator.TONE_DTMF_P)
63 .put('*', ToneGenerator.TONE_DTMF_S)
64 .build();
65
66 private final CallManager mCallManager;
67 private final CallModeler mCallModeler;
68 private final Object mToneGeneratorLock = new Object();
69 private ToneGenerator mToneGenerator;
70 private boolean mLocalToneEnabled;
71
Christine Chendaf7bf62013-08-05 19:12:31 -070072 // indicates that we are using automatically shortened DTMF tones
73 boolean mShortTone;
74
75 // indicate if the confirmation from TelephonyFW is pending.
76 private boolean mDTMFBurstCnfPending = false;
77
78 // Queue to queue the short dtmf characters.
79 private Queue<Character> mDTMFQueue = new LinkedList<Character>();
80
81 // Short Dtmf tone duration
82 private static final int DTMF_DURATION_MS = 120;
83
84 /**
85 * Our own handler to take care of the messages from the phone state changes
86 */
87 private final Handler mHandler = new Handler() {
88 @Override
89 public void handleMessage(Message msg) {
90 switch (msg.what) {
91 case DTMF_SEND_CNF:
92 logD("dtmf confirmation received from FW.");
93 // handle burst dtmf confirmation
94 handleBurstDtmfConfirmation();
95 break;
96 case DTMF_STOP:
97 logD("dtmf stop received");
98 stopDtmfTone();
99 break;
100 }
101 }
102 };
103
Santos Cordon2eaff902013-08-05 04:37:55 -0700104 public DTMFTonePlayer(CallManager callManager, CallModeler callModeler) {
105 mCallManager = callManager;
106 mCallModeler = callModeler;
Christine Chendaf7bf62013-08-05 19:12:31 -0700107 mCallModeler.addListener(this);
Santos Cordon2eaff902013-08-05 04:37:55 -0700108 }
109
110 @Override
111 public void onDisconnect(Call call) {
Christine Chendaf7bf62013-08-05 19:12:31 -0700112 logD("Call disconnected");
Santos Cordon2eaff902013-08-05 04:37:55 -0700113 checkCallState();
114 }
115
116 @Override
Chiao Cheng6c6b2722013-08-22 18:35:54 -0700117 public void onIncoming(Call call) {
Christine Chenee09a492013-08-06 16:02:29 -0700118 }
119
120 @Override
Chiao Cheng6c6b2722013-08-22 18:35:54 -0700121 public void onUpdate(List<Call> calls) {
Christine Chendaf7bf62013-08-05 19:12:31 -0700122 logD("Call updated");
Santos Cordon2eaff902013-08-05 04:37:55 -0700123 checkCallState();
124 }
125
Chiao Cheng3f015c92013-09-06 15:56:27 -0700126 @Override
127 public void onPostDialWait(int callId, String chars) {
128 // no-op
129 }
130
Santos Cordon2eaff902013-08-05 04:37:55 -0700131 /**
132 * Allocates some resources we keep around during a "dialer session".
133 *
134 * (Currently, a "dialer session" just means any situation where we
135 * might need to play local DTMF tones, which means that we need to
136 * keep a ToneGenerator instance around. A ToneGenerator instance
137 * keeps an AudioTrack resource busy in AudioFlinger, so we don't want
138 * to keep it around forever.)
139 *
140 * Call {@link stopDialerSession} to release the dialer session
141 * resources.
142 */
143 public void startDialerSession() {
144 logD("startDialerSession()... this = " + this);
145
146 // see if we need to play local tones.
147 if (PhoneGlobals.getInstance().getResources().getBoolean(R.bool.allow_local_dtmf_tones)) {
148 mLocalToneEnabled = Settings.System.getInt(
149 PhoneGlobals.getInstance().getContentResolver(),
150 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
151 } else {
152 mLocalToneEnabled = false;
153 }
154 logD("- startDialerSession: mLocalToneEnabled = " + mLocalToneEnabled);
155
156 // create the tone generator
157 // if the mToneGenerator creation fails, just continue without it. It is
158 // a local audio signal, and is not as important as the dtmf tone itself.
159 if (mLocalToneEnabled) {
160 synchronized (mToneGeneratorLock) {
161 if (mToneGenerator == null) {
162 try {
163 mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, 80);
164 } catch (RuntimeException e) {
165 Log.e(LOG_TAG, "Exception caught while creating local tone generator", e);
166 mToneGenerator = null;
167 }
168 }
169 }
170 }
171 }
172
173 /**
174 * Releases resources we keep around during a "dialer session"
175 * (see {@link startDialerSession}).
176 *
177 * It's safe to call this even without a corresponding
178 * startDialerSession call.
179 */
180 public void stopDialerSession() {
181 // release the tone generator.
182 synchronized (mToneGeneratorLock) {
183 if (mToneGenerator != null) {
184 mToneGenerator.release();
185 mToneGenerator = null;
186 }
187 }
Christine Chendaf7bf62013-08-05 19:12:31 -0700188
189 mHandler.removeMessages(DTMF_SEND_CNF);
190 synchronized (mDTMFQueue) {
191 mDTMFBurstCnfPending = false;
192 mDTMFQueue.clear();
193 }
Santos Cordon2eaff902013-08-05 04:37:55 -0700194 }
195
196 /**
197 * Starts playback of the dtmf tone corresponding to the parameter.
198 */
Christine Chendaf7bf62013-08-05 19:12:31 -0700199 public void playDtmfTone(char c, boolean timedShortTone) {
Santos Cordon2eaff902013-08-05 04:37:55 -0700200 // Only play the tone if it exists.
201 if (!mToneMap.containsKey(c)) {
202 return;
203 }
204
205 if (!okToDialDtmfTones()) {
206 return;
207 }
208
Christine Chendaf7bf62013-08-05 19:12:31 -0700209 PhoneGlobals.getInstance().pokeUserActivity();
210
Santos Cordon2eaff902013-08-05 04:37:55 -0700211 // Read the settings as it may be changed by the user during the call
212 Phone phone = mCallManager.getFgPhone();
213
Christine Chendaf7bf62013-08-05 19:12:31 -0700214 // Before we go ahead and start a tone, we need to make sure that any pending
215 // stop-tone message is processed.
216 if (mHandler.hasMessages(DTMF_STOP)) {
217 mHandler.removeMessages(DTMF_STOP);
218 stopDtmfTone();
219 }
220
221 mShortTone = useShortDtmfTones(phone, phone.getContext());
Santos Cordon2eaff902013-08-05 04:37:55 -0700222 logD("startDtmfTone()...");
223
Christine Chendaf7bf62013-08-05 19:12:31 -0700224 // For Short DTMF we need to play the local tone for fixed duration
225 if (mShortTone) {
226 sendShortDtmfToNetwork(c);
227 } else {
228 // Pass as a char to be sent to network
229 logD("send long dtmf for " + c);
230 mCallManager.startDtmf(c);
231
232 // If it is a timed tone, queue up the stop command in DTMF_DURATION_MS.
233 if (timedShortTone) {
234 mHandler.sendMessageDelayed(mHandler.obtainMessage(DTMF_STOP), DTMF_DURATION_MS);
235 }
236 }
Santos Cordon2eaff902013-08-05 04:37:55 -0700237
238 startLocalToneIfNeeded(c);
239 }
240
Christine Chendaf7bf62013-08-05 19:12:31 -0700241 /**
242 * Sends the dtmf character over the network for short DTMF settings
243 * When the characters are entered in quick succession,
244 * the characters are queued before sending over the network.
245 */
246 private void sendShortDtmfToNetwork(char dtmfDigit) {
247 synchronized (mDTMFQueue) {
248 if (mDTMFBurstCnfPending == true) {
249 // Insert the dtmf char to the queue
250 mDTMFQueue.add(new Character(dtmfDigit));
251 } else {
252 String dtmfStr = Character.toString(dtmfDigit);
253 mCallManager.sendBurstDtmf(dtmfStr, 0, 0, mHandler.obtainMessage(DTMF_SEND_CNF));
254 // Set flag to indicate wait for Telephony confirmation.
255 mDTMFBurstCnfPending = true;
256 }
257 }
258 }
259
260 /**
261 * Handles Burst Dtmf Confirmation from the Framework.
262 */
263 void handleBurstDtmfConfirmation() {
264 Character dtmfChar = null;
265 synchronized (mDTMFQueue) {
266 mDTMFBurstCnfPending = false;
267 if (!mDTMFQueue.isEmpty()) {
268 dtmfChar = mDTMFQueue.remove();
269 Log.i(LOG_TAG, "The dtmf character removed from queue" + dtmfChar);
270 }
271 }
272 if (dtmfChar != null) {
273 sendShortDtmfToNetwork(dtmfChar);
274 }
275 }
276
Santos Cordon2eaff902013-08-05 04:37:55 -0700277 public void stopDtmfTone() {
Christine Chendaf7bf62013-08-05 19:12:31 -0700278 if (!mShortTone) {
279 mCallManager.stopDtmf();
280 stopLocalToneIfNeeded();
281 }
Santos Cordon2eaff902013-08-05 04:37:55 -0700282 }
283
284 /**
285 * Plays the local tone based the phone type, optionally forcing a short
286 * tone.
287 */
288 private void startLocalToneIfNeeded(char c) {
289 if (mLocalToneEnabled) {
290 synchronized (mToneGeneratorLock) {
291 if (mToneGenerator == null) {
292 logD("startDtmfTone: mToneGenerator == null, tone: " + c);
293 } else {
294 logD("starting local tone " + c);
295 int toneDuration = -1;
Christine Chendaf7bf62013-08-05 19:12:31 -0700296 if (mShortTone) {
297 toneDuration = DTMF_DURATION_MS;
298 }
Santos Cordon2eaff902013-08-05 04:37:55 -0700299 mToneGenerator.startTone(mToneMap.get(c), toneDuration);
300 }
301 }
302 }
303 }
304
305 /**
306 * Stops the local tone based on the phone type.
307 */
308 public void stopLocalToneIfNeeded() {
Christine Chendaf7bf62013-08-05 19:12:31 -0700309 if (!mShortTone) {
310 // if local tone playback is enabled, stop it.
311 logD("trying to stop local tone...");
312 if (mLocalToneEnabled) {
313 synchronized (mToneGeneratorLock) {
314 if (mToneGenerator == null) {
315 logD("stopLocalTone: mToneGenerator == null");
316 } else {
317 logD("stopping local tone.");
318 mToneGenerator.stopTone();
319 }
Santos Cordon2eaff902013-08-05 04:37:55 -0700320 }
321 }
322 }
323 }
324
325 private boolean okToDialDtmfTones() {
326 boolean hasActiveCall = false;
327 boolean hasIncomingCall = false;
328
329 final List<Call> calls = mCallModeler.getFullList();
330 final int len = calls.size();
331
332 for (int i = 0; i < len; i++) {
Santos Cordon2b73bd62013-08-27 14:53:43 -0700333 // We can also dial while in DIALING state because there are
334 // some connections that never update to an ACTIVE state (no
335 // indication from the network).
336 hasActiveCall |= (calls.get(i).getState() == Call.State.ACTIVE)
337 || (calls.get(i).getState() == Call.State.DIALING);
Santos Cordon2eaff902013-08-05 04:37:55 -0700338 hasIncomingCall |= (calls.get(i).getState() == Call.State.INCOMING);
339 }
340
341 return hasActiveCall && !hasIncomingCall;
342 }
343
344 /**
Christine Chendaf7bf62013-08-05 19:12:31 -0700345 * On GSM devices, we never use short tones.
346 * On CDMA devices, it depends upon the settings.
347 */
348 private static boolean useShortDtmfTones(Phone phone, Context context) {
349 int phoneType = phone.getPhoneType();
350 if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
351 return false;
352 } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
353 int toneType = android.provider.Settings.System.getInt(
354 context.getContentResolver(),
355 Settings.System.DTMF_TONE_TYPE_WHEN_DIALING,
356 Constants.DTMF_TONE_TYPE_NORMAL);
357 if (toneType == Constants.DTMF_TONE_TYPE_NORMAL) {
358 return true;
359 } else {
360 return false;
361 }
362 } else if (phoneType == PhoneConstants.PHONE_TYPE_SIP) {
363 return false;
364 } else {
365 throw new IllegalStateException("Unexpected phone type: " + phoneType);
366 }
367 }
368
369 /**
Santos Cordon2eaff902013-08-05 04:37:55 -0700370 * Checks to see if there are any active calls. If there are, then we want to allocate the tone
371 * resources for playing DTMF tone, otherwise release them.
372 */
373 private void checkCallState() {
Christine Chendaf7bf62013-08-05 19:12:31 -0700374 logD("checkCallState");
Santos Cordon2b73bd62013-08-27 14:53:43 -0700375 if (mCallModeler.hasOutstandingActiveOrDialingCall()) {
Santos Cordon2eaff902013-08-05 04:37:55 -0700376 startDialerSession();
377 } else {
378 stopDialerSession();
379 }
380 }
381
382 /**
383 * static logging method
384 */
385 private static void logD(String msg) {
386 if (DBG) {
387 Log.d(LOG_TAG, msg);
388 }
389 }
390
391}