1/*
2 * Copyright (C) 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
17package com.android.services.telephony;
18
19import android.os.Handler;
20import android.os.Message;
21
22import android.provider.Settings;
23import android.telephony.DisconnectCause;
24import android.telephony.PhoneNumberUtils;
25
26import com.android.internal.telephony.Call;
27import com.android.internal.telephony.CallStateException;
28import com.android.internal.telephony.Connection;
29import com.android.internal.telephony.imsphone.ImsPhoneConnection;
30import com.android.internal.telephony.Phone;
31import com.android.phone.settings.SettingsConstants;
32
33import java.util.LinkedList;
34import java.util.Queue;
35
36/**
37 * Manages a single phone call handled by CDMA.
38 */
39final class CdmaConnection extends TelephonyConnection {
40
41    private static final int MSG_CALL_WAITING_MISSED = 1;
42    private static final int MSG_DTMF_SEND_CONFIRMATION = 2;
43    private static final int TIMEOUT_CALL_WAITING_MILLIS = 20 * 1000;
44
45    private final Handler mHandler = new Handler() {
46
47        /** ${inheritDoc} */
48        @Override
49        public void handleMessage(Message msg) {
50            switch (msg.what) {
51                case MSG_CALL_WAITING_MISSED:
52                    hangupCallWaiting(DisconnectCause.INCOMING_MISSED);
53                    break;
54                case MSG_DTMF_SEND_CONFIRMATION:
55                    handleBurstDtmfConfirmation();
56                    break;
57                default:
58                    break;
59            }
60        }
61
62    };
63
64    /**
65     * {@code True} if the CDMA connection should allow mute.
66     */
67    private boolean mAllowMute;
68    private final boolean mIsOutgoing;
69    // Queue of pending short-DTMF characters.
70    private final Queue<Character> mDtmfQueue = new LinkedList<>();
71    private final EmergencyTonePlayer mEmergencyTonePlayer;
72
73    // Indicates that the DTMF confirmation from telephony is pending.
74    private boolean mDtmfBurstConfirmationPending = false;
75    private boolean mIsCallWaiting;
76
77    CdmaConnection(
78            Connection connection,
79            EmergencyTonePlayer emergencyTonePlayer,
80            boolean allowMute,
81            boolean isOutgoing,
82            String telecomCallId) {
83        super(connection, telecomCallId);
84        mEmergencyTonePlayer = emergencyTonePlayer;
85        mAllowMute = allowMute;
86        mIsOutgoing = isOutgoing;
87        mIsCallWaiting = connection != null && connection.getState() == Call.State.WAITING;
88        boolean isImsCall = getOriginalConnection() instanceof ImsPhoneConnection;
89        // Start call waiting timer for CDMA waiting call.
90        if (mIsCallWaiting && !isImsCall) {
91            startCallWaitingTimer();
92        }
93    }
94
95    /** {@inheritDoc} */
96    @Override
97    public void onPlayDtmfTone(char digit) {
98        if (useBurstDtmf()) {
99            Log.i(this, "sending dtmf digit as burst");
100            sendShortDtmfToNetwork(digit);
101        } else {
102            Log.i(this, "sending dtmf digit directly");
103            getPhone().startDtmf(digit);
104        }
105    }
106
107    /** {@inheritDoc} */
108    @Override
109    public void onStopDtmfTone() {
110        if (!useBurstDtmf()) {
111            getPhone().stopDtmf();
112        }
113    }
114
115    @Override
116    public void onReject() {
117        Connection connection = getOriginalConnection();
118        if (connection != null) {
119            switch (connection.getState()) {
120                case INCOMING:
121                    // Normal ringing calls are handled the generic way.
122                    super.onReject();
123                    break;
124                case WAITING:
125                    hangupCallWaiting(DisconnectCause.INCOMING_REJECTED);
126                    break;
127                default:
128                    Log.e(this, new Exception(), "Rejecting a non-ringing call");
129                    // might as well hang this up, too.
130                    super.onReject();
131                    break;
132            }
133        }
134    }
135
136    @Override
137    public void onAnswer() {
138        mHandler.removeMessages(MSG_CALL_WAITING_MISSED);
139        super.onAnswer();
140    }
141
142    /**
143     * Clones the current {@link CdmaConnection}.
144     * <p>
145     * Listeners are not copied to the new instance.
146     *
147     * @return The cloned connection.
148     */
149    @Override
150    public TelephonyConnection cloneConnection() {
151        CdmaConnection cdmaConnection = new CdmaConnection(getOriginalConnection(),
152                mEmergencyTonePlayer, mAllowMute, mIsOutgoing, getTelecomCallId());
153        return cdmaConnection;
154    }
155
156    @Override
157    public void onStateChanged(int state) {
158        Connection originalConnection = getOriginalConnection();
159        mIsCallWaiting = originalConnection != null &&
160                originalConnection.getState() == Call.State.WAITING;
161
162        if (mEmergencyTonePlayer != null) {
163            if (state == android.telecom.Connection.STATE_DIALING) {
164                if (isEmergency()) {
165                    mEmergencyTonePlayer.start();
166                }
167            } else {
168                // No need to check if it is an emergency call, since it is a no-op if it
169                // isn't started.
170                mEmergencyTonePlayer.stop();
171            }
172        }
173
174        super.onStateChanged(state);
175    }
176
177    @Override
178    protected int buildConnectionCapabilities() {
179        int capabilities = super.buildConnectionCapabilities();
180        if (mAllowMute) {
181            capabilities |= CAPABILITY_MUTE;
182        }
183        return capabilities;
184    }
185
186    @Override
187    public void performConference(android.telecom.Connection otherConnection) {
188        if (isImsConnection()) {
189            super.performConference(otherConnection);
190        } else {
191            Log.w(this, "Non-IMS CDMA Connection attempted to call performConference.");
192        }
193    }
194
195    void forceAsDialing(boolean isDialing) {
196        if (isDialing) {
197            setStateOverride(Call.State.DIALING);
198        } else {
199            resetStateOverride();
200        }
201    }
202
203    boolean isOutgoing() {
204        return mIsOutgoing;
205    }
206
207    boolean isCallWaiting() {
208        return mIsCallWaiting;
209    }
210
211    /**
212     * We do not get much in the way of confirmation for Cdma call waiting calls. There is no
213     * indication that a rejected call succeeded, a call waiting call has stopped. Instead we
214     * simulate this for the user. We allow TIMEOUT_CALL_WAITING_MILLIS milliseconds before we
215     * assume that the call was missed and reject it ourselves. reject the call automatically.
216     */
217    private void startCallWaitingTimer() {
218        mHandler.sendEmptyMessageDelayed(MSG_CALL_WAITING_MISSED, TIMEOUT_CALL_WAITING_MILLIS);
219    }
220
221    private void hangupCallWaiting(int telephonyDisconnectCause) {
222        Connection originalConnection = getOriginalConnection();
223        if (originalConnection != null) {
224            try {
225                originalConnection.hangup();
226            } catch (CallStateException e) {
227                Log.e(this, e, "Failed to hangup call waiting call");
228            }
229            setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(telephonyDisconnectCause));
230        }
231    }
232
233    /**
234     * Read the settings to determine which type of DTMF method this CDMA phone calls.
235     */
236    private boolean useBurstDtmf() {
237        if (isImsConnection()) {
238            Log.d(this,"in ims call, return false");
239            return false;
240        }
241        int dtmfTypeSetting = Settings.System.getInt(
242                getPhone().getContext().getContentResolver(),
243                Settings.System.DTMF_TONE_TYPE_WHEN_DIALING,
244                SettingsConstants.DTMF_TONE_TYPE_NORMAL);
245        return dtmfTypeSetting == SettingsConstants.DTMF_TONE_TYPE_NORMAL;
246    }
247
248    private void sendShortDtmfToNetwork(char digit) {
249        synchronized(mDtmfQueue) {
250            if (mDtmfBurstConfirmationPending) {
251                mDtmfQueue.add(new Character(digit));
252            } else {
253                sendBurstDtmfStringLocked(Character.toString(digit));
254            }
255        }
256    }
257
258    private void sendBurstDtmfStringLocked(String dtmfString) {
259        getPhone().sendBurstDtmf(
260                dtmfString, 0, 0, mHandler.obtainMessage(MSG_DTMF_SEND_CONFIRMATION));
261        mDtmfBurstConfirmationPending = true;
262    }
263
264    private void handleBurstDtmfConfirmation() {
265        String dtmfDigits = null;
266        synchronized(mDtmfQueue) {
267            mDtmfBurstConfirmationPending = false;
268            if (!mDtmfQueue.isEmpty()) {
269                StringBuilder builder = new StringBuilder(mDtmfQueue.size());
270                while (!mDtmfQueue.isEmpty()) {
271                    builder.append(mDtmfQueue.poll());
272                }
273                dtmfDigits = builder.toString();
274
275                // It would be nice to log the digit, but since DTMF digits can be passwords
276                // to things, or other secure account numbers, we want to keep it away from
277                // the logs.
278                Log.i(this, "%d dtmf character[s] removed from the queue", dtmfDigits.length());
279            }
280            if (dtmfDigits != null) {
281                sendBurstDtmfStringLocked(dtmfDigits);
282            }
283        }
284    }
285
286    private boolean isEmergency() {
287        Phone phone = getPhone();
288        return phone != null &&
289                PhoneNumberUtils.isLocalEmergencyNumber(
290                    phone.getContext(), getAddress().getSchemeSpecificPart());
291    }
292
293    /**
294     * Called when ECM mode is exited; set the connection to allow mute and update the connection
295     * capabilities.
296     */
297    @Override
298    protected void handleExitedEcmMode() {
299        // We allow mute upon existing ECM mode and rebuild the capabilities.
300        mAllowMute = true;
301        super.handleExitedEcmMode();
302    }
303}
304