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
17package com.android.phone;
18
19import com.google.common.collect.ImmutableMap;
20
21import android.content.Context;
22import 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.Connection.PostDialState;
31import com.android.internal.telephony.Phone;
32import com.android.internal.telephony.PhoneConstants;
33import com.android.services.telephony.common.Call;
34
35import java.util.LinkedList;
36import java.util.List;
37import java.util.Map;
38import java.util.Queue;
39
40/**
41 * Playing DTMF tones through the CallManager.
42 */
43public 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