/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.services.telephony; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import android.net.Uri; import android.telecom.Connection; import android.telecom.ConnectionRequest; import android.telecom.ConnectionService; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telephony.CarrierConfigManager; import android.telephony.PhoneNumberUtils; import android.telephony.ServiceState; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import com.android.internal.telephony.Call; import com.android.internal.telephony.CallStateException; import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.PhoneFactory; import com.android.internal.telephony.PhoneProxy; import com.android.internal.telephony.SubscriptionController; import com.android.internal.telephony.cdma.CDMAPhone; import com.android.phone.MMIDialogActivity; import com.android.phone.PhoneUtils; import com.android.phone.R; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; /** * Service for making GSM and CDMA connections. */ public class TelephonyConnectionService extends ConnectionService { // If configured, reject attempts to dial numbers matching this pattern. private static final Pattern CDMA_ACTIVATION_CODE_REGEX_PATTERN = Pattern.compile("\\*228[0-9]{0,2}"); private final TelephonyConferenceController mTelephonyConferenceController = new TelephonyConferenceController(this); private final CdmaConferenceController mCdmaConferenceController = new CdmaConferenceController(this); private final ImsConferenceController mImsConferenceController = new ImsConferenceController(this); private ComponentName mExpectedComponentName = null; private EmergencyCallHelper mEmergencyCallHelper; private EmergencyTonePlayer mEmergencyTonePlayer; /** * A listener to actionable events specific to the TelephonyConnection. */ private final TelephonyConnection.TelephonyConnectionListener mTelephonyConnectionListener = new TelephonyConnection.TelephonyConnectionListener() { @Override public void onOriginalConnectionConfigured(TelephonyConnection c) { addConnectionToConferenceController(c); } }; @Override public void onCreate() { super.onCreate(); mExpectedComponentName = new ComponentName(this, this.getClass()); mEmergencyTonePlayer = new EmergencyTonePlayer(this); TelecomAccountRegistry.getInstance(this).setTelephonyConnectionService(this); } @Override public Connection onCreateOutgoingConnection( PhoneAccountHandle connectionManagerPhoneAccount, final ConnectionRequest request) { Log.i(this, "onCreateOutgoingConnection, request: " + request); Uri handle = request.getAddress(); if (handle == null) { Log.d(this, "onCreateOutgoingConnection, handle is null"); return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.NO_PHONE_NUMBER_SUPPLIED, "No phone number supplied")); } String scheme = handle.getScheme(); final String number; if (PhoneAccount.SCHEME_VOICEMAIL.equals(scheme)) { // TODO: We don't check for SecurityException here (requires // CALL_PRIVILEGED permission). final Phone phone = getPhoneForAccount(request.getAccountHandle(), false); if (phone == null) { Log.d(this, "onCreateOutgoingConnection, phone is null"); return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.OUT_OF_SERVICE, "Phone is null")); } number = phone.getVoiceMailNumber(); if (TextUtils.isEmpty(number)) { Log.d(this, "onCreateOutgoingConnection, no voicemail number set."); return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.VOICEMAIL_NUMBER_MISSING, "Voicemail scheme provided but no voicemail number set.")); } // Convert voicemail: to tel: handle = Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null); } else { if (!PhoneAccount.SCHEME_TEL.equals(scheme)) { Log.d(this, "onCreateOutgoingConnection, Handle %s is not type tel", scheme); return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.INVALID_NUMBER, "Handle scheme is not type tel")); } number = handle.getSchemeSpecificPart(); if (TextUtils.isEmpty(number)) { Log.d(this, "onCreateOutgoingConnection, unable to parse number"); return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.INVALID_NUMBER, "Unable to parse number")); } final Phone phone = getPhoneForAccount(request.getAccountHandle(), false); if (phone != null && CDMA_ACTIVATION_CODE_REGEX_PATTERN.matcher(number).matches()) { // Obtain the configuration for the outgoing phone's SIM. If the outgoing number // matches the *228 regex pattern, fail the call. This number is used for OTASP, and // when dialed could lock LTE SIMs to 3G if not prohibited.. boolean disableActivation = false; CarrierConfigManager cfgManager = (CarrierConfigManager) phone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE); if (cfgManager != null) { disableActivation = cfgManager.getConfigForSubId(phone.getSubId()) .getBoolean(CarrierConfigManager.KEY_DISABLE_CDMA_ACTIVATION_CODE_BOOL); } if (disableActivation) { return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause .CDMA_ALREADY_ACTIVATED, "Tried to dial *228")); } } } boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(this, number); // Get the right phone object from the account data passed in. final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber); if (phone == null) { Log.d(this, "onCreateOutgoingConnection, phone is null"); return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.OUT_OF_SERVICE, "Phone is null")); } // Check both voice & data RAT to enable normal CS call, // when voice RAT is OOS but Data RAT is present. int state = phone.getServiceState().getState(); if (state == ServiceState.STATE_OUT_OF_SERVICE) { if (phone.getServiceState().getDataNetworkType() == TelephonyManager.NETWORK_TYPE_LTE) { state = phone.getServiceState().getDataRegState(); } } boolean useEmergencyCallHelper = false; if (isEmergencyNumber) { if (!phone.isRadioOn()) { useEmergencyCallHelper = true; } } else { switch (state) { case ServiceState.STATE_IN_SERVICE: case ServiceState.STATE_EMERGENCY_ONLY: break; case ServiceState.STATE_OUT_OF_SERVICE: return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.OUT_OF_SERVICE, "ServiceState.STATE_OUT_OF_SERVICE")); case ServiceState.STATE_POWER_OFF: return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.POWER_OFF, "ServiceState.STATE_POWER_OFF")); default: Log.d(this, "onCreateOutgoingConnection, unknown service state: %d", state); return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.OUTGOING_FAILURE, "Unknown service state " + state)); } } final TelephonyConnection connection = createConnectionFor(phone, null, true /* isOutgoing */, request.getAccountHandle()); if (connection == null) { return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.OUTGOING_FAILURE, "Invalid phone type")); } connection.setAddress(handle, PhoneConstants.PRESENTATION_ALLOWED); connection.setInitializing(); connection.setVideoState(request.getVideoState()); if (useEmergencyCallHelper) { if (mEmergencyCallHelper == null) { mEmergencyCallHelper = new EmergencyCallHelper(this); } mEmergencyCallHelper.startTurnOnRadioSequence(phone, new EmergencyCallHelper.Callback() { @Override public void onComplete(boolean isRadioReady) { if (connection.getState() == Connection.STATE_DISCONNECTED) { // If the connection has already been disconnected, do nothing. } else if (isRadioReady) { connection.setInitialized(); placeOutgoingConnection(connection, phone, request); } else { Log.d(this, "onCreateOutgoingConnection, failed to turn on radio"); connection.setDisconnected( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.POWER_OFF, "Failed to turn on radio.")); connection.destroy(); } } }); } else { placeOutgoingConnection(connection, phone, request); } return connection; } @Override public Connection onCreateIncomingConnection( PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { Log.i(this, "onCreateIncomingConnection, request: " + request); Phone phone = getPhoneForAccount(request.getAccountHandle(), false); if (phone == null) { return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.ERROR_UNSPECIFIED, "Phone is null")); } Call call = phone.getRingingCall(); if (!call.getState().isRinging()) { Log.i(this, "onCreateIncomingConnection, no ringing call"); return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.INCOMING_MISSED, "Found no ringing call")); } com.android.internal.telephony.Connection originalConnection = call.getState() == Call.State.WAITING ? call.getLatestConnection() : call.getEarliestConnection(); if (isOriginalConnectionKnown(originalConnection)) { Log.i(this, "onCreateIncomingConnection, original connection already registered"); return Connection.createCanceledConnection(); } Connection connection = createConnectionFor(phone, originalConnection, false /* isOutgoing */, request.getAccountHandle()); if (connection == null) { return Connection.createCanceledConnection(); } else { return connection; } } @Override public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) { Log.i(this, "onCreateUnknownConnection, request: " + request); Phone phone = getPhoneForAccount(request.getAccountHandle(), false); if (phone == null) { return Connection.createFailedConnection( DisconnectCauseUtil.toTelecomDisconnectCause( android.telephony.DisconnectCause.ERROR_UNSPECIFIED, "Phone is null")); } final List allConnections = new ArrayList<>(); final Call ringingCall = phone.getRingingCall(); if (ringingCall.hasConnections()) { allConnections.addAll(ringingCall.getConnections()); } final Call foregroundCall = phone.getForegroundCall(); if (foregroundCall.hasConnections()) { allConnections.addAll(foregroundCall.getConnections()); } final Call backgroundCall = phone.getBackgroundCall(); if (backgroundCall.hasConnections()) { allConnections.addAll(phone.getBackgroundCall().getConnections()); } com.android.internal.telephony.Connection unknownConnection = null; for (com.android.internal.telephony.Connection telephonyConnection : allConnections) { if (!isOriginalConnectionKnown(telephonyConnection)) { unknownConnection = telephonyConnection; break; } } if (unknownConnection == null) { Log.i(this, "onCreateUnknownConnection, did not find previously unknown connection."); return Connection.createCanceledConnection(); } TelephonyConnection connection = createConnectionFor(phone, unknownConnection, !unknownConnection.isIncoming() /* isOutgoing */, request.getAccountHandle()); if (connection == null) { return Connection.createCanceledConnection(); } else { connection.updateState(); return connection; } } @Override public void onConference(Connection connection1, Connection connection2) { if (connection1 instanceof TelephonyConnection && connection2 instanceof TelephonyConnection) { ((TelephonyConnection) connection1).performConference( (TelephonyConnection) connection2); } } private void placeOutgoingConnection( TelephonyConnection connection, Phone phone, ConnectionRequest request) { String number = connection.getAddress().getSchemeSpecificPart(); com.android.internal.telephony.Connection originalConnection; try { originalConnection = phone.dial(number, null, request.getVideoState(), request.getExtras()); } catch (CallStateException e) { Log.e(this, e, "placeOutgoingConnection, phone.dial exception: " + e); int cause = android.telephony.DisconnectCause.OUTGOING_FAILURE; if (e.getError() == CallStateException.ERROR_DISCONNECTED) { cause = android.telephony.DisconnectCause.OUT_OF_SERVICE; } connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause( cause, e.getMessage())); return; } if (originalConnection == null) { int telephonyDisconnectCause = android.telephony.DisconnectCause.OUTGOING_FAILURE; // On GSM phones, null connection means that we dialed an MMI code if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) { Log.d(this, "dialed MMI code"); telephonyDisconnectCause = android.telephony.DisconnectCause.DIALED_MMI; final Intent intent = new Intent(this, MMIDialogActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); startActivity(intent); } Log.d(this, "placeOutgoingConnection, phone.dial returned null"); connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause( telephonyDisconnectCause, "Connection is null")); } else { connection.setOriginalConnection(originalConnection); } } private TelephonyConnection createConnectionFor( Phone phone, com.android.internal.telephony.Connection originalConnection, boolean isOutgoing, PhoneAccountHandle phoneAccountHandle) { TelephonyConnection returnConnection = null; int phoneType = phone.getPhoneType(); if (phoneType == TelephonyManager.PHONE_TYPE_GSM) { returnConnection = new GsmConnection(originalConnection); } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) { boolean allowMute = allowMute(phone); returnConnection = new CdmaConnection( originalConnection, mEmergencyTonePlayer, allowMute, isOutgoing); } if (returnConnection != null) { // Listen to Telephony specific callbacks from the connection returnConnection.addTelephonyConnectionListener(mTelephonyConnectionListener); returnConnection.setVideoPauseSupported( TelecomAccountRegistry.getInstance(this).isVideoPauseSupported( phoneAccountHandle)); } return returnConnection; } private boolean isOriginalConnectionKnown( com.android.internal.telephony.Connection originalConnection) { for (Connection connection : getAllConnections()) { if (connection instanceof TelephonyConnection) { TelephonyConnection telephonyConnection = (TelephonyConnection) connection; if (telephonyConnection.getOriginalConnection() == originalConnection) { return true; } } } return false; } private Phone getPhoneForAccount(PhoneAccountHandle accountHandle, boolean isEmergency) { if (isEmergency) { return PhoneFactory.getDefaultPhone(); } int subId = PhoneUtils.getSubIdForPhoneAccountHandle(accountHandle); if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { int phoneId = SubscriptionController.getInstance().getPhoneId(subId); return PhoneFactory.getPhone(phoneId); } return null; } private Phone getFirstPhoneForEmergencyCall() { Phone selectPhone = null; for (int i = 0; i < TelephonyManager.getDefault().getSimCount(); i++) { int[] subIds = SubscriptionController.getInstance().getSubIdUsingSlotId(i); if (subIds.length == 0) continue; int phoneId = SubscriptionController.getInstance().getPhoneId(subIds[0]); Phone phone = PhoneFactory.getPhone(phoneId); if (phone == null) continue; if (ServiceState.STATE_IN_SERVICE == phone.getServiceState().getState()) { // the slot is radio on & state is in service Log.d(this, "pickBestPhoneForEmergencyCall, radio on & in service, slotId:" + i); return phone; } else if (ServiceState.STATE_POWER_OFF != phone.getServiceState().getState()) { // the slot is radio on & with SIM card inserted. if (TelephonyManager.getDefault().hasIccCard(i)) { Log.d(this, "pickBestPhoneForEmergencyCall," + "radio on and SIM card inserted, slotId:" + i); selectPhone = phone; } else if (selectPhone == null) { Log.d(this, "pickBestPhoneForEmergencyCall, radio on, slotId:" + i); selectPhone = phone; } } } if (selectPhone == null) { Log.d(this, "pickBestPhoneForEmergencyCall, return default phone"); selectPhone = PhoneFactory.getDefaultPhone(); } return selectPhone; } /** * Determines if the connection should allow mute. * * @param phone The current phone. * @return {@code True} if the connection should allow mute. */ private boolean allowMute(Phone phone) { // For CDMA phones, check if we are in Emergency Callback Mode (ECM). Mute is disallowed // in ECM mode. if (phone.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { PhoneProxy phoneProxy = (PhoneProxy)phone; CDMAPhone cdmaPhone = (CDMAPhone)phoneProxy.getActivePhone(); if (cdmaPhone != null) { if (cdmaPhone.isInEcm()) { return false; } } } return true; } @Override public void removeConnection(Connection connection) { super.removeConnection(connection); if (connection instanceof TelephonyConnection) { TelephonyConnection telephonyConnection = (TelephonyConnection) connection; telephonyConnection.removeTelephonyConnectionListener(mTelephonyConnectionListener); } } /** * When a {@link TelephonyConnection} has its underlying original connection configured, * we need to add it to the correct conference controller. * * @param connection The connection to be added to the controller */ public void addConnectionToConferenceController(TelephonyConnection connection) { // TODO: Do we need to handle the case of the original connection changing // and triggering this callback multiple times for the same connection? // If that is the case, we might want to remove this connection from all // conference controllers first before re-adding it. if (connection.isImsConnection()) { Log.d(this, "Adding IMS connection to conference controller: " + connection); mImsConferenceController.add(connection); } else { int phoneType = connection.getCall().getPhone().getPhoneType(); if (phoneType == TelephonyManager.PHONE_TYPE_GSM) { Log.d(this, "Adding GSM connection to conference controller: " + connection); mTelephonyConferenceController.add(connection); } else if (phoneType == TelephonyManager.PHONE_TYPE_CDMA && connection instanceof CdmaConnection) { Log.d(this, "Adding CDMA connection to conference controller: " + connection); mCdmaConferenceController.add((CdmaConnection)connection); } Log.d(this, "Removing connection from IMS conference controller: " + connection); mImsConferenceController.remove(connection); } } }