TelephonyConnectionService.java revision 87050c69ed4c62d5fddc494eb1a9b85a23d78214
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.PhoneAccount;
26import android.telecom.PhoneAccountHandle;
27import android.telephony.PhoneNumberUtils;
28import android.telephony.ServiceState;
29import android.telephony.TelephonyManager;
30import android.text.TextUtils;
31
32import com.android.internal.telephony.Call;
33import com.android.internal.telephony.CallStateException;
34import com.android.internal.telephony.Phone;
35import com.android.internal.telephony.PhoneConstants;
36import com.android.internal.telephony.PhoneFactory;
37import com.android.internal.telephony.PhoneProxy;
38import com.android.internal.telephony.SubscriptionController;
39import com.android.internal.telephony.cdma.CDMAPhone;
40import com.android.phone.MMIDialogActivity;
41
42import java.util.ArrayList;
43import java.util.List;
44import java.util.Objects;
45
46/**
47 * Service for making GSM and CDMA connections.
48 */
49public class TelephonyConnectionService extends ConnectionService {
50    private final TelephonyConferenceController mTelephonyConferenceController =
51            new TelephonyConferenceController(this);
52    private final CdmaConferenceController mCdmaConferenceController =
53            new CdmaConferenceController(this);
54    private final ImsConferenceController mImsConferenceController =
55            new ImsConferenceController(this);
56    private ComponentName mExpectedComponentName = null;
57    private EmergencyCallHelper mEmergencyCallHelper;
58    private EmergencyTonePlayer mEmergencyTonePlayer;
59
60    /**
61     * A listener to actionable events specific to the TelephonyConnection.
62     */
63    private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener =
64            new TelephonyConnection.TelephonyConnectionListener() {
65        @Override
66        public void onOriginalConnectionConfigured(TelephonyConnection c) {
67            addConnectionToConferenceController(c);
68        }
69    };
70
71    @Override
72    public void onCreate() {
73        super.onCreate();
74        mExpectedComponentName = new ComponentName(this, this.getClass());
75        mEmergencyTonePlayer = new EmergencyTonePlayer(this);
76        TelecomAccountRegistry.getInstance(this).setTelephonyConnectionService(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        // Check both voice & data RAT to enable normal CS call,
149        // when voice RAT is OOS but Data RAT is present.
150        int state = phone.getServiceState().getState();
151        if (state == ServiceState.STATE_OUT_OF_SERVICE) {
152            state = phone.getServiceState().getDataRegState();
153        }
154        boolean useEmergencyCallHelper = false;
155
156        if (isEmergencyNumber) {
157            if (!phone.isRadioOn()) {
158                useEmergencyCallHelper = true;
159            }
160        } else {
161            switch (state) {
162                case ServiceState.STATE_IN_SERVICE:
163                case ServiceState.STATE_EMERGENCY_ONLY:
164                    break;
165                case ServiceState.STATE_OUT_OF_SERVICE:
166                    return Connection.createFailedConnection(
167                            DisconnectCauseUtil.toTelecomDisconnectCause(
168                                    android.telephony.DisconnectCause.OUT_OF_SERVICE,
169                                    "ServiceState.STATE_OUT_OF_SERVICE"));
170                case ServiceState.STATE_POWER_OFF:
171                    return Connection.createFailedConnection(
172                            DisconnectCauseUtil.toTelecomDisconnectCause(
173                                    android.telephony.DisconnectCause.POWER_OFF,
174                                    "ServiceState.STATE_POWER_OFF"));
175                default:
176                    Log.d(this, "onCreateOutgoingConnection, unknown service state: %d", state);
177                    return Connection.createFailedConnection(
178                            DisconnectCauseUtil.toTelecomDisconnectCause(
179                                    android.telephony.DisconnectCause.OUTGOING_FAILURE,
180                                    "Unknown service state " + state));
181            }
182        }
183
184        final TelephonyConnection connection =
185                createConnectionFor(phone, null, true /* isOutgoing */);
186        if (connection == null) {
187            return Connection.createFailedConnection(
188                    DisconnectCauseUtil.toTelecomDisconnectCause(
189                            android.telephony.DisconnectCause.OUTGOING_FAILURE,
190                            "Invalid phone type"));
191        }
192        connection.setAddress(handle, PhoneConstants.PRESENTATION_ALLOWED);
193        connection.setInitializing();
194        connection.setVideoState(request.getVideoState());
195
196        if (useEmergencyCallHelper) {
197            if (mEmergencyCallHelper == null) {
198                mEmergencyCallHelper = new EmergencyCallHelper(this);
199            }
200            mEmergencyCallHelper.startTurnOnRadioSequence(phone,
201                    new EmergencyCallHelper.Callback() {
202                        @Override
203                        public void onComplete(boolean isRadioReady) {
204                            if (connection.getState() == Connection.STATE_DISCONNECTED) {
205                                // If the connection has already been disconnected, do nothing.
206                            } else if (isRadioReady) {
207                                connection.setInitialized();
208                                placeOutgoingConnection(connection, phone, request);
209                            } else {
210                                Log.d(this, "onCreateOutgoingConnection, failed to turn on radio");
211                                connection.setDisconnected(
212                                        DisconnectCauseUtil.toTelecomDisconnectCause(
213                                                android.telephony.DisconnectCause.POWER_OFF,
214                                                "Failed to turn on radio."));
215                                connection.destroy();
216                            }
217                        }
218                    });
219
220        } else {
221            placeOutgoingConnection(connection, phone, request);
222        }
223
224        return connection;
225    }
226
227    @Override
228    public Connection onCreateIncomingConnection(
229            PhoneAccountHandle connectionManagerPhoneAccount,
230            ConnectionRequest request) {
231        Log.i(this, "onCreateIncomingConnection, request: " + request);
232
233        Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
234        if (phone == null) {
235            return Connection.createFailedConnection(
236                    DisconnectCauseUtil.toTelecomDisconnectCause(
237                            android.telephony.DisconnectCause.ERROR_UNSPECIFIED));
238        }
239
240        Call call = phone.getRingingCall();
241        if (!call.getState().isRinging()) {
242            Log.i(this, "onCreateIncomingConnection, no ringing call");
243            return Connection.createFailedConnection(
244                    DisconnectCauseUtil.toTelecomDisconnectCause(
245                            android.telephony.DisconnectCause.INCOMING_MISSED,
246                            "Found no ringing call"));
247        }
248
249        com.android.internal.telephony.Connection originalConnection =
250                call.getState() == Call.State.WAITING ?
251                    call.getLatestConnection() : call.getEarliestConnection();
252        if (isOriginalConnectionKnown(originalConnection)) {
253            Log.i(this, "onCreateIncomingConnection, original connection already registered");
254            return Connection.createCanceledConnection();
255        }
256
257        Connection connection =
258                createConnectionFor(phone, originalConnection, false /* isOutgoing */);
259        if (connection == null) {
260            connection = Connection.createCanceledConnection();
261            return Connection.createCanceledConnection();
262        } else {
263            return connection;
264        }
265    }
266
267    @Override
268    public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
269            ConnectionRequest request) {
270        Log.i(this, "onCreateUnknownConnection, request: " + request);
271
272        Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
273        if (phone == null) {
274            return Connection.createFailedConnection(
275                    DisconnectCauseUtil.toTelecomDisconnectCause(
276                            android.telephony.DisconnectCause.ERROR_UNSPECIFIED));
277        }
278
279        final List<com.android.internal.telephony.Connection> allConnections = new ArrayList<>();
280        final Call ringingCall = phone.getRingingCall();
281        if (ringingCall.hasConnections()) {
282            allConnections.addAll(ringingCall.getConnections());
283        }
284        final Call foregroundCall = phone.getForegroundCall();
285        if (foregroundCall.hasConnections()) {
286            allConnections.addAll(foregroundCall.getConnections());
287        }
288        final Call backgroundCall = phone.getBackgroundCall();
289        if (backgroundCall.hasConnections()) {
290            allConnections.addAll(phone.getBackgroundCall().getConnections());
291        }
292
293        com.android.internal.telephony.Connection unknownConnection = null;
294        for (com.android.internal.telephony.Connection telephonyConnection : allConnections) {
295            if (!isOriginalConnectionKnown(telephonyConnection)) {
296                unknownConnection = telephonyConnection;
297                break;
298            }
299        }
300
301        if (unknownConnection == null) {
302            Log.i(this, "onCreateUnknownConnection, did not find previously unknown connection.");
303            return Connection.createCanceledConnection();
304        }
305
306        TelephonyConnection connection =
307                createConnectionFor(phone, unknownConnection,
308                        !unknownConnection.isIncoming() /* isOutgoing */);
309
310        if (connection == null) {
311            return Connection.createCanceledConnection();
312        } else {
313            connection.updateState();
314            return connection;
315        }
316    }
317
318    @Override
319    public void onConference(Connection connection1, Connection connection2) {
320        if (connection1 instanceof TelephonyConnection &&
321                connection2 instanceof TelephonyConnection) {
322            ((TelephonyConnection) connection1).performConference(
323                (TelephonyConnection) connection2);
324        }
325
326    }
327
328    private void placeOutgoingConnection(
329            TelephonyConnection connection, Phone phone, ConnectionRequest request) {
330        String number = connection.getAddress().getSchemeSpecificPart();
331
332        com.android.internal.telephony.Connection originalConnection;
333        try {
334            originalConnection = phone.dial(number, request.getVideoState());
335        } catch (CallStateException e) {
336            Log.e(this, e, "placeOutgoingConnection, phone.dial exception: " + e);
337            int cause = android.telephony.DisconnectCause.OUTGOING_FAILURE;
338            if (e.getError() == CallStateException.ERROR_DISCONNECTED) {
339                cause = android.telephony.DisconnectCause.OUT_OF_SERVICE;
340            }
341            connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
342                    cause, e.getMessage()));
343            return;
344        }
345
346        if (originalConnection == null) {
347            int telephonyDisconnectCause = android.telephony.DisconnectCause.OUTGOING_FAILURE;
348            // On GSM phones, null connection means that we dialed an MMI code
349            if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) {
350                Log.d(this, "dialed MMI code");
351                telephonyDisconnectCause = android.telephony.DisconnectCause.DIALED_MMI;
352                final Intent intent = new Intent(this, MMIDialogActivity.class);
353                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
354                        Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
355                startActivity(intent);
356            }
357            Log.d(this, "placeOutgoingConnection, phone.dial returned null");
358            connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
359                    telephonyDisconnectCause, "Connection is null"));
360        } else {
361            connection.setOriginalConnection(originalConnection);
362        }
363    }
364
365    private TelephonyConnection createConnectionFor(
366            Phone phone,
367            com.android.internal.telephony.Connection originalConnection,
368            boolean isOutgoing) {
369        TelephonyConnection returnConnection = null;
370        int phoneType = phone.getPhoneType();
371        if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
372            returnConnection = new GsmConnection(originalConnection);
373        } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
374            boolean allowMute = allowMute(phone);
375            returnConnection = new CdmaConnection(
376                    originalConnection, mEmergencyTonePlayer, allowMute, isOutgoing);
377        }
378        if (returnConnection != null) {
379            // Listen to Telephony specific callbacks from the connection
380            returnConnection.addTelephonyConnectionListener(mTelephonyConnectionListener);
381        }
382        return returnConnection;
383    }
384
385    private boolean isOriginalConnectionKnown(
386            com.android.internal.telephony.Connection originalConnection) {
387        for (Connection connection : getAllConnections()) {
388            if (connection instanceof TelephonyConnection) {
389                TelephonyConnection telephonyConnection = (TelephonyConnection) connection;
390                if (telephonyConnection.getOriginalConnection() == originalConnection) {
391                    return true;
392                }
393            }
394        }
395        return false;
396    }
397
398    private Phone getPhoneForAccount(PhoneAccountHandle accountHandle, boolean isEmergency) {
399        if (Objects.equals(mExpectedComponentName, accountHandle.getComponentName())) {
400            if (accountHandle.getId() != null) {
401                try {
402                    int phoneId = SubscriptionController.getInstance().getPhoneId(
403                            Integer.parseInt(accountHandle.getId()));
404                    return PhoneFactory.getPhone(phoneId);
405                } catch (NumberFormatException e) {
406                    Log.w(this, "Could not get subId from account: " + accountHandle.getId());
407                }
408            }
409        }
410
411        if (isEmergency) {
412            // If this is an emergency number and we've been asked to dial it using a PhoneAccount
413            // which does not exist, then default to whatever subscription is available currently.
414            return getFirstPhoneForEmergencyCall();
415        }
416
417        return null;
418    }
419
420    private Phone getFirstPhoneForEmergencyCall() {
421        Phone selectPhone = null;
422        for (int i = 0; i < TelephonyManager.getDefault().getSimCount(); i++) {
423            int[] subIds = SubscriptionController.getInstance().getSubIdUsingSlotId(i);
424            if (subIds.length == 0)
425                continue;
426
427            int phoneId = SubscriptionController.getInstance().getPhoneId(subIds[0]);
428            Phone phone = PhoneFactory.getPhone(phoneId);
429            if (phone == null)
430                continue;
431
432            if (ServiceState.STATE_IN_SERVICE == phone.getServiceState().getState()) {
433                // the slot is radio on & state is in service
434                Log.d(this, "pickBestPhoneForEmergencyCall, radio on & in service, slotId:" + i);
435                return phone;
436            } else if (ServiceState.STATE_POWER_OFF != phone.getServiceState().getState()) {
437                // the slot is radio on & with SIM card inserted.
438                if (TelephonyManager.getDefault().hasIccCard(i)) {
439                    Log.d(this, "pickBestPhoneForEmergencyCall," +
440                            "radio on and SIM card inserted, slotId:" + i);
441                    selectPhone = phone;
442                } else if (selectPhone == null) {
443                    Log.d(this, "pickBestPhoneForEmergencyCall, radio on, slotId:" + i);
444                    selectPhone = phone;
445                }
446            }
447        }
448
449        if (selectPhone == null) {
450            Log.d(this, "pickBestPhoneForEmergencyCall, return default phone");
451            selectPhone = PhoneFactory.getDefaultPhone();
452        }
453
454        return selectPhone;
455    }
456
457    /**
458     * Determines if the connection should allow mute.
459     *
460     * @param phone The current phone.
461     * @return {@code True} if the connection should allow mute.
462     */
463    private boolean allowMute(Phone phone) {
464        // For CDMA phones, check if we are in Emergency Callback Mode (ECM).  Mute is disallowed
465        // in ECM mode.
466        if (phone.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
467            PhoneProxy phoneProxy = (PhoneProxy)phone;
468            CDMAPhone cdmaPhone = (CDMAPhone)phoneProxy.getActivePhone();
469            if (cdmaPhone != null) {
470                if (cdmaPhone.isInEcm()) {
471                    return false;
472                }
473            }
474        }
475
476        return true;
477    }
478
479    @Override
480    public void removeConnection(Connection connection) {
481        super.removeConnection(connection);
482        if (connection instanceof TelephonyConnection) {
483            TelephonyConnection telephonyConnection = (TelephonyConnection) connection;
484            telephonyConnection.removeTelephonyConnectionListener(mTelephonyConnectionListener);
485        }
486    }
487
488    /**
489     * When a {@link TelephonyConnection} has its underlying original connection configured,
490     * we need to add it to the correct conference controller.
491     *
492     * @param connection The connection to be added to the controller
493     */
494    public void addConnectionToConferenceController(TelephonyConnection connection) {
495        // TODO: Do we need to handle the case of the original connection changing
496        // and triggering this callback multiple times for the same connection?
497        // If that is the case, we might want to remove this connection from all
498        // conference controllers first before re-adding it.
499        if (connection.isImsConnection()) {
500            Log.d(this, "Adding IMS connection to conference controller: " + connection);
501            mImsConferenceController.add(connection);
502        } else {
503            int phoneType = connection.getCall().getPhone().getPhoneType();
504            if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
505                Log.d(this, "Adding GSM connection to conference controller: " + connection);
506                mTelephonyConferenceController.add(connection);
507            } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA &&
508                    connection instanceof CdmaConnection) {
509                Log.d(this, "Adding CDMA connection to conference controller: " + connection);
510                mCdmaConferenceController.add((CdmaConnection)connection);
511            }
512        }
513    }
514}
515