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