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