/* * Copyright (C) 2018 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.internal.telephony; import android.app.Activity; import android.content.Context; import android.os.Binder; import android.os.Message; import android.os.PersistableBundle; import android.os.RemoteException; import android.provider.Telephony.Sms.Intents; import android.telephony.CarrierConfigManager; import android.telephony.ServiceState; import android.telephony.SmsManager; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.RegistrationManager; import android.telephony.ims.aidl.IImsSmsListener; import android.telephony.ims.feature.MmTelFeature; import android.telephony.ims.stub.ImsRegistrationImplBase; import android.telephony.ims.stub.ImsSmsImplBase; import android.telephony.ims.stub.ImsSmsImplBase.SendStatusResult; import com.android.ims.FeatureConnector; import com.android.ims.ImsException; import com.android.ims.ImsManager; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; import com.android.internal.telephony.analytics.TelephonyAnalytics; import com.android.internal.telephony.analytics.TelephonyAnalytics.SmsMmsAnalytics; import com.android.internal.telephony.metrics.TelephonyMetrics; import com.android.internal.telephony.uicc.IccUtils; import com.android.internal.telephony.util.SMSDispatcherUtil; import com.android.telephony.Rlog; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; /** * Responsible for communications with {@link com.android.ims.ImsManager} to send/receive messages * over IMS. * @hide */ public class ImsSmsDispatcher extends SMSDispatcher { private static final String TAG = "ImsSmsDispatcher"; private static final int CONNECT_DELAY_MS = 5000; // 5 seconds; public static final int MAX_SEND_RETRIES_OVER_IMS = MAX_SEND_RETRIES; /** * Creates FeatureConnector instances for ImsManager, used during testing to inject mock * connector instances. */ @VisibleForTesting public interface FeatureConnectorFactory { /** * Create a new FeatureConnector for ImsManager. */ FeatureConnector create(Context context, int phoneId, String logPrefix, FeatureConnector.Listener listener, Executor executor); } public List mMemoryAvailableNotifierList = new ArrayList(); @VisibleForTesting public Map mTrackers = new ConcurrentHashMap<>(); @VisibleForTesting public AtomicInteger mNextToken = new AtomicInteger(); private final Object mLock = new Object(); private volatile boolean mIsSmsCapable; private volatile boolean mIsImsServiceUp; private volatile boolean mIsRegistered; private final FeatureConnector mImsManagerConnector; /** Telephony metrics instance for logging metrics event */ private TelephonyMetrics mMetrics = TelephonyMetrics.getInstance(); private ImsManager mImsManager; private FeatureConnectorFactory mConnectorFactory; private Runnable mConnectRunnable = new Runnable() { @Override public void run() { mImsManagerConnector.connect(); } }; /** * Listen to the IMS service state change * */ private RegistrationManager.RegistrationCallback mRegistrationCallback = new RegistrationManager.RegistrationCallback() { @Override public void onRegistered( @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) { logd("onImsConnected imsRadioTech=" + imsRadioTech); synchronized (mLock) { mIsRegistered = true; } } @Override public void onRegistering( @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) { logd("onImsProgressing imsRadioTech=" + imsRadioTech); synchronized (mLock) { mIsRegistered = false; } } @Override public void onUnregistered(ImsReasonInfo info) { logd("onImsDisconnected imsReasonInfo=" + info); synchronized (mLock) { mIsRegistered = false; } } }; private android.telephony.ims.ImsMmTelManager.CapabilityCallback mCapabilityCallback = new android.telephony.ims.ImsMmTelManager.CapabilityCallback() { @Override public void onCapabilitiesStatusChanged( MmTelFeature.MmTelCapabilities capabilities) { synchronized (mLock) { mIsSmsCapable = capabilities.isCapable( MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_SMS); } } }; private final IImsSmsListener mImsSmsListener = new IImsSmsListener.Stub() { @Override public void onMemoryAvailableResult(int token, @SendStatusResult int status, int networkReasonCode) { final long identity = Binder.clearCallingIdentity(); try { logd("onMemoryAvailableResult token=" + token + " status=" + status + " networkReasonCode=" + networkReasonCode); if (!mMemoryAvailableNotifierList.contains(token)) { loge("onMemoryAvailableResult Invalid token"); return; } mMemoryAvailableNotifierList.remove(Integer.valueOf(token)); /** * The Retrans flag is set and reset As per section 6.3.3.1.2 in TS 124011 * Note: Assuming that SEND_STATUS_ERROR_RETRY is sent in case of temporary failure */ if (status == ImsSmsImplBase.SEND_STATUS_ERROR_RETRY) { if (!mRPSmmaRetried) { sendMessageDelayed(obtainMessage(EVENT_RETRY_SMMA), SEND_RETRY_DELAY); mRPSmmaRetried = true; } else { mRPSmmaRetried = false; } } else { mRPSmmaRetried = false; } } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onSendSmsResult(int token, int messageRef, @SendStatusResult int status, @SmsManager.Result int reason, int networkReasonCode) { final long identity = Binder.clearCallingIdentity(); try { logd("onSendSmsResult token=" + token + " messageRef=" + messageRef + " status=" + status + " reason=" + reason + " networkReasonCode=" + networkReasonCode); // TODO integrate networkReasonCode into IMS SMS metrics. SmsTracker tracker = mTrackers.get(token); mMetrics.writeOnImsServiceSmsSolicitedResponse(mPhone.getPhoneId(), status, reason, (tracker != null ? tracker.mMessageId : 0L)); if (tracker == null) { throw new IllegalArgumentException("Invalid token."); } tracker.mMessageRef = messageRef; switch(status) { case ImsSmsImplBase.SEND_STATUS_OK: if (tracker.mDeliveryIntent != null) { // Expecting a status report. Put this tracker to the map. mSmsDispatchersController.putDeliveryPendingTracker(tracker); } tracker.onSent(mContext); mTrackers.remove(token); mPhone.notifySmsSent(tracker.mDestAddress); mSmsDispatchersController.notifySmsSentToEmergencyStateTracker( tracker.mDestAddress, tracker.mMessageId, true, tracker.isSinglePartOrLastPart()); break; case ImsSmsImplBase.SEND_STATUS_ERROR: tracker.onFailed(mContext, reason, networkReasonCode); mTrackers.remove(token); notifySmsSentFailedToEmergencyStateTracker(tracker, true); break; case ImsSmsImplBase.SEND_STATUS_ERROR_RETRY: int maxRetryCountOverIms = getMaxRetryCountOverIms(); if (tracker.mRetryCount < getMaxSmsRetryCount()) { if (maxRetryCountOverIms < getMaxSmsRetryCount() && tracker.mRetryCount >= maxRetryCountOverIms) { tracker.mRetryCount += 1; mTrackers.remove(token); fallbackToPstn(tracker); break; } tracker.mRetryCount += 1; sendMessageDelayed( obtainMessage(EVENT_SEND_RETRY, tracker), getSmsRetryDelayValue()); } else { tracker.onFailed(mContext, reason, networkReasonCode); mTrackers.remove(token); notifySmsSentFailedToEmergencyStateTracker(tracker, true); } break; case ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK: // Skip MAX_SEND_RETRIES checking here. It allows CSFB after // SEND_STATUS_ERROR_RETRY up to MAX_SEND_RETRIES even. tracker.mRetryCount += 1; mTrackers.remove(token); fallbackToPstn(tracker); break; default: } mPhone.getSmsStats().onOutgoingSms( true /* isOverIms */, SmsConstants.FORMAT_3GPP2.equals(getFormat()), status == ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK, reason, networkReasonCode, tracker.mMessageId, tracker.isFromDefaultSmsApplication(mContext), tracker.getInterval(), mTelephonyManager.isEmergencyNumber(tracker.mDestAddress)); if (mPhone != null) { TelephonyAnalytics telephonyAnalytics = mPhone.getTelephonyAnalytics(); if (telephonyAnalytics != null) { SmsMmsAnalytics smsMmsAnalytics = telephonyAnalytics.getSmsMmsAnalytics(); if (smsMmsAnalytics != null) { smsMmsAnalytics.onOutgoingSms( true /* isOverIms */, reason); } } } } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onSmsStatusReportReceived(int token, String format, byte[] pdu) throws RemoteException { final long identity = Binder.clearCallingIdentity(); try { logd("Status report received."); android.telephony.SmsMessage message = android.telephony.SmsMessage.createFromPdu(pdu, format); if (message == null || message.mWrappedSmsMessage == null) { throw new RemoteException( "Status report received with a PDU that could not be parsed."); } mSmsDispatchersController.handleSmsStatusReport(format, pdu); try { getImsManager().acknowledgeSmsReport( token, message.mWrappedSmsMessage.mMessageRef, ImsSmsImplBase.STATUS_REPORT_STATUS_OK); } catch (ImsException e) { loge("Failed to acknowledgeSmsReport(). Error: " + e.getMessage()); } } finally { Binder.restoreCallingIdentity(identity); } } @Override public void onSmsReceived(int token, String format, byte[] pdu) { final long identity = Binder.clearCallingIdentity(); try { logd("SMS received."); android.telephony.SmsMessage message = android.telephony.SmsMessage.createFromPdu(pdu, format); mSmsDispatchersController.injectSmsPdu(message, format, result -> { logd("SMS handled result: " + result); int mappedResult; switch (result) { case Intents.RESULT_SMS_HANDLED: mappedResult = ImsSmsImplBase.DELIVER_STATUS_OK; if (message != null) { mSmsDispatchersController .notifySmsReceivedViaImsToEmergencyStateTracker( message.getOriginatingAddress()); } break; case Intents.RESULT_SMS_OUT_OF_MEMORY: mappedResult = ImsSmsImplBase.DELIVER_STATUS_ERROR_NO_MEMORY; break; case Intents.RESULT_SMS_UNSUPPORTED: mappedResult = ImsSmsImplBase.DELIVER_STATUS_ERROR_REQUEST_NOT_SUPPORTED; break; case Activity.RESULT_OK: // class2 message saving to SIM operation is in progress, defer ack // until saving to SIM is success/failure return; default: mappedResult = ImsSmsImplBase.DELIVER_STATUS_ERROR_GENERIC; break; } try { if (message != null && message.mWrappedSmsMessage != null) { getImsManager().acknowledgeSms(token, message.mWrappedSmsMessage.mMessageRef, mappedResult); } else { logw("SMS Received with a PDU that could not be parsed."); getImsManager().acknowledgeSms(token, 0, mappedResult); } } catch (ImsException e) { loge("Failed to acknowledgeSms(). Error: " + e.getMessage()); } }, true /* ignoreClass */, true /* isOverIms */, token); } finally { Binder.restoreCallingIdentity(identity); } } }; @Override public void handleMessage(Message msg) { switch (msg.what) { case EVENT_SEND_RETRY: logd("SMS retry.."); sendSms((SmsTracker) msg.obj); break; case EVENT_RETRY_SMMA: logd("SMMA Notification retry.."); onMemoryAvailable(); break; default: super.handleMessage(msg); } } public ImsSmsDispatcher(Phone phone, SmsDispatchersController smsDispatchersController, FeatureConnectorFactory factory) { super(phone, smsDispatchersController); mConnectorFactory = factory; mImsManagerConnector = mConnectorFactory.create(mContext, mPhone.getPhoneId(), TAG, new FeatureConnector.Listener() { public void connectionReady(ImsManager manager, int subId) throws ImsException { logd("ImsManager: connection ready."); synchronized (mLock) { mImsManager = manager; setListeners(); mIsImsServiceUp = true; /* set ImsManager */ mSmsDispatchersController.setImsManager(mImsManager); } } @Override public void connectionUnavailable(int reason) { logd("ImsManager: connection unavailable, reason=" + reason); if (reason == FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE) { loge("connectionUnavailable: unexpected, received server error"); removeCallbacks(mConnectRunnable); postDelayed(mConnectRunnable, CONNECT_DELAY_MS); } synchronized (mLock) { mImsManager = null; mIsImsServiceUp = false; /* unset ImsManager */ mSmsDispatchersController.setImsManager(null); } } }, this::post); post(mConnectRunnable); } private void setListeners() throws ImsException { getImsManager().addRegistrationCallback(mRegistrationCallback, this::post); getImsManager().addCapabilitiesCallback(mCapabilityCallback, this::post); getImsManager().setSmsListener(getSmsListener()); getImsManager().onSmsReady(); } private boolean isLteService() { return ((mPhone.getServiceState().getRilDataRadioTechnology() == ServiceState.RIL_RADIO_TECHNOLOGY_LTE) && (mPhone.getServiceState(). getDataRegistrationState() == ServiceState.STATE_IN_SERVICE)); } private boolean isLimitedLteService() { return ((mPhone.getServiceState().getRilVoiceRadioTechnology() == ServiceState.RIL_RADIO_TECHNOLOGY_LTE) && mPhone.getServiceState().isEmergencyOnly()); } private boolean isEmergencySmsPossible() { return isLteService() || isLimitedLteService(); } public boolean isEmergencySmsSupport(String destAddr) { PersistableBundle b; boolean eSmsCarrierSupport = false; if (!mTelephonyManager.isEmergencyNumber(destAddr)) { logi(Rlog.pii(TAG, destAddr) + " is not emergency number"); return false; } final long identity = Binder.clearCallingIdentity(); try { CarrierConfigManager configManager = (CarrierConfigManager) mContext .getSystemService(Context.CARRIER_CONFIG_SERVICE); if (configManager == null) { loge("configManager is null"); return false; } b = configManager.getConfigForSubId(getSubId()); if (b == null) { loge("PersistableBundle is null"); return false; } eSmsCarrierSupport = b.getBoolean( CarrierConfigManager.KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL); boolean lteOrLimitedLte = isEmergencySmsPossible(); logi("isEmergencySmsSupport emergencySmsCarrierSupport: " + eSmsCarrierSupport + " destAddr: " + Rlog.pii(TAG, destAddr) + " mIsImsServiceUp: " + mIsImsServiceUp + " lteOrLimitedLte: " + lteOrLimitedLte); return eSmsCarrierSupport && mIsImsServiceUp && lteOrLimitedLte; } finally { Binder.restoreCallingIdentity(identity); } } public boolean isAvailable() { synchronized (mLock) { logd("isAvailable: up=" + mIsImsServiceUp + ", reg= " + mIsRegistered + ", cap= " + mIsSmsCapable); return mIsImsServiceUp && mIsRegistered && mIsSmsCapable; } } @Override protected String getFormat() { // This is called in the constructor before ImsSmsDispatcher has a chance to initialize // mLock. ImsManager will not be up anyway at this point, so report UNKNOWN. if (mLock == null) return SmsConstants.FORMAT_UNKNOWN; try { return getImsManager().getSmsFormat(); } catch (ImsException e) { loge("Failed to get sms format. Error: " + e.getMessage()); return SmsConstants.FORMAT_UNKNOWN; } } @Override public int getMaxSmsRetryCount() { int retryCount = MAX_SEND_RETRIES; CarrierConfigManager mConfigManager; mConfigManager = (CarrierConfigManager) mContext.getSystemService( Context.CARRIER_CONFIG_SERVICE); if (mConfigManager != null) { PersistableBundle carrierConfig = mConfigManager.getConfigForSubId( getSubId()); if (carrierConfig != null) { retryCount = carrierConfig.getInt( CarrierConfigManager.ImsSms.KEY_SMS_MAX_RETRY_COUNT_INT); } } Rlog.d(TAG, "Retry Count: " + retryCount); return retryCount; } /** * Returns the number of times SMS can be sent over IMS * * @return retry count over Ims from carrier configuration */ @VisibleForTesting public int getMaxRetryCountOverIms() { int retryCountOverIms = MAX_SEND_RETRIES_OVER_IMS; CarrierConfigManager mConfigManager; mConfigManager = (CarrierConfigManager) mContext.getSystemService(Context .CARRIER_CONFIG_SERVICE); if (mConfigManager != null) { PersistableBundle carrierConfig = mConfigManager.getConfigForSubId( getSubId()); if (carrierConfig != null) { retryCountOverIms = carrierConfig.getInt( CarrierConfigManager.ImsSms.KEY_SMS_MAX_RETRY_OVER_IMS_COUNT_INT); } } Rlog.d(TAG, "Retry Count Over Ims: " + retryCountOverIms); return retryCountOverIms; } @Override public int getSmsRetryDelayValue() { int retryDelay = SEND_RETRY_DELAY; CarrierConfigManager mConfigManager; mConfigManager = (CarrierConfigManager) mContext.getSystemService( Context.CARRIER_CONFIG_SERVICE); if (mConfigManager != null) { PersistableBundle carrierConfig = mConfigManager.getConfigForSubId( getSubId()); if (carrierConfig != null) { retryDelay = carrierConfig.getInt( CarrierConfigManager.ImsSms.KEY_SMS_OVER_IMS_SEND_RETRY_DELAY_MILLIS_INT); } } Rlog.d(TAG, "Retry delay: " + retryDelay); return retryDelay; } @Override protected boolean shouldBlockSmsForEcbm() { // We should not block outgoing SMS during ECM on IMS. It only applies to outgoing CDMA // SMS. return false; } @Override protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr, String message, boolean statusReportRequested, SmsHeader smsHeader, int priority, int validityPeriod) { return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, message, statusReportRequested, smsHeader, priority, validityPeriod); } @Override protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr, int destPort, byte[] message, boolean statusReportRequested) { return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, destPort, message, statusReportRequested); } @Override protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr, String message, boolean statusReportRequested, SmsHeader smsHeader, int priority, int validityPeriod, int messageRef) { return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, message, statusReportRequested, smsHeader, priority, validityPeriod, messageRef); } @Override protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr, int destPort, byte[] message, boolean statusReportRequested, int messageRef) { return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, destPort, message, statusReportRequested, messageRef); } @Override protected TextEncodingDetails calculateLength(CharSequence messageBody, boolean use7bitOnly) { return SMSDispatcherUtil.calculateLength(isCdmaMo(), messageBody, use7bitOnly); } /** * Send the Memory Available Event to the ImsService */ public void onMemoryAvailable() { logd("onMemoryAvailable "); int token = mNextToken.incrementAndGet(); try { logd("onMemoryAvailable: token = " + token); mMemoryAvailableNotifierList.add(token); getImsManager().onMemoryAvailable(token); } catch (ImsException e) { loge("onMemoryAvailable failed: " + e.getMessage()); if (mMemoryAvailableNotifierList.contains(token)) { mMemoryAvailableNotifierList.remove(Integer.valueOf(token)); } } } @Override public void sendSms(SmsTracker tracker) { logd("sendSms: " + " mRetryCount=" + tracker.mRetryCount + " mMessageRef=" + tracker.mMessageRef + " SS=" + mPhone.getServiceState().getState()); // Flag that this Tracker is using the ImsService implementation of SMS over IMS for sending // this message. Any fallbacks will happen over CS only. tracker.mUsesImsServiceForIms = true; HashMap map = tracker.getData(); byte[] pdu = (byte[]) map.get(MAP_KEY_PDU); byte smsc[] = (byte[]) map.get(MAP_KEY_SMSC); boolean isRetry = tracker.mRetryCount > 0; String format = getFormat(); if (SmsConstants.FORMAT_3GPP.equals(format) && isRetry) { // per TS 23.040 Section 9.2.3.6: If TP-MTI SMS-SUBMIT (0x01) type // TP-RD (bit 2) is 1 for retry // and TP-MR is set to previously failed sms TP-MR if (((0x01 & pdu[0]) == 0x01)) { pdu[0] |= 0x04; // TP-RD pdu[1] = (byte) tracker.mMessageRef; // TP-MR } } int token = mNextToken.incrementAndGet(); mTrackers.put(token, tracker); try { getImsManager().sendSms( token, tracker.mMessageRef, format, smsc != null ? IccUtils.bytesToHexString(smsc) : null, isRetry, pdu); mMetrics.writeImsServiceSendSms(mPhone.getPhoneId(), format, ImsSmsImplBase.SEND_STATUS_OK, tracker.mMessageId); } catch (ImsException e) { loge("sendSms failed. Falling back to PSTN. Error: " + e.getMessage()); mTrackers.remove(token); fallbackToPstn(tracker); mMetrics.writeImsServiceSendSms(mPhone.getPhoneId(), format, ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK, tracker.mMessageId); mPhone.getSmsStats().onOutgoingSms( true /* isOverIms */, SmsConstants.FORMAT_3GPP2.equals(format), true /* fallbackToCs */, SmsManager.RESULT_SYSTEM_ERROR, tracker.mMessageId, tracker.isFromDefaultSmsApplication(mContext), tracker.getInterval(), mTelephonyManager.isEmergencyNumber(tracker.mDestAddress)); if (mPhone != null) { TelephonyAnalytics telephonyAnalytics = mPhone.getTelephonyAnalytics(); if (telephonyAnalytics != null) { SmsMmsAnalytics smsMmsAnalytics = telephonyAnalytics.getSmsMmsAnalytics(); if (smsMmsAnalytics != null) { smsMmsAnalytics.onOutgoingSms( true /* isOverIms */, SmsManager.RESULT_SYSTEM_ERROR ); } } } } } private ImsManager getImsManager() throws ImsException { synchronized (mLock) { if (mImsManager == null) { throw new ImsException("ImsManager not up", ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN); } return mImsManager; } } @VisibleForTesting public void fallbackToPstn(SmsTracker tracker) { mSmsDispatchersController.sendRetrySms(tracker); } @Override protected boolean isCdmaMo() { return mSmsDispatchersController.isCdmaFormat(getFormat()); } @VisibleForTesting public IImsSmsListener getSmsListener() { return mImsSmsListener; } private void logd(String s) { Rlog.d(TAG + " [" + getPhoneId(mPhone) + "]", s); } private void logi(String s) { Rlog.i(TAG + " [" + getPhoneId(mPhone) + "]", s); } private void logw(String s) { Rlog.w(TAG + " [" + getPhoneId(mPhone) + "]", s); } private void loge(String s) { Rlog.e(TAG + " [" + getPhoneId(mPhone) + "]", s); } private static String getPhoneId(Phone phone) { return (phone != null) ? Integer.toString(phone.getPhoneId()) : "?"; } }