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