• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.mms.service;
18 
19 import android.app.Activity;
20 import android.app.PendingIntent;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.net.Uri;
24 import android.net.wifi.WifiInfo;
25 import android.net.wifi.WifiManager;
26 import android.os.Bundle;
27 import android.service.carrier.CarrierMessagingService;
28 import android.service.carrier.CarrierMessagingServiceWrapper.CarrierMessagingCallback;
29 import android.telephony.AnomalyReporter;
30 import android.telephony.PreciseDataConnectionState;
31 import android.telephony.SmsManager;
32 import android.telephony.TelephonyCallback;
33 import android.telephony.TelephonyManager;
34 import android.telephony.data.ApnSetting;
35 import android.telephony.ims.ImsMmTelManager;
36 import android.telephony.ims.feature.MmTelFeature;
37 import android.telephony.ims.stub.ImsRegistrationImplBase;
38 
39 import com.android.mms.service.exception.ApnException;
40 import com.android.mms.service.exception.MmsHttpException;
41 import com.android.mms.service.exception.MmsNetworkException;
42 import com.android.mms.service.metrics.MmsStats;
43 
44 import java.util.UUID;
45 
46 /**
47  * Base class for MMS requests. This has the common logic of sending/downloading MMS.
48  */
49 public abstract class MmsRequest {
50     private static final int RETRY_TIMES = 3;
51     // Signal level threshold for both wifi and cellular
52     private static final int SIGNAL_LEVEL_THRESHOLD = 2;
53     // MMS anomaly uuid
54     private final UUID mAnomalyUUID = UUID.fromString("e4330975-17be-43b7-87d6-d9f281d33278");
55     public static final String EXTRA_LAST_CONNECTION_FAILURE_CAUSE_CODE
56             = "android.telephony.extra.LAST_CONNECTION_FAILURE_CAUSE_CODE";
57     public static final String EXTRA_HANDLED_BY_CARRIER_APP
58             = "android.telephony.extra.HANDLED_BY_CARRIER_APP";
59 
60     /**
61      * Interface for certain functionalities from MmsService
62      */
63     public static interface RequestManager {
64         /**
65          * Enqueue an MMS request
66          *
67          * @param request the request to enqueue
68          */
addSimRequest(MmsRequest request)69         public void addSimRequest(MmsRequest request);
70 
71         /*
72          * @return Whether to auto persist received MMS
73          */
getAutoPersistingPref()74         public boolean getAutoPersistingPref();
75 
76         /**
77          * Read pdu (up to maxSize bytes) from supplied content uri
78          * @param contentUri content uri from which to read
79          * @param maxSize maximum number of bytes to read
80          * @return read pdu (else null in case of error or too big)
81          */
readPduFromContentUri(final Uri contentUri, final int maxSize)82         public byte[] readPduFromContentUri(final Uri contentUri, final int maxSize);
83 
84         /**
85          * Write pdu to supplied content uri
86          * @param contentUri content uri to which bytes should be written
87          * @param pdu pdu bytes to write
88          * @return true in case of success (else false)
89          */
writePduToContentUri(final Uri contentUri, final byte[] pdu)90         public boolean writePduToContentUri(final Uri contentUri, final byte[] pdu);
91     }
92 
93     // The reference to the pending requests manager (i.e. the MmsService)
94     protected RequestManager mRequestManager;
95     // The SIM id
96     protected int mSubId;
97     // The creator app
98     protected String mCreator;
99     // MMS config
100     protected Bundle mMmsConfig;
101     // Context used to get TelephonyManager.
102     protected Context mContext;
103     protected long mMessageId;
104     protected int mLastConnectionFailure;
105     private MmsStats mMmsStats;
106 
107     class MonitorTelephonyCallback extends TelephonyCallback implements
108             TelephonyCallback.PreciseDataConnectionStateListener {
109         @Override
onPreciseDataConnectionStateChanged( PreciseDataConnectionState connectionState)110         public void onPreciseDataConnectionStateChanged(
111                 PreciseDataConnectionState connectionState) {
112             if (connectionState == null) {
113                 return;
114             }
115             ApnSetting apnSetting = connectionState.getApnSetting();
116             int apnTypes = apnSetting.getApnTypeBitmask();
117             if ((apnTypes & ApnSetting.TYPE_MMS) != 0) {
118                 mLastConnectionFailure = connectionState.getLastCauseCode();
119                 LogUtil.d("onPreciseDataConnectionStateChanged mLastConnectionFailure: "
120                         + mLastConnectionFailure);
121             }
122         }
123     }
124 
MmsRequest(RequestManager requestManager, int subId, String creator, Bundle mmsConfig, Context context, long messageId, MmsStats mmsStats)125     public MmsRequest(RequestManager requestManager, int subId, String creator,
126             Bundle mmsConfig, Context context, long messageId, MmsStats mmsStats) {
127         mRequestManager = requestManager;
128         mSubId = subId;
129         mCreator = creator;
130         mMmsConfig = mmsConfig;
131         mContext = context;
132         mMessageId = messageId;
133         mMmsStats = mmsStats;
134     }
135 
getSubId()136     public int getSubId() {
137         return mSubId;
138     }
139 
140     /**
141      * Execute the request
142      *
143      * @param context The context
144      * @param networkManager The network manager to use
145      */
execute(Context context, MmsNetworkManager networkManager)146     public void execute(Context context, MmsNetworkManager networkManager) {
147         final String requestId = this.getRequestId();
148         LogUtil.i(requestId, "Executing...");
149         int result = SmsManager.MMS_ERROR_UNSPECIFIED;
150         int httpStatusCode = 0;
151         byte[] response = null;
152         int retryId = 0;
153         // TODO: add mms data channel check back to fast fail if no way to send mms,
154         // when telephony provides such API.
155         if (!prepareForHttpRequest()) { // Prepare request, like reading pdu data from user
156             LogUtil.e(requestId, "Failed to prepare for request");
157             result = SmsManager.MMS_ERROR_IO_ERROR;
158         } else { // Execute
159             long retryDelaySecs = 2;
160             // Try multiple times of MMS HTTP request, depending on the error.
161             for (retryId = 0; retryId < RETRY_TIMES; retryId++) {
162                 httpStatusCode = 0; // Clear for retry.
163                 MonitorTelephonyCallback connectionStateCallback = new MonitorTelephonyCallback();
164                 try {
165                     listenToDataConnectionState(connectionStateCallback);
166                     networkManager.acquireNetwork(requestId);
167                     final String apnName = networkManager.getApnName();
168                     LogUtil.d(requestId, "APN name is " + apnName);
169                     try {
170                         ApnSettings apn = null;
171                         try {
172                             apn = ApnSettings.load(context, apnName, mSubId, requestId);
173                         } catch (ApnException e) {
174                             // If no APN could be found, fall back to trying without the APN name
175                             if (apnName == null) {
176                                 // If the APN name was already null then don't need to retry
177                                 throw (e);
178                             }
179                             LogUtil.i(requestId, "No match with APN name: "
180                                     + apnName + ", try with no name");
181                             apn = ApnSettings.load(context, null, mSubId, requestId);
182                         }
183                         LogUtil.i(requestId, "Using " + apn.toString());
184                         response = doHttp(context, networkManager, apn);
185                         result = Activity.RESULT_OK;
186                         // Success
187                         break;
188                     } finally {
189                         // Release the MMS network immediately except successful DownloadRequest.
190                         networkManager.releaseNetwork(requestId,
191                                 this instanceof DownloadRequest && result == Activity.RESULT_OK);
192                     }
193                 } catch (ApnException e) {
194                     LogUtil.e(requestId, "APN failure", e);
195                     result = SmsManager.MMS_ERROR_INVALID_APN;
196                     break;
197                 } catch (MmsNetworkException e) {
198                     LogUtil.e(requestId, "MMS network acquiring failure", e);
199                     result = SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS;
200                     break;
201                 } catch (MmsHttpException e) {
202                     LogUtil.e(requestId, "HTTP or network I/O failure", e);
203                     result = SmsManager.MMS_ERROR_HTTP_FAILURE;
204                     httpStatusCode = e.getStatusCode();
205                     // Retry
206                 } catch (Exception e) {
207                     LogUtil.e(requestId, "Unexpected failure", e);
208                     result = SmsManager.MMS_ERROR_UNSPECIFIED;
209                     break;
210                 } finally {
211                     stopListeningToDataConnectionState(connectionStateCallback);
212                 }
213                 try {
214                     Thread.sleep(retryDelaySecs * 1000, 0/*nano*/);
215                 } catch (InterruptedException e) {}
216                 retryDelaySecs <<= 1;
217             }
218         }
219         processResult(context, result, response, httpStatusCode, /* handledByCarrierApp= */ false,
220                 retryId);
221     }
222 
listenToDataConnectionState(MonitorTelephonyCallback connectionStateCallback)223     private void listenToDataConnectionState(MonitorTelephonyCallback connectionStateCallback) {
224         final TelephonyManager telephonyManager = mContext.getSystemService(
225                 TelephonyManager.class).createForSubscriptionId(mSubId);
226         telephonyManager.registerTelephonyCallback(r -> r.run(), connectionStateCallback);
227     }
228 
stopListeningToDataConnectionState( MonitorTelephonyCallback connectionStateCallback)229     private void stopListeningToDataConnectionState(
230             MonitorTelephonyCallback connectionStateCallback) {
231         final TelephonyManager telephonyManager = mContext.getSystemService(
232                 TelephonyManager.class).createForSubscriptionId(mSubId);
233         telephonyManager.unregisterTelephonyCallback(connectionStateCallback);
234     }
235 
236     /**
237      * Process the result of the completed request, including updating the message status
238      * in database and sending back the result via pending intents.
239      * @param context The context
240      * @param result The result code of execution
241      * @param response The response body
242      * @param httpStatusCode The optional http status code in case of http failure
243      * @param handledByCarrierApp True if the sending/downloading was handled by a carrier app
244      *                            rather than MmsService.
245      */
processResult(Context context, int result, byte[] response, int httpStatusCode, boolean handledByCarrierApp)246     public void processResult(Context context, int result, byte[] response, int httpStatusCode,
247             boolean handledByCarrierApp) {
248         processResult(context, result, response, httpStatusCode, handledByCarrierApp, 0);
249     }
250 
processResult(Context context, int result, byte[] response, int httpStatusCode, boolean handledByCarrierApp, int retryId)251     private void processResult(Context context, int result, byte[] response, int httpStatusCode,
252             boolean handledByCarrierApp, int retryId) {
253         final Uri messageUri = persistIfRequired(context, result, response);
254 
255         final String requestId = this.getRequestId();
256         // As noted in the @param comment above, the httpStatusCode is only set when there's
257         // an http failure. On success, such as an http code of 200, the value here will be 0.
258         // "httpStatusCode: xxx" is now reported for an http failure only.
259         LogUtil.i(requestId, "processResult: "
260                 + (result == Activity.RESULT_OK ? "success" : "failure(" + result + ")")
261                 + (httpStatusCode != 0 ? ", httpStatusCode: " + httpStatusCode : "")
262                 + " handledByCarrierApp: " + handledByCarrierApp
263                 + " mLastConnectionFailure: " + mLastConnectionFailure);
264 
265         // Return MMS HTTP request result via PendingIntent
266         final PendingIntent pendingIntent = getPendingIntent();
267         if (pendingIntent != null) {
268             boolean succeeded = true;
269             // Extra information to send back with the pending intent
270             Intent fillIn = new Intent();
271             if (response != null) {
272                 succeeded = transferResponse(fillIn, response);
273             }
274             if (messageUri != null) {
275                 fillIn.putExtra("uri", messageUri.toString());
276             }
277             if (result == SmsManager.MMS_ERROR_HTTP_FAILURE && httpStatusCode != 0) {
278                 fillIn.putExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, httpStatusCode);
279             }
280             fillIn.putExtra(EXTRA_LAST_CONNECTION_FAILURE_CAUSE_CODE,
281                     mLastConnectionFailure);
282             fillIn.putExtra(EXTRA_HANDLED_BY_CARRIER_APP, handledByCarrierApp);
283             try {
284                 if (!succeeded) {
285                     result = SmsManager.MMS_ERROR_IO_ERROR;
286                 }
287                 reportPossibleAnomaly(result, httpStatusCode);
288                 pendingIntent.send(context, result, fillIn);
289                 mMmsStats.addAtomToStorage(result, retryId, handledByCarrierApp);
290             } catch (PendingIntent.CanceledException e) {
291                 LogUtil.e(requestId, "Sending pending intent canceled", e);
292             }
293         }
294 
295         revokeUriPermission(context);
296     }
297 
reportPossibleAnomaly(int result, int httpStatusCode)298     private void reportPossibleAnomaly(int result, int httpStatusCode) {
299         switch (result) {
300             case SmsManager.MMS_ERROR_HTTP_FAILURE:
301                 if (isPoorSignal()) {
302                     LogUtil.i(this.toString(), "Poor Signal");
303                     break;
304                 }
305             case SmsManager.MMS_ERROR_INVALID_APN:
306             case SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS:
307             case SmsManager.MMS_ERROR_UNSPECIFIED:
308             case SmsManager.MMS_ERROR_IO_ERROR:
309                 String message = "MMS failed";
310                 LogUtil.i(this.toString(),
311                         message + " with error: " + result + " httpStatus:" + httpStatusCode);
312                 TelephonyManager telephonyManager =
313                         mContext.getSystemService(TelephonyManager.class)
314                                 .createForSubscriptionId(mSubId);
315                 AnomalyReporter.reportAnomaly(
316                         generateUUID(result, httpStatusCode),
317                         message,
318                         telephonyManager.getSimCarrierId());
319                 break;
320             default:
321                 break;
322         }
323     }
324 
generateUUID(int result, int httpStatusCode)325     private UUID generateUUID(int result, int httpStatusCode) {
326         long lresult = result;
327         long lhttpStatusCode = httpStatusCode;
328         return new UUID(mAnomalyUUID.getMostSignificantBits(),
329                 mAnomalyUUID.getLeastSignificantBits() + ((lhttpStatusCode << 32) + lresult));
330     }
331 
isPoorSignal()332     private boolean isPoorSignal() {
333         // Check Wifi signal strength when IMS registers via Wifi
334         if (isImsOnWifi()) {
335             int rssi = 0;
336             WifiManager wifiManager = mContext.getSystemService(WifiManager.class);
337             final WifiInfo wifiInfo = wifiManager.getConnectionInfo();
338             if (wifiInfo != null) {
339                 rssi = wifiInfo.getRssi();
340             } else {
341                 return false;
342             }
343             final int wifiLevel = wifiManager.calculateSignalLevel(rssi);
344             LogUtil.d(this.toString(), "Wifi signal rssi: " + rssi + " level:" + wifiLevel);
345             if (wifiLevel <= SIGNAL_LEVEL_THRESHOLD) {
346                 return true;
347             }
348             return false;
349         } else {
350             // Check cellular signal strength
351             final TelephonyManager telephonyManager = mContext.getSystemService(
352                     TelephonyManager.class).createForSubscriptionId(mSubId);
353             final int cellLevel = telephonyManager.getSignalStrength().getLevel();
354             LogUtil.d(this.toString(), "Cellular signal level:" + cellLevel);
355             if (cellLevel <= SIGNAL_LEVEL_THRESHOLD) {
356                 return true;
357             }
358             return false;
359         }
360     }
361 
isImsOnWifi()362     private boolean isImsOnWifi() {
363         ImsMmTelManager imsManager;
364         try {
365             imsManager = ImsMmTelManager.createForSubscriptionId(mSubId);
366         } catch (IllegalArgumentException e) {
367             LogUtil.e(this.toString(), "invalid subid:" + mSubId);
368             return false;
369         }
370         if (imsManager != null) {
371             return imsManager.isAvailable(MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
372                     ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN);
373         } else {
374             return false;
375         }
376     }
377 
378     /**
379      * Returns true if sending / downloading using the carrier app has failed and completes the
380      * action using platform API's, otherwise false.
381      */
maybeFallbackToRegularDelivery(int carrierMessagingAppResult)382     protected boolean maybeFallbackToRegularDelivery(int carrierMessagingAppResult) {
383         if (carrierMessagingAppResult
384                 == CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK
385                 || carrierMessagingAppResult
386                         == CarrierMessagingService.DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK) {
387             LogUtil.d(this.toString(), "Sending/downloading MMS by IP failed. "
388                     + MmsService.formatCrossStackMessageId(mMessageId));
389             mRequestManager.addSimRequest(MmsRequest.this);
390             return true;
391         } else {
392             return false;
393         }
394     }
395 
396     /**
397      * Converts from {@code carrierMessagingAppResult} to a platform result code.
398      */
toSmsManagerResult(int carrierMessagingAppResult)399     protected static int toSmsManagerResult(int carrierMessagingAppResult) {
400         switch (carrierMessagingAppResult) {
401             case CarrierMessagingService.SEND_STATUS_OK:
402                 return Activity.RESULT_OK;
403             case CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK:
404                 return SmsManager.MMS_ERROR_RETRY;
405             default:
406                 return SmsManager.MMS_ERROR_UNSPECIFIED;
407         }
408     }
409 
410     @Override
toString()411     public String toString() {
412         return getClass().getSimpleName() + '@' + Integer.toHexString(hashCode())
413                 + " " + MmsService.formatCrossStackMessageId(mMessageId);
414     }
415 
getRequestId()416     protected String getRequestId() {
417         return this.toString();
418     }
419 
420     /**
421      * Making the HTTP request to MMSC
422      *
423      * @param context The context
424      * @param netMgr The current {@link MmsNetworkManager}
425      * @param apn The APN setting
426      * @return The HTTP response data
427      * @throws MmsHttpException If any network error happens
428      */
doHttp(Context context, MmsNetworkManager netMgr, ApnSettings apn)429     protected abstract byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettings apn)
430             throws MmsHttpException;
431 
432     /**
433      * @return The PendingIntent associate with the MMS sending invocation
434      */
getPendingIntent()435     protected abstract PendingIntent getPendingIntent();
436 
437     /**
438      * @return The queue should be used by this request, 0 is sending and 1 is downloading
439      */
getQueueType()440     protected abstract int getQueueType();
441 
442     /**
443      * Persist message into telephony if required (i.e. when auto-persisting is on or
444      * the calling app is non-default sms app for sending)
445      *
446      * @param context The context
447      * @param result The result code of execution
448      * @param response The response body
449      * @return The persisted URI of the message or null if we don't persist or fail
450      */
persistIfRequired(Context context, int result, byte[] response)451     protected abstract Uri persistIfRequired(Context context, int result, byte[] response);
452 
453     /**
454      * Prepare to make the HTTP request - will download message for sending
455      * @return true if preparation succeeds (and request can proceed) else false
456      */
prepareForHttpRequest()457     protected abstract boolean prepareForHttpRequest();
458 
459     /**
460      * Transfer the received response to the caller
461      *
462      * @param fillIn the intent that will be returned to the caller
463      * @param response the pdu to transfer
464      * @return true if response transfer succeeds else false
465      */
transferResponse(Intent fillIn, byte[] response)466     protected abstract boolean transferResponse(Intent fillIn, byte[] response);
467 
468     /**
469      * Revoke the content URI permission granted by the MMS app to the phone package.
470      *
471      * @param context The context
472      */
revokeUriPermission(Context context)473     protected abstract void revokeUriPermission(Context context);
474 
475     /**
476      * Base class for handling carrier app send / download result.
477      */
478     protected abstract class CarrierMmsActionCallback implements CarrierMessagingCallback {
479         @Override
onSendSmsComplete(int result, int messageRef)480         public void onSendSmsComplete(int result, int messageRef) {
481             LogUtil.e("Unexpected onSendSmsComplete call for "
482                     + MmsService.formatCrossStackMessageId(mMessageId)
483                     + " with result: " + result);
484         }
485 
486         @Override
onSendMultipartSmsComplete(int result, int[] messageRefs)487         public void onSendMultipartSmsComplete(int result, int[] messageRefs) {
488             LogUtil.e("Unexpected onSendMultipartSmsComplete call for "
489                     + MmsService.formatCrossStackMessageId(mMessageId)
490                     + " with result: " + result);
491         }
492 
493         @Override
onReceiveSmsComplete(int result)494         public void onReceiveSmsComplete(int result) {
495             LogUtil.e("Unexpected onFilterComplete call for "
496                     + MmsService.formatCrossStackMessageId(mMessageId)
497                     + " with result: " + result);
498         }
499     }
500 }
501