| /* |
| * 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.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.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.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 { |
| final Phone phone = getPhoneForAccount(request.getAccountHandle(), false); |
| 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")); |
| } |
| |
| // 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 would lock LTE SIMs to 3G if not prohibited.. |
| SubscriptionManager subManager = SubscriptionManager.from(phone.getContext()); |
| SubscriptionInfo subInfo = subManager.getActiveSubscriptionInfo(phone.getSubId()); |
| if (subInfo != null) { |
| Configuration config = new Configuration(); |
| config.mcc = subInfo.getMcc(); |
| config.mnc = subInfo.getMnc(); |
| Context subContext = phone.getContext().createConfigurationContext(config); |
| |
| if (subContext.getResources() != null && subContext.getResources() |
| .getBoolean(R.bool.config_disable_cdma_activation_code)) { |
| if (CDMA_ACTIVATION_CODE_REGEX_PATTERN.matcher(number).matches()) { |
| return Connection.createFailedConnection( |
| DisconnectCauseUtil.toTelecomDisconnectCause( |
| android.telephony.DisconnectCause.INVALID_NUMBER, |
| "Tried to dial *228")); |
| } |
| } |
| } |
| } |
| |
| boolean isEmergencyNumber = PhoneNumberUtils.isPotentialEmergencyNumber(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) { |
| state = phone.getServiceState().getDataRegState(); |
| } |
| boolean useEmergencyCallHelper = false; |
| |
| if (isEmergencyNumber) { |
| if (state == ServiceState.STATE_POWER_OFF) { |
| 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 */); |
| 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)); |
| } |
| |
| 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 */); |
| if (connection == null) { |
| connection = Connection.createCanceledConnection(); |
| 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)); |
| } |
| |
| final List<com.android.internal.telephony.Connection> 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 */); |
| |
| 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, request.getVideoState()); |
| } catch (CallStateException e) { |
| Log.e(this, e, "placeOutgoingConnection, phone.dial exception: " + e); |
| connection.setDisconnected(DisconnectCauseUtil.toTelecomDisconnectCause( |
| android.telephony.DisconnectCause.OUTGOING_FAILURE, |
| 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) { |
| 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); |
| } |
| 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 (Objects.equals(mExpectedComponentName, accountHandle.getComponentName())) { |
| if (accountHandle.getId() != null) { |
| try { |
| int phoneId = SubscriptionController.getInstance().getPhoneId( |
| Integer.parseInt(accountHandle.getId())); |
| return PhoneFactory.getPhone(phoneId); |
| } catch (NumberFormatException e) { |
| Log.w(this, "Could not get subId from account: " + accountHandle.getId()); |
| } |
| } |
| } |
| |
| if (isEmergency) { |
| // If this is an emergency number and we've been asked to dial it using a PhoneAccount |
| // which does not exist, then default to whatever subscription is available currently. |
| return getFirstPhoneForEmergencyCall(); |
| } |
| |
| 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); |
| } |
| } |
| } |
| } |