TelephonyConnection.java revision 3199aa7f8bcb48569eb8289abc055ba0f8496ba8
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.AsyncResult;
20import android.os.Handler;
21import android.os.Message;
22import android.telecomm.CallAudioState;
23import android.telephony.DisconnectCause;
24
25import com.android.internal.telephony.Call;
26import com.android.internal.telephony.CallStateException;
27import com.android.internal.telephony.Connection.PostDialListener;
28import com.android.internal.telephony.Phone;
29import android.telecomm.Connection;
30
31import java.util.List;
32
33/**
34 * Base class for CDMA and GSM connections.
35 */
36abstract class TelephonyConnection extends Connection {
37    private static final int MSG_PRECISE_CALL_STATE_CHANGED = 1;
38    private static final int MSG_RINGBACK_TONE = 2;
39
40    private final Handler mHandler = new Handler() {
41        @Override
42        public void handleMessage(Message msg) {
43            // TODO: This code assumes that there is only one connection in the foreground call,
44            // in other words, it punts on network-mediated conference calling.
45            if (getOriginalConnection() != getForegroundConnection()) {
46                Log.v(TelephonyConnection.this, "handleMessage, original connection is not " +
47                        "foreground connection, skipping");
48                return;
49            }
50
51            switch (msg.what) {
52                case MSG_PRECISE_CALL_STATE_CHANGED:
53                    Log.v(TelephonyConnection.this, "MSG_PRECISE_CALL_STATE_CHANGED");
54                    updateState();
55                    break;
56                case MSG_RINGBACK_TONE:
57                    Log.v(TelephonyConnection.this, "MSG_RINGBACK_TONE");
58                    setRequestingRingback((Boolean) ((AsyncResult) msg.obj).result);
59                    break;
60            }
61        }
62    };
63
64    private final PostDialListener mPostDialListener = new PostDialListener() {
65        @Override
66        public void onPostDialWait() {
67            Log.v(TelephonyConnection.this, "onPostDialWait");
68            if (mOriginalConnection != null) {
69                setPostDialWait(mOriginalConnection.getRemainingPostDialString());
70            }
71        }
72    };
73
74    private com.android.internal.telephony.Connection mOriginalConnection;
75    private Call.State mOriginalConnectionState = Call.State.IDLE;
76
77    protected TelephonyConnection(com.android.internal.telephony.Connection originalConnection) {
78        Log.v(this, "new TelephonyConnection, originalConnection: " + originalConnection);
79        mOriginalConnection = originalConnection;
80        getPhone().registerForPreciseCallStateChanged(
81                mHandler, MSG_PRECISE_CALL_STATE_CHANGED, null);
82        getPhone().registerForRingbackTone(mHandler, MSG_RINGBACK_TONE, null);
83        mOriginalConnection.addPostDialListener(mPostDialListener);
84        updateState();
85    }
86
87    @Override
88    protected void onSetAudioState(CallAudioState audioState) {
89        // TODO: update TTY mode.
90        if (getPhone() != null) {
91            getPhone().setEchoSuppressionEnabled();
92        }
93    }
94
95    @Override
96    protected void onSetState(int state) {
97        Log.v(this, "onSetState, state: " + Connection.stateToString(state));
98    }
99
100    @Override
101    protected void onDisconnect() {
102        Log.v(this, "onDisconnect");
103        hangup(DisconnectCause.LOCAL);
104    }
105
106    @Override
107    protected void onSeparate() {
108        Log.v(this, "onSeparate");
109        if (mOriginalConnection != null) {
110            try {
111                mOriginalConnection.separate();
112            } catch (CallStateException e) {
113                Log.e(this, e, "Call to Connection.separate failed with exception");
114            }
115        }
116    }
117
118    @Override
119    protected void onAbort() {
120        Log.v(this, "onAbort");
121        hangup(DisconnectCause.LOCAL);
122    }
123
124    @Override
125    protected void onHold() {
126        Log.v(this, "onHold");
127        // TODO(santoscordon): Can dialing calls be put on hold as well since they take up the
128        // foreground call slot?
129        if (Call.State.ACTIVE == mOriginalConnectionState) {
130            Log.v(this, "Holding active call");
131            try {
132                Phone phone = mOriginalConnection.getCall().getPhone();
133                Call ringingCall = phone.getRingingCall();
134
135                // Although the method says switchHoldingAndActive, it eventually calls a RIL method
136                // called switchWaitingOrHoldingAndActive. What this means is that if we try to put
137                // a call on hold while a call-waiting call exists, it'll end up accepting the
138                // call-waiting call, which is bad if that was not the user's intention. We are
139                // cheating here and simply skipping it because we know any attempt to hold a call
140                // while a call-waiting call is happening is likely a request from Telecomm prior to
141                // accepting the call-waiting call.
142                // TODO(santoscordon): Investigate a better solution. It would be great here if we
143                // could "fake" hold by silencing the audio and microphone streams for this call
144                // instead of actually putting it on hold.
145                if (ringingCall.getState() != Call.State.WAITING) {
146                    phone.switchHoldingAndActive();
147                }
148
149                // TODO(santoscordon): Cdma calls are slightly different.
150            } catch (CallStateException e) {
151                Log.e(this, e, "Exception occurred while trying to put call on hold.");
152            }
153        } else {
154            Log.w(this, "Cannot put a call that is not currently active on hold.");
155        }
156    }
157
158    @Override
159    protected void onUnhold() {
160        Log.v(this, "onUnhold");
161        if (Call.State.HOLDING == mOriginalConnectionState) {
162            try {
163                // TODO: This doesn't handle multiple calls across connection services yet
164                mOriginalConnection.getCall().getPhone().switchHoldingAndActive();
165            } catch (CallStateException e) {
166                Log.e(this, e, "Exception occurred while trying to release call from hold.");
167            }
168        } else {
169            Log.w(this, "Cannot release a call that is not already on hold from hold.");
170        }
171    }
172
173    @Override
174    protected void onAnswer() {
175        Log.v(this, "onAnswer");
176        // TODO(santoscordon): Tons of hairy logic is missing here around multiple active calls on
177        // CDMA devices. See {@link CallManager.acceptCall}.
178
179        if (isValidRingingCall() && getPhone() != null) {
180            try {
181                getPhone().acceptCall();
182            } catch (CallStateException e) {
183                Log.e(this, e, "Failed to accept call.");
184            }
185        }
186    }
187
188    @Override
189    protected void onReject() {
190        Log.v(this, "onReject");
191        if (isValidRingingCall()) {
192            hangup(DisconnectCause.INCOMING_REJECTED);
193        }
194        super.onReject();
195    }
196
197    @Override
198    protected void onPostDialContinue(boolean proceed) {
199        Log.v(this, "onPostDialContinue, proceed: " + proceed);
200        if (mOriginalConnection != null) {
201            if (proceed) {
202                mOriginalConnection.proceedAfterWaitChar();
203            } else {
204                mOriginalConnection.cancelPostDial();
205            }
206        }
207    }
208
209    @Override
210    protected void onSwapWithBackgroundCall() {
211        Log.v(this, "onSwapWithBackgroundCall");
212    }
213
214    @Override
215    protected void onChildrenChanged(List<Connection> children) {
216        Log.v(this, "onChildrenChanged, children: " + children);
217    }
218
219    @Override
220    protected void onPhoneAccountClicked() {
221        Log.v(this, "onPhoneAccountClicked");
222    }
223
224    protected abstract int buildCallCapabilities();
225
226    final void updateCallCapabilities() {
227        int newCallCapabilities = buildCallCapabilities();
228        if (getCallCapabilities() != newCallCapabilities) {
229            setCallCapabilities(newCallCapabilities);
230        }
231    }
232
233    void onAddedToCallService() {
234        updateCallCapabilities();
235        if (mOriginalConnection != null) {
236            setCallerDisplayName(
237                    mOriginalConnection.getCnapName(),
238                    mOriginalConnection.getCnapNamePresentation());
239        }
240    }
241
242    void onRemovedFromCallService() {
243    }
244
245    private void hangup(int disconnectCause) {
246        if (mOriginalConnection != null) {
247            try {
248                Call call = mOriginalConnection.getCall();
249                if (call != null && !call.isMultiparty()) {
250                    call.hangup();
251                } else {
252                    mOriginalConnection.hangup();
253                }
254                // Set state deliberately since we are going to close() and will no longer be
255                // listening to state updates from mOriginalConnection
256                setDisconnected(disconnectCause, null);
257            } catch (CallStateException e) {
258                Log.e(this, e, "Call to Connection.hangup failed with exception");
259            }
260        }
261        close();
262    }
263
264    com.android.internal.telephony.Connection getOriginalConnection() {
265        return mOriginalConnection;
266    }
267
268    protected Call getCall() {
269        if (mOriginalConnection != null) {
270            return mOriginalConnection.getCall();
271        }
272        return null;
273    }
274
275    Phone getPhone() {
276        Call call = getCall();
277        if (call != null) {
278            return call.getPhone();
279        }
280        return null;
281    }
282
283    private com.android.internal.telephony.Connection getForegroundConnection() {
284        if (getPhone() != null) {
285            return getPhone().getForegroundCall().getEarliestConnection();
286        }
287        return null;
288    }
289
290    /**
291     * Checks to see the original connection corresponds to an active incoming call. Returns false
292     * if there is no such actual call, or if the associated call is not incoming (See
293     * {@link Call.State#isRinging}).
294     */
295    private boolean isValidRingingCall() {
296        if (getPhone() == null) {
297            Log.v(this, "isValidRingingCall, phone is null");
298            return false;
299        }
300
301        Call ringingCall = getPhone().getRingingCall();
302        if (!ringingCall.getState().isRinging()) {
303            Log.v(this, "isValidRingingCall, ringing call is not in ringing state");
304            return false;
305        }
306
307        if (ringingCall.getEarliestConnection() != mOriginalConnection) {
308            Log.v(this, "isValidRingingCall, ringing call connection does not match");
309            return false;
310        }
311
312        Log.v(this, "isValidRingingCall, returning true");
313        return true;
314    }
315
316    private void updateState() {
317        if (mOriginalConnection == null) {
318            return;
319        }
320
321        Call.State newState = mOriginalConnection.getState();
322        Log.v(this, "Update state from %s to %s for %s", mOriginalConnectionState, newState, this);
323        if (mOriginalConnectionState != newState) {
324            mOriginalConnectionState = newState;
325            switch (newState) {
326                case IDLE:
327                    break;
328                case ACTIVE:
329                    setActive();
330                    break;
331                case HOLDING:
332                    setOnHold();
333                    break;
334                case DIALING:
335                case ALERTING:
336                    setDialing();
337                    break;
338                case INCOMING:
339                case WAITING:
340                    setRinging();
341                    break;
342                case DISCONNECTED:
343                    setDisconnected(mOriginalConnection.getDisconnectCause(), null);
344                    break;
345                case DISCONNECTING:
346                    break;
347            }
348            updateCallCapabilities();
349        }
350    }
351
352    private void close() {
353        Log.v(this, "close");
354        if (getPhone() != null) {
355            getPhone().unregisterForPreciseCallStateChanged(mHandler);
356            getPhone().unregisterForRingbackTone(mHandler);
357        }
358        mOriginalConnection = null;
359        setDestroyed();
360    }
361}
362