TelephonyConnectionService.java revision 64220bcfad27244b223f9c4377b85e1be95ca328
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.Context;
21import android.content.Intent;
22import android.net.Uri;
23import android.telecom.Connection;
24import android.telecom.ConnectionRequest;
25import android.telecom.ConnectionService;
26import android.telecom.PhoneAccount;
27import android.telecom.PhoneAccountHandle;
28import android.telephony.CarrierConfigManager;
29import android.telephony.PhoneNumberUtils;
30import android.telephony.ServiceState;
31import android.telephony.SubscriptionManager;
32import android.telephony.TelephonyManager;
33import android.text.TextUtils;
34
35import com.android.internal.telephony.Call;
36import com.android.internal.telephony.CallStateException;
37import com.android.internal.telephony.Phone;
38import com.android.internal.telephony.PhoneConstants;
39import com.android.internal.telephony.PhoneFactory;
40import com.android.internal.telephony.PhoneProxy;
41import com.android.internal.telephony.SubscriptionController;
42import com.android.internal.telephony.cdma.CDMAPhone;
43import com.android.phone.MMIDialogActivity;
44import com.android.phone.PhoneUtils;
45import com.android.phone.R;
46
47import java.util.ArrayList;
48import java.util.List;
49import java.util.regex.Pattern;
50
51/**
52 * Service for making GSM and CDMA connections.
53 */
54public class TelephonyConnectionService extends ConnectionService {
55
56    // If configured, reject attempts to dial numbers matching this pattern.
57    private static final Pattern CDMA_ACTIVATION_CODE_REGEX_PATTERN =
58            Pattern.compile("\\*228[0-9]{0,2}");
59
60    private final TelephonyConferenceController mTelephonyConferenceController =
61            new TelephonyConferenceController(this);
62    private final CdmaConferenceController mCdmaConferenceController =
63            new CdmaConferenceController(this);
64    private final ImsConferenceController mImsConferenceController =
65            new ImsConferenceController(this);
66
67    private ComponentName mExpectedComponentName = null;
68    private EmergencyCallHelper mEmergencyCallHelper;
69    private EmergencyTonePlayer mEmergencyTonePlayer;
70
71    /**
72     * A listener to actionable events specific to the TelephonyConnection.
73     */
74    private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener =
75            new TelephonyConnection.TelephonyConnectionListener() {
76        @Override
77        public void onOriginalConnectionConfigured(TelephonyConnection c) {
78            addConnectionToConferenceController(c);
79        }
80    };
81
82    @Override
83    public void onCreate() {
84        super.onCreate();
85        mExpectedComponentName = new ComponentName(this, this.getClass());
86        mEmergencyTonePlayer = new EmergencyTonePlayer(this);
87        TelecomAccountRegistry.getInstance(this).setTelephonyConnectionService(this);
88    }
89
90    @Override
91    public Connection onCreateOutgoingConnection(
92            PhoneAccountHandle connectionManagerPhoneAccount,
93            final ConnectionRequest request) {
94        Log.i(this, "onCreateOutgoingConnection, request: " + request);
95
96        Uri handle = request.getAddress();
97        if (handle == null) {
98            Log.d(this, "onCreateOutgoingConnection, handle is null");
99            return Connection.createFailedConnection(
100                    DisconnectCauseUtil.toTelecomDisconnectCause(
101                            android.telephony.DisconnectCause.NO_PHONE_NUMBER_SUPPLIED,
102                            "No phone number supplied"));
103        }
104
105        String scheme = handle.getScheme();
106        final String number;
107        if (PhoneAccount.SCHEME_VOICEMAIL.equals(scheme)) {
108            // TODO: We don't check for SecurityException here (requires
109            // CALL_PRIVILEGED permission).
110            final Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
111            if (phone == null) {
112                Log.d(this, "onCreateOutgoingConnection, phone is null");
113                return Connection.createFailedConnection(
114                        DisconnectCauseUtil.toTelecomDisconnectCause(
115                                android.telephony.DisconnectCause.OUT_OF_SERVICE,
116                                "Phone is null"));
117            }
118            number = phone.getVoiceMailNumber();
119            if (TextUtils.isEmpty(number)) {
120                Log.d(this, "onCreateOutgoingConnection, no voicemail number set.");
121                return Connection.createFailedConnection(
122                        DisconnectCauseUtil.toTelecomDisconnectCause(
123                                android.telephony.DisconnectCause.VOICEMAIL_NUMBER_MISSING,
124                                "Voicemail scheme provided but no voicemail number set."));
125            }
126
127            // Convert voicemail: to tel:
128            handle = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);
129        } else {
130            if (!PhoneAccount.SCHEME_TEL.equals(scheme)) {
131                Log.d(this, "onCreateOutgoingConnection, Handle %s is not type tel", scheme);
132                return Connection.createFailedConnection(
133                        DisconnectCauseUtil.toTelecomDisconnectCause(
134                                android.telephony.DisconnectCause.INVALID_NUMBER,
135                                "Handle scheme is not type tel"));
136            }
137
138            number = handle.getSchemeSpecificPart();
139            if (TextUtils.isEmpty(number)) {
140                Log.d(this, "onCreateOutgoingConnection, unable to parse number");
141                return Connection.createFailedConnection(
142                        DisconnectCauseUtil.toTelecomDisconnectCause(
143                                android.telephony.DisconnectCause.INVALID_NUMBER,
144                                "Unable to parse number"));
145            }
146
147            final Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
148            if (phone != null && CDMA_ACTIVATION_CODE_REGEX_PATTERN.matcher(number).matches()) {
149                // Obtain the configuration for the outgoing phone's SIM. If the outgoing number
150                // matches the *228 regex pattern, fail the call. This number is used for OTASP, and
151                // when dialed could lock LTE SIMs to 3G if not prohibited..
152                boolean disableActivation = false;
153                CarrierConfigManager cfgManager = (CarrierConfigManager)
154                        phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
155                if (cfgManager != null) {
156                    disableActivation = cfgManager.getConfigForSubId(phone.getSubId())
157                            .getBoolean(CarrierConfigManager.KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL);
158                }
159
160                if (disableActivation) {
161                    return Connection.createFailedConnection(
162                            DisconnectCauseUtil.toTelecomDisconnectCause(
163                                    android.telephony.DisconnectCause
164                                            .CDMA_ALREADY_ACTIVATED,
165                                    "Tried to dial *228"));
166                }
167            }
168        }
169
170        boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(this, number);
171
172        // Get the right phone object from the account data passed in.
173        final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber);
174        if (phone == null) {
175            Log.d(this, "onCreateOutgoingConnection, phone is null");
176            return Connection.createFailedConnection(
177                    DisconnectCauseUtil.toTelecomDisconnectCause(
178                            android.telephony.DisconnectCause.OUT_OF_SERVICE, "Phone is null"));
179        }
180
181        // Check both voice & data RAT to enable normal CS call,
182        // when voice RAT is OOS but Data RAT is present.
183        int state = phone.getServiceState().getState();
184        if (state == ServiceState.STATE_OUT_OF_SERVICE) {
185            if (phone.getServiceState().getDataNetworkType() == TelephonyManager.NETWORK_TYPE_LTE) {
186                state = phone.getServiceState().getDataRegState();
187            }
188        }
189        boolean useEmergencyCallHelper = false;
190
191        if (isEmergencyNumber) {
192            if (!phone.isRadioOn()) {
193                useEmergencyCallHelper = true;
194            }
195        } else {
196            switch (state) {
197                case ServiceState.STATE_IN_SERVICE:
198                case ServiceState.STATE_EMERGENCY_ONLY:
199                    break;
200                case ServiceState.STATE_OUT_OF_SERVICE:
201                    if (phone.isUtEnabled() && number.endsWith("#")) {
202                        Log.d(this, "onCreateOutgoingConnection dial for UT");
203                        break;
204                    } else {
205                        return Connection.createFailedConnection(
206                                DisconnectCauseUtil.toTelecomDisconnectCause(
207                                        android.telephony.DisconnectCause.OUT_OF_SERVICE,
208                                        "ServiceState.STATE_OUT_OF_SERVICE"));
209                    }
210                case ServiceState.STATE_POWER_OFF:
211                    return Connection.createFailedConnection(
212                            DisconnectCauseUtil.toTelecomDisconnectCause(
213                                    android.telephony.DisconnectCause.POWER_OFF,
214                                    "ServiceState.STATE_POWER_OFF"));
215                default:
216                    Log.d(this, "onCreateOutgoingConnection, unknown service state: %d", state);
217                    return Connection.createFailedConnection(
218                            DisconnectCauseUtil.toTelecomDisconnectCause(
219                                    android.telephony.DisconnectCause.OUTGOING_FAILURE,
220                                    "Unknown service state " + state));
221            }
222        }
223
224        final TelephonyConnection connection =
225                createConnectionFor(phone, null, true /* isOutgoing */, request.getAccountHandle());
226        if (connection == null) {
227            return Connection.createFailedConnection(
228                    DisconnectCauseUtil.toTelecomDisconnectCause(
229                            android.telephony.DisconnectCause.OUTGOING_FAILURE,
230                            "Invalid phone type"));
231        }
232        connection.setAddress(handle, PhoneConstants.PRESENTATION_ALLOWED);
233        connection.setInitializing();
234        connection.setVideoState(request.getVideoState());
235
236        if (useEmergencyCallHelper) {
237            if (mEmergencyCallHelper == null) {
238                mEmergencyCallHelper = new EmergencyCallHelper(this);
239            }
240            mEmergencyCallHelper.startTurnOnRadioSequence(phone,
241                    new EmergencyCallHelper.Callback() {
242                        @Override
243                        public void onComplete(boolean isRadioReady) {
244                            if (connection.getState() == Connection.STATE_DISCONNECTED) {
245                                // If the connection has already been disconnected, do nothing.
246                            } else if (isRadioReady) {
247                                connection.setInitialized();
248                                placeOutgoingConnection(connection, phone, request);
249                            } else {
250                                Log.d(this, "onCreateOutgoingConnection, failed to turn on radio");
251                                connection.setDisconnected(
252                                        DisconnectCauseUtil.toTelecomDisconnectCause(
253                                                android.telephony.DisconnectCause.POWER_OFF,
254                                                "Failed to turn on radio."));
255                                connection.destroy();
256                            }
257                        }
258                    });
259
260        } else {
261            placeOutgoingConnection(connection, phone, request);
262        }
263
264        return connection;
265    }
266
267    @Override
268    public Connection onCreateIncomingConnection(
269            PhoneAccountHandle connectionManagerPhoneAccount,
270            ConnectionRequest request) {
271        Log.i(this, "onCreateIncomingConnection, request: " + request);
272
273        Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
274        if (phone == null) {
275            return Connection.createFailedConnection(
276                    DisconnectCauseUtil.toTelecomDisconnectCause(
277                            android.telephony.DisconnectCause.ERROR_UNSPECIFIED,
278                            "Phone is null"));
279        }
280
281        Call call = phone.getRingingCall();
282        if (!call.getState().isRinging()) {
283            Log.i(this, "onCreateIncomingConnection, no ringing call");
284            return Connection.createFailedConnection(
285                    DisconnectCauseUtil.toTelecomDisconnectCause(
286                            android.telephony.DisconnectCause.INCOMING_MISSED,
287                            "Found no ringing call"));
288        }
289
290        com.android.internal.telephony.Connection originalConnection =
291                call.getState() == Call.State.WAITING ?
292                    call.getLatestConnection() : call.getEarliestConnection();
293        if (isOriginalConnectionKnown(originalConnection)) {
294            Log.i(this, "onCreateIncomingConnection, original connection already registered");
295            return Connection.createCanceledConnection();
296        }
297
298        Connection connection =
299                createConnectionFor(phone, originalConnection, false /* isOutgoing */,
300                        request.getAccountHandle());
301        if (connection == null) {
302            return Connection.createCanceledConnection();
303        } else {
304            return connection;
305        }
306    }
307
308    @Override
309    public void triggerConferenceRecalculate() {
310        if (mTelephonyConferenceController.shouldRecalculate()) {
311            mTelephonyConferenceController.recalculate();
312        }
313    }
314
315    @Override
316    public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount,
317            ConnectionRequest request) {
318        Log.i(this, "onCreateUnknownConnection, request: " + request);
319
320        Phone phone = getPhoneForAccount(request.getAccountHandle(), false);
321        if (phone == null) {
322            return Connection.createFailedConnection(
323                    DisconnectCauseUtil.toTelecomDisconnectCause(
324                            android.telephony.DisconnectCause.ERROR_UNSPECIFIED,
325                            "Phone is null"));
326        }
327
328        final List<com.android.internal.telephony.Connection> allConnections = new ArrayList<>();
329        final Call ringingCall = phone.getRingingCall();
330        if (ringingCall.hasConnections()) {
331            allConnections.addAll(ringingCall.getConnections());
332        }
333        final Call foregroundCall = phone.getForegroundCall();
334        if ((foregroundCall.getState() != Call.State.DISCONNECTED)
335                && (foregroundCall.hasConnections())) {
336            allConnections.addAll(foregroundCall.getConnections());
337        }
338        if (phone.getImsPhone() != null) {
339            final Call imsFgCall = phone.getImsPhone().getForegroundCall();
340            if ((imsFgCall.getState() != Call.State.DISCONNECTED) && imsFgCall.hasConnections()) {
341                allConnections.addAll(imsFgCall.getConnections());
342            }
343        }
344        final Call backgroundCall = phone.getBackgroundCall();
345        if (backgroundCall.hasConnections()) {
346            allConnections.addAll(phone.getBackgroundCall().getConnections());
347        }
348
349        com.android.internal.telephony.Connection unknownConnection = null;
350        for (com.android.internal.telephony.Connection telephonyConnection : allConnections) {
351            if (!isOriginalConnectionKnown(telephonyConnection)) {
352                unknownConnection = telephonyConnection;
353                Log.d(this, "onCreateUnknownConnection: conn = " + unknownConnection);
354                break;
355            }
356        }
357
358        if (unknownConnection == null) {
359            Log.i(this, "onCreateUnknownConnection, did not find previously unknown connection.");
360            return Connection.createCanceledConnection();
361        }
362
363        TelephonyConnection connection =
364                createConnectionFor(phone, unknownConnection,
365                        !unknownConnection.isIncoming() /* isOutgoing */,
366                        request.getAccountHandle());
367
368        if (connection == null) {
369            return Connection.createCanceledConnection();
370        } else {
371            connection.updateState();
372            return connection;
373        }
374    }
375
376    @Override
377    public void onConference(Connection connection1, Connection connection2) {
378        if (connection1 instanceof TelephonyConnection &&
379                connection2 instanceof TelephonyConnection) {
380            ((TelephonyConnection) connection1).performConference(
381                (TelephonyConnection) connection2);
382        }
383
384    }
385
386    private void placeOutgoingConnection(
387            TelephonyConnection connection, Phone phone, ConnectionRequest request) {
388        String number = connection.getAddress().getSchemeSpecificPart();
389
390        com.android.internal.telephony.Connection originalConnection;
391        try {
392            originalConnection =
393                    phone.dial(number, null, request.getVideoState(), request.getExtras());
394        } catch (CallStateException e) {
395            Log.e(this, e, "placeOutgoingConnection, phone.dial exception: " + e);
396            int cause = android.telephony.DisconnectCause.OUTGOING_FAILURE;
397            if (e.getError() == CallStateException.ERROR_DISCONNECTED) {
398                cause = android.telephony.DisconnectCause.OUT_OF_SERVICE;
399            }
400            connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
401                    cause, e.getMessage()));
402            return;
403        }
404
405        if (originalConnection == null) {
406            int telephonyDisconnectCause = android.telephony.DisconnectCause.OUTGOING_FAILURE;
407            // On GSM phones, null connection means that we dialed an MMI code
408            if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) {
409                Log.d(this, "dialed MMI code");
410                telephonyDisconnectCause = android.telephony.DisconnectCause.DIALED_MMI;
411                final Intent intent = new Intent(this, MMIDialogActivity.class);
412                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
413                        Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
414                startActivity(intent);
415            }
416            Log.d(this, "placeOutgoingConnection, phone.dial returned null");
417            connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause(
418                    telephonyDisconnectCause, "Connection is null"));
419        } else {
420            connection.setOriginalConnection(originalConnection);
421        }
422    }
423
424    private TelephonyConnection createConnectionFor(
425            Phone phone,
426            com.android.internal.telephony.Connection originalConnection,
427            boolean isOutgoing,
428            PhoneAccountHandle phoneAccountHandle) {
429        TelephonyConnection returnConnection = null;
430        int phoneType = phone.getPhoneType();
431        if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
432            returnConnection = new GsmConnection(originalConnection);
433        } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
434            boolean allowMute = allowMute(phone);
435            returnConnection = new CdmaConnection(
436                    originalConnection, mEmergencyTonePlayer, allowMute, isOutgoing);
437        }
438        if (returnConnection != null) {
439            // Listen to Telephony specific callbacks from the connection
440            returnConnection.addTelephonyConnectionListener(mTelephonyConnectionListener);
441            returnConnection.setVideoPauseSupported(
442                    TelecomAccountRegistry.getInstance(this).isVideoPauseSupported(
443                            phoneAccountHandle));
444            returnConnection.setConferenceSupported(
445                    TelecomAccountRegistry.getInstance(this).isMergeCallSupported(
446                            phoneAccountHandle));
447        }
448        return returnConnection;
449    }
450
451    private boolean isOriginalConnectionKnown(
452            com.android.internal.telephony.Connection originalConnection) {
453        for (Connection connection : getAllConnections()) {
454            if (connection instanceof TelephonyConnection) {
455                TelephonyConnection telephonyConnection = (TelephonyConnection) connection;
456                if (telephonyConnection.getOriginalConnection() == originalConnection) {
457                    return true;
458                }
459            }
460        }
461        return false;
462    }
463
464    private Phone getPhoneForAccount(PhoneAccountHandle accountHandle, boolean isEmergency) {
465        if (isEmergency) {
466            return PhoneFactory.getDefaultPhone();
467        }
468
469        int subId = PhoneUtils.getSubIdForPhoneAccountHandle(accountHandle);
470        if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
471            int phoneId = SubscriptionController.getInstance().getPhoneId(subId);
472            return PhoneFactory.getPhone(phoneId);
473        }
474
475        return null;
476    }
477
478    private Phone getFirstPhoneForEmergencyCall() {
479        Phone selectPhone = null;
480        for (int i = 0; i < TelephonyManager.getDefault().getSimCount(); i++) {
481            int[] subIds = SubscriptionController.getInstance().getSubIdUsingSlotId(i);
482            if (subIds.length == 0)
483                continue;
484
485            int phoneId = SubscriptionController.getInstance().getPhoneId(subIds[0]);
486            Phone phone = PhoneFactory.getPhone(phoneId);
487            if (phone == null)
488                continue;
489
490            if (ServiceState.STATE_IN_SERVICE == phone.getServiceState().getState()) {
491                // the slot is radio on & state is in service
492                Log.d(this, "pickBestPhoneForEmergencyCall, radio on & in service, slotId:" + i);
493                return phone;
494            } else if (ServiceState.STATE_POWER_OFF != phone.getServiceState().getState()) {
495                // the slot is radio on & with SIM card inserted.
496                if (TelephonyManager.getDefault().hasIccCard(i)) {
497                    Log.d(this, "pickBestPhoneForEmergencyCall," +
498                            "radio on and SIM card inserted, slotId:" + i);
499                    selectPhone = phone;
500                } else if (selectPhone == null) {
501                    Log.d(this, "pickBestPhoneForEmergencyCall, radio on, slotId:" + i);
502                    selectPhone = phone;
503                }
504            }
505        }
506
507        if (selectPhone == null) {
508            Log.d(this, "pickBestPhoneForEmergencyCall, return default phone");
509            selectPhone = PhoneFactory.getDefaultPhone();
510        }
511
512        return selectPhone;
513    }
514
515    /**
516     * Determines if the connection should allow mute.
517     *
518     * @param phone The current phone.
519     * @return {@code True} if the connection should allow mute.
520     */
521    private boolean allowMute(Phone phone) {
522        // For CDMA phones, check if we are in Emergency Callback Mode (ECM).  Mute is disallowed
523        // in ECM mode.
524        if (phone.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
525            PhoneProxy phoneProxy = (PhoneProxy)phone;
526            CDMAPhone cdmaPhone = (CDMAPhone)phoneProxy.getActivePhone();
527            if (cdmaPhone != null) {
528                if (cdmaPhone.isInEcm()) {
529                    return false;
530                }
531            }
532        }
533
534        return true;
535    }
536
537    @Override
538    public void removeConnection(Connection connection) {
539        super.removeConnection(connection);
540        if (connection instanceof TelephonyConnection) {
541            TelephonyConnection telephonyConnection = (TelephonyConnection) connection;
542            telephonyConnection.removeTelephonyConnectionListener(mTelephonyConnectionListener);
543        }
544    }
545
546    /**
547     * When a {@link TelephonyConnection} has its underlying original connection configured,
548     * we need to add it to the correct conference controller.
549     *
550     * @param connection The connection to be added to the controller
551     */
552    public void addConnectionToConferenceController(TelephonyConnection connection) {
553        // TODO: Do we need to handle the case of the original connection changing
554        // and triggering this callback multiple times for the same connection?
555        // If that is the case, we might want to remove this connection from all
556        // conference controllers first before re-adding it.
557        if (connection.isImsConnection()) {
558            Log.d(this, "Adding IMS connection to conference controller: " + connection);
559            mImsConferenceController.add(connection);
560        } else {
561            int phoneType = connection.getCall().getPhone().getPhoneType();
562            if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
563                Log.d(this, "Adding GSM connection to conference controller: " + connection);
564                mTelephonyConferenceController.add(connection);
565            } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA &&
566                    connection instanceof CdmaConnection) {
567                Log.d(this, "Adding CDMA connection to conference controller: " + connection);
568                mCdmaConferenceController.add((CdmaConnection)connection);
569            }
570            Log.d(this, "Removing connection from IMS conference controller: " + connection);
571            mImsConferenceController.remove(connection);
572        }
573    }
574}
575