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