1 /*
2  * Copyright (C) 2018 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 
17 package com.android.internal.telephony;
18 
19 import android.content.Context;
20 import android.os.Binder;
21 import android.os.PersistableBundle;
22 import android.os.RemoteException;
23 import android.provider.Telephony.Sms.Intents;
24 import android.telephony.CarrierConfigManager;
25 import android.telephony.ServiceState;
26 import android.telephony.SmsManager;
27 import android.telephony.ims.ImsReasonInfo;
28 import android.telephony.ims.RegistrationManager;
29 import android.telephony.ims.aidl.IImsSmsListener;
30 import android.telephony.ims.feature.MmTelFeature;
31 import android.telephony.ims.stub.ImsRegistrationImplBase;
32 import android.telephony.ims.stub.ImsSmsImplBase;
33 import android.telephony.ims.stub.ImsSmsImplBase.SendStatusResult;
34 
35 import com.android.ims.FeatureConnector;
36 import com.android.ims.ImsException;
37 import com.android.ims.ImsManager;
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
40 import com.android.internal.telephony.metrics.TelephonyMetrics;
41 import com.android.internal.telephony.uicc.IccUtils;
42 import com.android.internal.telephony.util.SMSDispatcherUtil;
43 import com.android.telephony.Rlog;
44 
45 import java.util.HashMap;
46 import java.util.Map;
47 import java.util.concurrent.ConcurrentHashMap;
48 import java.util.concurrent.Executor;
49 import java.util.concurrent.atomic.AtomicInteger;
50 
51 /**
52  * Responsible for communications with {@link com.android.ims.ImsManager} to send/receive messages
53  * over IMS.
54  * @hide
55  */
56 public class ImsSmsDispatcher extends SMSDispatcher {
57 
58     private static final String TAG = "ImsSmsDispatcher";
59     private static final int CONNECT_DELAY_MS = 5000; // 5 seconds;
60 
61     /**
62      * Creates FeatureConnector instances for ImsManager, used during testing to inject mock
63      * connector instances.
64      */
65     @VisibleForTesting
66     public interface FeatureConnectorFactory {
67         /**
68          * Create a new FeatureConnector for ImsManager.
69          */
create(Context context, int phoneId, String logPrefix, FeatureConnector.Listener<ImsManager> listener, Executor executor)70         FeatureConnector<ImsManager> create(Context context, int phoneId, String logPrefix,
71                 FeatureConnector.Listener<ImsManager> listener, Executor executor);
72     }
73 
74     @VisibleForTesting
75     public Map<Integer, SmsTracker> mTrackers = new ConcurrentHashMap<>();
76     @VisibleForTesting
77     public AtomicInteger mNextToken = new AtomicInteger();
78     private final Object mLock = new Object();
79     private volatile boolean mIsSmsCapable;
80     private volatile boolean mIsImsServiceUp;
81     private volatile boolean mIsRegistered;
82     private final FeatureConnector<ImsManager> mImsManagerConnector;
83     /** Telephony metrics instance for logging metrics event */
84     private TelephonyMetrics mMetrics = TelephonyMetrics.getInstance();
85     private ImsManager mImsManager;
86     private FeatureConnectorFactory mConnectorFactory;
87 
88     private Runnable mConnectRunnable = new Runnable() {
89         @Override
90         public void run() {
91             mImsManagerConnector.connect();
92         }
93     };
94 
95     /**
96      * Listen to the IMS service state change
97      *
98      */
99     private RegistrationManager.RegistrationCallback mRegistrationCallback =
100             new RegistrationManager.RegistrationCallback() {
101                 @Override
102                 public void onRegistered(
103                         @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
104                     logd("onImsConnected imsRadioTech=" + imsRadioTech);
105                     synchronized (mLock) {
106                         mIsRegistered = true;
107                     }
108                 }
109 
110                 @Override
111                 public void onRegistering(
112                         @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
113                     logd("onImsProgressing imsRadioTech=" + imsRadioTech);
114                     synchronized (mLock) {
115                         mIsRegistered = false;
116                     }
117                 }
118 
119                 @Override
120                 public void onUnregistered(ImsReasonInfo info) {
121                     logd("onImsDisconnected imsReasonInfo=" + info);
122                     synchronized (mLock) {
123                         mIsRegistered = false;
124                     }
125                 }
126             };
127 
128     private android.telephony.ims.ImsMmTelManager.CapabilityCallback mCapabilityCallback =
129             new android.telephony.ims.ImsMmTelManager.CapabilityCallback() {
130                 @Override
131                 public void onCapabilitiesStatusChanged(
132                         MmTelFeature.MmTelCapabilities capabilities) {
133                     synchronized (mLock) {
134                         mIsSmsCapable = capabilities.isCapable(
135                                 MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_SMS);
136                     }
137                 }
138     };
139 
140     private final IImsSmsListener mImsSmsListener = new IImsSmsListener.Stub() {
141         @Override
142         public void onSendSmsResult(int token, int messageRef, @SendStatusResult int status,
143                 @SmsManager.Result int reason, int networkReasonCode) {
144             final long identity = Binder.clearCallingIdentity();
145             try {
146                 logd("onSendSmsResult token=" + token + " messageRef=" + messageRef
147                         + " status=" + status + " reason=" + reason + " networkReasonCode="
148                         + networkReasonCode);
149                 // TODO integrate networkReasonCode into IMS SMS metrics.
150                 SmsTracker tracker = mTrackers.get(token);
151                 mMetrics.writeOnImsServiceSmsSolicitedResponse(mPhone.getPhoneId(), status, reason,
152                         (tracker != null ? tracker.mMessageId : 0L));
153                 if (tracker == null) {
154                     throw new IllegalArgumentException("Invalid token.");
155                 }
156                 tracker.mMessageRef = messageRef;
157                 switch(status) {
158                     case ImsSmsImplBase.SEND_STATUS_OK:
159                         if (tracker.mDeliveryIntent != null) {
160                             // Expecting a status report. Put this tracker to the map.
161                             mSmsDispatchersController.putDeliveryPendingTracker(tracker);
162                         }
163                         tracker.onSent(mContext);
164                         mTrackers.remove(token);
165                         mPhone.notifySmsSent(tracker.mDestAddress);
166                         break;
167                     case ImsSmsImplBase.SEND_STATUS_ERROR:
168                         tracker.onFailed(mContext, reason, networkReasonCode);
169                         mTrackers.remove(token);
170                         break;
171                     case ImsSmsImplBase.SEND_STATUS_ERROR_RETRY:
172                         tracker.mRetryCount += 1;
173                         sendSms(tracker);
174                         break;
175                     case ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK:
176                         tracker.mRetryCount += 1;
177                         mTrackers.remove(token);
178                         fallbackToPstn(tracker);
179                         break;
180                     default:
181                 }
182                 mPhone.getSmsStats().onOutgoingSms(
183                         true /* isOverIms */,
184                         SmsConstants.FORMAT_3GPP2.equals(getFormat()),
185                         status == ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK,
186                         reason,
187                         tracker.mMessageId,
188                         tracker.isFromDefaultSmsApplication(mContext));
189             } finally {
190                 Binder.restoreCallingIdentity(identity);
191             }
192         }
193 
194         @Override
195         public void onSmsStatusReportReceived(int token, String format, byte[] pdu)
196                 throws RemoteException {
197             final long identity = Binder.clearCallingIdentity();
198             try {
199                 logd("Status report received.");
200                 android.telephony.SmsMessage message =
201                         android.telephony.SmsMessage.createFromPdu(pdu, format);
202                 if (message == null || message.mWrappedSmsMessage == null) {
203                     throw new RemoteException(
204                             "Status report received with a PDU that could not be parsed.");
205                 }
206                 mSmsDispatchersController.handleSmsStatusReport(format, pdu);
207                 try {
208                     getImsManager().acknowledgeSmsReport(
209                             token,
210                             message.mWrappedSmsMessage.mMessageRef,
211                             ImsSmsImplBase.STATUS_REPORT_STATUS_OK);
212                 } catch (ImsException e) {
213                     loge("Failed to acknowledgeSmsReport(). Error: " + e.getMessage());
214                 }
215             } finally {
216                 Binder.restoreCallingIdentity(identity);
217             }
218         }
219 
220         @Override
221         public void onSmsReceived(int token, String format, byte[] pdu) {
222             final long identity = Binder.clearCallingIdentity();
223             try {
224                 logd("SMS received.");
225                 android.telephony.SmsMessage message =
226                         android.telephony.SmsMessage.createFromPdu(pdu, format);
227                 mSmsDispatchersController.injectSmsPdu(message, format, result -> {
228                     logd("SMS handled result: " + result);
229                     int mappedResult;
230                     switch (result) {
231                         case Intents.RESULT_SMS_HANDLED:
232                             mappedResult = ImsSmsImplBase.DELIVER_STATUS_OK;
233                             break;
234                         case Intents.RESULT_SMS_OUT_OF_MEMORY:
235                             mappedResult = ImsSmsImplBase.DELIVER_STATUS_ERROR_NO_MEMORY;
236                             break;
237                         case Intents.RESULT_SMS_UNSUPPORTED:
238                             mappedResult =
239                                     ImsSmsImplBase.DELIVER_STATUS_ERROR_REQUEST_NOT_SUPPORTED;
240                             break;
241                         default:
242                             mappedResult = ImsSmsImplBase.DELIVER_STATUS_ERROR_GENERIC;
243                             break;
244                     }
245                     try {
246                         if (message != null && message.mWrappedSmsMessage != null) {
247                             getImsManager().acknowledgeSms(token,
248                                     message.mWrappedSmsMessage.mMessageRef, mappedResult);
249                         } else {
250                             logw("SMS Received with a PDU that could not be parsed.");
251                             getImsManager().acknowledgeSms(token, 0, mappedResult);
252                         }
253                     } catch (ImsException e) {
254                         loge("Failed to acknowledgeSms(). Error: " + e.getMessage());
255                     }
256                 }, true /* ignoreClass */, true /* isOverIms */);
257             } finally {
258                 Binder.restoreCallingIdentity(identity);
259             }
260         }
261     };
262 
ImsSmsDispatcher(Phone phone, SmsDispatchersController smsDispatchersController, FeatureConnectorFactory factory)263     public ImsSmsDispatcher(Phone phone, SmsDispatchersController smsDispatchersController,
264             FeatureConnectorFactory factory) {
265         super(phone, smsDispatchersController);
266         mConnectorFactory = factory;
267 
268         mImsManagerConnector = mConnectorFactory.create(mContext, mPhone.getPhoneId(), TAG,
269                 new FeatureConnector.Listener<ImsManager>() {
270                     public void connectionReady(ImsManager manager) throws ImsException {
271                         logd("ImsManager: connection ready.");
272                         synchronized (mLock) {
273                             mImsManager = manager;
274                             setListeners();
275                             mIsImsServiceUp = true;
276                         }
277                     }
278 
279                     @Override
280                     public void connectionUnavailable(int reason) {
281                         logd("ImsManager: connection unavailable, reason=" + reason);
282                         if (reason == FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE) {
283                             loge("connectionUnavailable: unexpected, received server error");
284                             removeCallbacks(mConnectRunnable);
285                             postDelayed(mConnectRunnable, CONNECT_DELAY_MS);
286                         }
287                         synchronized (mLock) {
288                             mImsManager = null;
289                             mIsImsServiceUp = false;
290                         }
291                     }
292                 }, this::post);
293         post(mConnectRunnable);
294     }
295 
setListeners()296     private void setListeners() throws ImsException {
297         getImsManager().addRegistrationCallback(mRegistrationCallback, this::post);
298         getImsManager().addCapabilitiesCallback(mCapabilityCallback, this::post);
299         getImsManager().setSmsListener(getSmsListener());
300         getImsManager().onSmsReady();
301     }
302 
isLteService()303     private boolean isLteService() {
304         return ((mPhone.getServiceState().getRilDataRadioTechnology() ==
305             ServiceState.RIL_RADIO_TECHNOLOGY_LTE) && (mPhone.getServiceState().
306                 getDataRegistrationState() == ServiceState.STATE_IN_SERVICE));
307     }
308 
isLimitedLteService()309     private boolean isLimitedLteService() {
310         return ((mPhone.getServiceState().getRilVoiceRadioTechnology() ==
311             ServiceState.RIL_RADIO_TECHNOLOGY_LTE) && mPhone.getServiceState().isEmergencyOnly());
312     }
313 
isEmergencySmsPossible()314     private boolean isEmergencySmsPossible() {
315         return isLteService() || isLimitedLteService();
316     }
317 
isEmergencySmsSupport(String destAddr)318     public boolean isEmergencySmsSupport(String destAddr) {
319         PersistableBundle b;
320         boolean eSmsCarrierSupport = false;
321         if (!mTelephonyManager.isEmergencyNumber(destAddr)) {
322             logi(Rlog.pii(TAG, destAddr) + " is not emergency number");
323             return false;
324         }
325 
326         final long identity = Binder.clearCallingIdentity();
327         try {
328             CarrierConfigManager configManager = (CarrierConfigManager) mContext
329                     .getSystemService(Context.CARRIER_CONFIG_SERVICE);
330             if (configManager == null) {
331                 loge("configManager is null");
332                 return false;
333             }
334             b = configManager.getConfigForSubId(getSubId());
335             if (b == null) {
336                 loge("PersistableBundle is null");
337                 return false;
338             }
339             eSmsCarrierSupport = b.getBoolean(
340                     CarrierConfigManager.KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL);
341             boolean lteOrLimitedLte = isEmergencySmsPossible();
342             logi("isEmergencySmsSupport emergencySmsCarrierSupport: "
343                     + eSmsCarrierSupport + " destAddr: " + Rlog.pii(TAG, destAddr)
344                     + " mIsImsServiceUp: " + mIsImsServiceUp + " lteOrLimitedLte: "
345                     + lteOrLimitedLte);
346 
347             return eSmsCarrierSupport && mIsImsServiceUp && lteOrLimitedLte;
348         } finally {
349             Binder.restoreCallingIdentity(identity);
350         }
351     }
352 
isAvailable()353     public boolean isAvailable() {
354         synchronized (mLock) {
355             logd("isAvailable: up=" + mIsImsServiceUp + ", reg= " + mIsRegistered
356                     + ", cap= " + mIsSmsCapable);
357             return mIsImsServiceUp && mIsRegistered && mIsSmsCapable;
358         }
359     }
360 
361     @Override
getFormat()362     protected String getFormat() {
363         // This is called in the constructor before ImsSmsDispatcher has a chance to initialize
364         // mLock. ImsManager will not be up anyway at this point, so report UNKNOWN.
365         if (mLock == null) return SmsConstants.FORMAT_UNKNOWN;
366         try {
367             return getImsManager().getSmsFormat();
368         } catch (ImsException e) {
369             loge("Failed to get sms format. Error: " + e.getMessage());
370             return SmsConstants.FORMAT_UNKNOWN;
371         }
372     }
373 
374     @Override
shouldBlockSmsForEcbm()375     protected boolean shouldBlockSmsForEcbm() {
376         // We should not block outgoing SMS during ECM on IMS. It only applies to outgoing CDMA
377         // SMS.
378         return false;
379     }
380 
381     @Override
getSubmitPdu(String scAddr, String destAddr, String message, boolean statusReportRequested, SmsHeader smsHeader, int priority, int validityPeriod)382     protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
383             String message, boolean statusReportRequested, SmsHeader smsHeader, int priority,
384             int validityPeriod) {
385         return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, message,
386                 statusReportRequested, smsHeader, priority, validityPeriod);
387     }
388 
389     @Override
getSubmitPdu(String scAddr, String destAddr, int destPort, byte[] message, boolean statusReportRequested)390     protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
391             int destPort, byte[] message, boolean statusReportRequested) {
392         return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, destPort, message,
393                 statusReportRequested);
394     }
395 
396     @Override
calculateLength(CharSequence messageBody, boolean use7bitOnly)397     protected TextEncodingDetails calculateLength(CharSequence messageBody, boolean use7bitOnly) {
398         return SMSDispatcherUtil.calculateLength(isCdmaMo(), messageBody, use7bitOnly);
399     }
400 
401     @Override
sendSms(SmsTracker tracker)402     public void sendSms(SmsTracker tracker) {
403         logd("sendSms: "
404                 + " mRetryCount=" + tracker.mRetryCount
405                 + " mMessageRef=" + tracker.mMessageRef
406                 + " SS=" + mPhone.getServiceState().getState());
407 
408         // Flag that this Tracker is using the ImsService implementation of SMS over IMS for sending
409         // this message. Any fallbacks will happen over CS only.
410         tracker.mUsesImsServiceForIms = true;
411 
412         HashMap<String, Object> map = tracker.getData();
413 
414         byte[] pdu = (byte[]) map.get(MAP_KEY_PDU);
415         byte smsc[] = (byte[]) map.get(MAP_KEY_SMSC);
416         boolean isRetry = tracker.mRetryCount > 0;
417         String format = getFormat();
418 
419         if (SmsConstants.FORMAT_3GPP.equals(format) && tracker.mRetryCount > 0) {
420             // per TS 23.040 Section 9.2.3.6:  If TP-MTI SMS-SUBMIT (0x01) type
421             //   TP-RD (bit 2) is 1 for retry
422             //   and TP-MR is set to previously failed sms TP-MR
423             if (((0x01 & pdu[0]) == 0x01)) {
424                 pdu[0] |= 0x04; // TP-RD
425                 pdu[1] = (byte) tracker.mMessageRef; // TP-MR
426             }
427         }
428 
429         int token = mNextToken.incrementAndGet();
430         mTrackers.put(token, tracker);
431         try {
432             getImsManager().sendSms(
433                     token,
434                     tracker.mMessageRef,
435                     format,
436                     smsc != null ? IccUtils.bytesToHexString(smsc) : null,
437                     isRetry,
438                     pdu);
439             mMetrics.writeImsServiceSendSms(mPhone.getPhoneId(), format,
440                     ImsSmsImplBase.SEND_STATUS_OK, tracker.mMessageId);
441         } catch (ImsException e) {
442             loge("sendSms failed. Falling back to PSTN. Error: " + e.getMessage());
443             mTrackers.remove(token);
444             fallbackToPstn(tracker);
445             mMetrics.writeImsServiceSendSms(mPhone.getPhoneId(), format,
446                     ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK, tracker.mMessageId);
447             mPhone.getSmsStats().onOutgoingSms(
448                     true /* isOverIms */,
449                     SmsConstants.FORMAT_3GPP2.equals(format),
450                     true /* fallbackToCs */,
451                     SmsManager.RESULT_SYSTEM_ERROR,
452                     tracker.mMessageId,
453                     tracker.isFromDefaultSmsApplication(mContext));
454         }
455     }
456 
getImsManager()457     private ImsManager getImsManager() throws ImsException {
458         synchronized (mLock) {
459             if (mImsManager == null) {
460                 throw new ImsException("ImsManager not up",
461                         ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
462             }
463             return mImsManager;
464         }
465     }
466 
467     @VisibleForTesting
fallbackToPstn(SmsTracker tracker)468     public void fallbackToPstn(SmsTracker tracker) {
469         mSmsDispatchersController.sendRetrySms(tracker);
470     }
471 
472     @Override
isCdmaMo()473     protected boolean isCdmaMo() {
474         return mSmsDispatchersController.isCdmaFormat(getFormat());
475     }
476 
477     @VisibleForTesting
getSmsListener()478     public IImsSmsListener getSmsListener() {
479         return mImsSmsListener;
480     }
481 
logd(String s)482     private void logd(String s) {
483         Rlog.d(TAG + " [" + getPhoneId(mPhone) + "]", s);
484     }
485 
logi(String s)486     private void logi(String s) {
487         Rlog.i(TAG + " [" + getPhoneId(mPhone) + "]", s);
488     }
489 
logw(String s)490     private void logw(String s) {
491         Rlog.w(TAG + " [" + getPhoneId(mPhone) + "]", s);
492     }
493 
loge(String s)494     private void loge(String s) {
495         Rlog.e(TAG + " [" + getPhoneId(mPhone) + "]", s);
496     }
497 
getPhoneId(Phone phone)498     private static String getPhoneId(Phone phone) {
499         return (phone != null) ? Integer.toString(phone.getPhoneId()) : "?";
500     }
501 }
502