Home | History | Annotate | Download | only in phone
      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