TelephonyConnectionService.java revision 00bce13d80bfe8f8b2fff17c75fe2b27b57568f6
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.content.ComponentName;
20import android.content.Intent;
21import android.net.Uri;
22import android.telecom.Connection;
23import android.telecom.ConnectionRequest;
24import android.telecom.ConnectionService;
25import android.telecom.DisconnectCause;
26import android.telecom.PhoneAccount;
27import android.telecom.PhoneAccountHandle;
28import android.telephony.PhoneNumberUtils;
29import android.telephony.ServiceState;
30import android.telephony.TelephonyManager;
31import android.text.TextUtils;
32
33import com.android.internal.telephony.Call;
34import com.android.internal.telephony.CallStateException;
35import com.android.internal.telephony.Phone;
36import com.android.internal.telephony.PhoneConstants;
37import com.android.internal.telephony.PhoneFactory;
38import com.android.internal.telephony.PhoneProxy;
39import com.android.internal.telephony.SubscriptionController;
40import com.android.internal.telephony.cdma.CDMAPhone;
41import com.android.phone.MMIDialogActivity;
42
43import java.util.ArrayList;
44import java.util.List;
45import java.util.Objects;
46
47/**
48 * Service for making GSM and CDMA connections.
49 */
50public class TelephonyConnectionService extends ConnectionService {
51    private final TelephonyConferenceController mTelephonyConferenceController =
52            new TelephonyConferenceController(this);
53    private final CdmaConferenceController mCdmaConferenceController =
54            new CdmaConferenceController(this);
55    private final ImsConferenceController mImsConferenceController =
56            new ImsConferenceController(this);
57    private ComponentName mExpectedComponentName = null;
58    private EmergencyCallHelper mEmergencyCallHelper;
59    private EmergencyTonePlayer mEmergencyTonePlayer;
60
61    /**
62     * A listener to actionable events specific to the TelephonyConnection.
63     */
64    private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener =
65            new TelephonyConnection.TelephonyConnectionListener() {
66        @Override
67        public void onOriginalConnectionConfigured(TelephonyConnection c) {
68            addConnectionToConferenceController(c);
69        }
70    };
71
72    @Override
73    public void onCreate() {
74        super.onCreate();
75        mExpectedComponentName = new ComponentName(this, this.getClass());
76        mEmergencyTonePlayer = new EmergencyTonePlayer(this);
77    }
78
79    @Override
80    public Connection onCreateOutgoingConnection(
81            PhoneAccountHandle connectionManagerPhoneAccount,
82            final ConnectionRequest request) {
83        Log.i(this, "onCreateOutgoingConnection, request: " + request);
84
85        Uri handle = request.getAddress();
86        if (handle == null) {
87            Log.d(this, "onCreateOutgoingConnection, handle is null");
88            return Connection.createFailedConnection(
89                    DisconnectCauseUtil.toTelecomDisconnectCause(
90                            android.telephony.DisconnectCause.NO_PHONE_NUMBER_SUPPLIED,
91                            "No phone number supplied"));
92        }
93
94        String scheme = handle.getScheme();
95        final String number;
96        if (PhoneAccount.SCHEME_VOICEMAIL.equals(scheme)) {
97            // TODO: We don't check for SecurityException here (requires
98            // CALL_PRIVILEGED permission).
99            final Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
100            if (phone == null) {
101                Log.d(this, "onCreateOutgoingConnection, phone is null");
102                return Connection.createFailedConnection(
103                        DisconnectCauseUtil.toTelecomDisconnectCause(
104                                android.telephony.DisconnectCause.OUT_OF_SERVICE,
105                                "Phone is null"));
106            }
107            number = phone.getVoiceMailNumber();
108            if (TextUtils.isEmpty(number)) {
109                Log.d(this, "onCreateOutgoingConnection, no voicemail number set.");
110                return Connection.createFailedConnection(
111                        DisconnectCauseUtil.toTelecomDisconnectCause(
112                                android.telephony.DisconnectCause.VOICEMAIL_NUMBER_MISSING,
113                                "Voicemail scheme provided but no voicemail number set."));
114            }
115
116            // Convert voicemail: to tel:
117            handle = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);
118        } else {
119            if (!PhoneAccount.SCHEME_TEL.equals(scheme)) {
120                Log.d(this, "onCreateOutgoingConnection, Handle %s is not type tel", scheme);
121                return Connection.createFailedConnection(
122                        DisconnectCauseUtil.toTelecomDisconnectCause(
123                                android.telephony.DisconnectCause.INVALID_NUMBER,
124                                "Handle scheme is not type tel"));
125            }
126
127            number = handle.getSchemeSpecificPart();
128            if (TextUtils.isEmpty(number)) {
129                Log.d(this, "onCreateOutgoingConnection, unable to parse number");
130                return Connection.createFailedConnection(
131                        DisconnectCauseUtil.toTelecomDisconnectCause(
132                                android.telephony.DisconnectCause.INVALID_NUMBER,
133                                "Unable to parse number"));
134            }
135        }
136
137        boolean isEmergencyNumber = PhoneNumberUtils.isPotentialEmergencyNumber(number);
138
139        // Get the right phone object from the account data passed in.
140        final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber);
141        if (phone == null) {
142            Log.d(this, "onCreateOutgoingConnection, phone is null");
143            return Connection.createFailedConnection(
144                    DisconnectCauseUtil.toTelecomDisconnectCause(
145                            android.telephony.DisconnectCause.OUT_OF_SERVICE, "Phone is null"));
146        }
147
148        int state = phone.getServiceState().getState();
149        boolean useEmergencyCallHelper = false;
150
151        if (isEmergencyNumber) {
152            if (state == ServiceState.STATE_POWER_OFF) {
153                useEmergencyCallHelper = true;
154            }
155        } else {
156            switch (state) {
157                case ServiceState.STATE_IN_SERVICE:
158                case ServiceState.STATE_EMERGENCY_ONLY:
159                    break;
160                case ServiceState.STATE_OUT_OF_SERVICE:
161                    return Connection.createFailedConnection(
162                            DisconnectCauseUtil.toTelecomDisconnectCause(
163                                    android.telephony.DisconnectCause.OUT_OF_SERVICE,
164                                    "ServiceState.STATE_OUT_OF_SERVICE"));
165                case ServiceState.STATE_POWER_OFF:
166                    return Connection.createFailedConnection(
167                            DisconnectCauseUtil.toTelecomDisconnectCause(
168                                    android.telephony.DisconnectCause.POWER_OFF,
169                                    "ServiceState.STATE_POWER_OFF"));
170                default:
171                    Log.d(this, "onCreateOutgoingConnection, unknown service state: %d", state);
172                    return Connection.createFailedConnection(
173                            DisconnectCauseUtil.toTelecomDisconnectCause(
174                                    android.telephony.DisconnectCause.OUTGOING_FAILURE,
175                                    "Unknown service state " + state));
176            }
177        }
178
179        final TelephonyConnection connection =
180                createConnectionFor(phone, null, true /* isOutgoing */);
181        if (connection == null) {
182            return Connection.createFailedConnection(
183                    DisconnectCauseUtil.toTelecomDisconnectCause(
184                            android.telephony.DisconnectCause.OUTGOING_FAILURE,
185                            "Invalid phone type"));
186        }
187        connection.setAddress(handle, PhoneConstants.PRESENTATION_ALLOWED);
188        connection.setInitializing();
189        connection.setVideoState(request.getVideoState());
190
191        if (useEmergencyCallHelper) {
192            if (mEmergencyCallHelper == null) {
193                mEmergencyCallHelper = new EmergencyCallHelper(this);
194            }
195            mEmergencyCallHelper.startTurnOnRadioSequence(phone,
196                    new EmergencyCallHelper.Callback() {
197                        @Override
198                        public void onComplete(boolean isRadioReady) {
199                            if (connection.getState() == Connection.STATE_DISCONNECTED) {
200                                // If the connection has already been disconnected, do nothing.
201                            } else if (isRadioReady) {
202                                connection.setInitialized();
203                                placeOutgoingConnection(connection, phone, request);
204                            } else {
205                                Log.d(this, "onCreateOutgoingConnection, failed to turn on radio");
206                                connection.setDisconnected(
207                                        DisconnectCauseUtil.toTelecomDisconnectCause(
208                                                android.telephony.DisconnectCause.POWER_OFF,
209                                                "Failed to turn on radio."));
210                                connection.destroy();
211                            }
212                        }
213                    });
214
215        } else {
216            placeOutgoingConnection(connection, phone, request);
217        }
218
219        return connection;
220    }
221
222    @Override
223    public Connection onCreateIncomingConnection(
224            PhoneAccountHandle connectionManagerPhoneAccount,
225            ConnectionRequest request) {
226        Log.i(this, "onCreateIncomingConnection, request: " + request);
227
228        Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
229        if (phone == null) {
230            return Connection.createFailedConnection(
231                    DisconnectCauseUtil.toTelecomDisconnectCause(
232                            android.telephony.DisconnectCause.ERROR_UNSPECIFIED));
233        }
234
235        Call call = phone.getRingingCall();
236        if (!call.getState().isRinging()) {
237            Log.i(this, "onCreateIncomingConnection, no ringing call");
238            return Connection.createFailedConnection(
239                    DisconnectCauseUtil.toTelecomDisconnectCause(
240                            android.telephony.DisconnectCause.INCOMING_MISSED,
241                            "Found no ringing call"));
242        }
243
244        com.android.internal.telephony.Connection originalConnection =
245                call.getState() == Call.State.WAITING ?
246                    call.getLatestConnection() : call.getEarliestConnection();
247        if (isOriginalConnectionKnown(originalConnection)) {
248            Log.i(this, "onCreateIncomingConnection, original connection already registered");
249            return Connection.createCanceledConnection();
250        }
251
252        Connection connection =
253                createConnectionFor(phone, originalConnection, false /* isOutgoing */);
254        if (connection == null) {
255            connection = Connection.createCanceledConnection();
256            return Connection.createCanceledConnection();
257        } else {
258            return connection;
259        }
260    }
261
262    @Override
263    public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
264            ConnectionRequest request) {
265        Log.i(this, "onCreateUnknownConnection, request: " + request);
266
267        Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
268        if (phone == null) {
269            return Connection.createFailedConnection(
270                    DisconnectCauseUtil.toTelecomDisconnectCause(
271                            android.telephony.DisconnectCause.ERROR_UNSPECIFIED));
272        }
273
274        final List<com.android.internal.telephony.Connection> allConnections = new ArrayList<>();
275        final Call ringingCall = phone.getRingingCall();
276        if (ringingCall.hasConnections()) {
277            allConnections.addAll(ringingCall.getConnections());
278        }
279        final Call foregroundCall = phone.getForegroundCall();
280        if (foregroundCall.hasConnections()) {
281            allConnections.addAll(foregroundCall.getConnections());
282        }
283        final Call backgroundCall = phone.getBackgroundCall();
284        if (backgroundCall.hasConnections()) {
285            allConnections.addAll(phone.getBackgroundCall().getConnections());
286        }
287
288        com.android.internal.telephony.Connection unknownConnection = null;
289        for (com.android.internal.telephony.Connection telephonyConnection : allConnections) {
290            if (!isOriginalConnectionKnown(telephonyConnection)) {
291                unknownConnection = telephonyConnection;
292                break;
293            }
294        }
295
296        if (unknownConnection == null) {
297            Log.i(this, "onCreateUnknownConnection, did not find previously unknown connection.");
298            return Connection.createCanceledConnection();
299        }
300
301        TelephonyConnection connection =
302                createConnectionFor(phone, unknownConnection,
303                        !unknownConnection.isIncoming() /* isOutgoing */);
304
305        if (connection == null) {
306            return Connection.createCanceledConnection();
307        } else {
308            connection.updateState();
309            return connection;
310        }
311    }
312
313    @Override
314    public void onConference(Connection connection1, Connection connection2) {
315        if (connection1 instanceof TelephonyConnection &&
316                connection2 instanceof TelephonyConnection) {
317            ((TelephonyConnection) connection1).performConference(
318                (TelephonyConnection) connection2);
319        }
320
321    }
322
323    private void placeOutgoingConnection(
324            TelephonyConnection connection, Phone phone, ConnectionRequest request) {
325        String number = connection.getAddress().getSchemeSpecificPart();
326
327        com.android.internal.telephony.Connection originalConnection;
328        try {
329            originalConnection = phone.dial(number, request.getVideoState());
330        } catch (CallStateException e) {
331            Log.e(this, e, "placeOutgoingConnection, phone.dial exception: " + e);
332            connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
333                    android.telephony.DisconnectCause.OUTGOING_FAILURE,
334                    e.getMessage()));
335            return;
336        }
337
338        if (originalConnection == null) {
339            int telephonyDisconnectCause = android.telephony.DisconnectCause.OUTGOING_FAILURE;
340            // On GSM phones, null connection means that we dialed an MMI code
341            if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) {
342                Log.d(this, "dialed MMI code");
343                telephonyDisconnectCause = android.telephony.DisconnectCause.DIALED_MMI;
344                final Intent intent = new Intent(this, MMIDialogActivity.class);
345                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
346                        Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
347                startActivity(intent);
348            }
349            Log.d(this, "placeOutgoingConnection, phone.dial returned null");
350            connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
351                    telephonyDisconnectCause, "Connection is null"));
352        } else {
353            connection.setOriginalConnection(originalConnection);
354        }
355    }
356
357    private TelephonyConnection createConnectionFor(
358            Phone phone,
359            com.android.internal.telephony.Connection originalConnection,
360            boolean isOutgoing) {
361        TelephonyConnection returnConnection = null;
362        int phoneType = phone.getPhoneType();
363        if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
364            returnConnection = new GsmConnection(originalConnection);
365        } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
366            boolean allowMute = allowMute(phone);
367            returnConnection = new CdmaConnection(
368                    originalConnection, mEmergencyTonePlayer, allowMute, isOutgoing);
369        }
370        if (returnConnection != null) {
371            // Listen to Telephony specific callbacks from the connection
372            returnConnection.addTelephonyConnectionListener(mTelephonyConnectionListener);
373        }
374        return returnConnection;
375    }
376
377    private boolean isOriginalConnectionKnown(
378            com.android.internal.telephony.Connection originalConnection) {
379        for (Connection connection : getAllConnections()) {
380            if (connection instanceof TelephonyConnection) {
381                TelephonyConnection telephonyConnection = (TelephonyConnection) connection;
382                if (telephonyConnection.getOriginalConnection() == originalConnection) {
383                    return true;
384                }
385            }
386        }
387        return false;
388    }
389
390    private Phone getPhoneForAccount(PhoneAccountHandle accountHandle, boolean isEmergency) {
391        if (isEmergency) {
392            return PhoneFactory.getDefaultPhone();
393        }
394
395        if (Objects.equals(mExpectedComponentName, accountHandle.getComponentName())) {
396            if (accountHandle.getId() != null) {
397                try {
398                    int phoneId = SubscriptionController.getInstance().getPhoneId(
399                            Integer.parseInt(accountHandle.getId()));
400                    return PhoneFactory.getPhone(phoneId);
401                } catch (NumberFormatException e) {
402                    Log.w(this, "Could not get subId from account: " + accountHandle.getId());
403                }
404            }
405        }
406        return null;
407    }
408
409    /**
410     * Determines if the connection should allow mute.
411     *
412     * @param phone The current phone.
413     * @return {@code True} if the connection should allow mute.
414     */
415    private boolean allowMute(Phone phone) {
416        // For CDMA phones, check if we are in Emergency Callback Mode (ECM).  Mute is disallowed
417        // in ECM mode.
418        if (phone.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
419            PhoneProxy phoneProxy = (PhoneProxy)phone;
420            CDMAPhone cdmaPhone = (CDMAPhone)phoneProxy.getActivePhone();
421            if (cdmaPhone != null) {
422                if (cdmaPhone.isInEcm()) {
423                    return false;
424                }
425            }
426        }
427
428        return true;
429    }
430
431    @Override
432    public void removeConnection(Connection connection) {
433        super.removeConnection(connection);
434        if (connection instanceof TelephonyConnection) {
435            TelephonyConnection telephonyConnection = (TelephonyConnection) connection;
436            telephonyConnection.removeTelephonyConnectionListener(mTelephonyConnectionListener);
437        }
438    }
439
440    /**
441     * When a {@link TelephonyConnection} has its underlying original connection configured,
442     * we need to add it to the correct conference controller.
443     *
444     * @param connection The connection to be added to the controller
445     */
446    public void addConnectionToConferenceController(TelephonyConnection connection) {
447        // TODO: Do we need to handle the case of the original connection changing
448        // and triggering this callback multiple times for the same connection?
449        // If that is the case, we might want to remove this connection from all
450        // conference controllers first before re-adding it.
451        if (connection.isImsConnection()) {
452            Log.d(this, "Adding IMS connection to conference controller: " + connection);
453            mImsConferenceController.add(connection);
454        } else {
455            int phoneType = connection.getCall().getPhone().getPhoneType();
456            if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
457                Log.d(this, "Adding GSM connection to conference controller: " + connection);
458                mTelephonyConferenceController.add(connection);
459            } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA &&
460                    connection instanceof CdmaConnection) {
461                Log.d(this, "Adding CDMA connection to conference controller: " + connection);
462                mCdmaConferenceController.add((CdmaConnection)connection);
463            }
464        }
465    }
466}
467