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