/* * 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.mms.service; import android.app.Activity; import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; import android.os.Bundle; import android.os.UserHandle; import android.provider.BlockedNumberContract; import android.provider.Telephony; import android.service.carrier.CarrierMessagingService; import android.service.carrier.CarrierMessagingServiceWrapper; import android.telephony.SmsManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.SmsApplication; import com.android.internal.telephony.SmsNumberUtils; import com.android.mms.service.exception.MmsHttpException; import com.android.mms.service.metrics.MmsStats; import com.google.android.mms.MmsException; import com.google.android.mms.pdu.EncodedStringValue; import com.google.android.mms.pdu.GenericPdu; import com.google.android.mms.pdu.PduComposer; import com.google.android.mms.pdu.PduHeaders; import com.google.android.mms.pdu.PduParser; import com.google.android.mms.pdu.PduPersister; import com.google.android.mms.pdu.SendConf; import com.google.android.mms.pdu.SendReq; import com.google.android.mms.util.SqliteWrapper; /** * Request to send an MMS */ public class SendRequest extends MmsRequest { private final Uri mPduUri; @VisibleForTesting public byte[] mPduData; private final String mLocationUrl; private final PendingIntent mSentIntent; public SendRequest(RequestManager manager, int subId, Uri contentUri, String locationUrl, PendingIntent sentIntent, String creator, Bundle configOverrides, Context context, long messageId, MmsStats mmsStats, TelephonyManager telephonyManager) { super(manager, subId, creator, configOverrides, context, messageId, mmsStats, telephonyManager); mPduUri = contentUri; mPduData = null; mLocationUrl = locationUrl; mSentIntent = sentIntent; } @Override protected byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettings apn) throws MmsHttpException { final String requestId = getRequestId(); final MmsHttpClient mmsHttpClient = netMgr.getOrCreateHttpClient(); if (mmsHttpClient == null) { String notReady = "MMS network is not ready! " + MmsService.formatCrossStackMessageId(mMessageId); LogUtil.e(requestId, notReady); throw new MmsHttpException(0/*statusCode*/, notReady); } final GenericPdu parsedPdu = parsePdu(); notifyIfEmergencyContactNoThrow(parsedPdu); updateDestinationAddress(parsedPdu); return mmsHttpClient.execute( mLocationUrl != null ? mLocationUrl : apn.getMmscUrl(), mPduData, MmsHttpClient.METHOD_POST, apn.isProxySet(), apn.getProxyAddress(), apn.getProxyPort(), mMmsConfig, mSubId, requestId); } private GenericPdu parsePdu() { final String requestId = getRequestId(); try { if (mPduData == null) { LogUtil.d(requestId, "Empty PDU raw data. " + MmsService.formatCrossStackMessageId(mMessageId)); return null; } final boolean supportContentDisposition = mMmsConfig.getBoolean(SmsManager.MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION); return new PduParser(mPduData, supportContentDisposition).parse(); } catch (final Exception e) { LogUtil.e(requestId, "Failed to parse PDU raw data. " + MmsService.formatCrossStackMessageId(mMessageId), e); } return null; } /** * If the MMS is being sent to an emergency number, the blocked number provider is notified * so that it can disable number blocking. */ private void notifyIfEmergencyContactNoThrow(final GenericPdu parsedPdu) { try { notifyIfEmergencyContact(parsedPdu); } catch (Exception e) { LogUtil.w(getRequestId(), "Error in notifyIfEmergencyContact. " + MmsService.formatCrossStackMessageId(mMessageId), e); } } private void notifyIfEmergencyContact(final GenericPdu parsedPdu) { if (parsedPdu != null && parsedPdu.getMessageType() == PduHeaders.MESSAGE_TYPE_SEND_REQ) { SendReq sendReq = (SendReq) parsedPdu; for (EncodedStringValue encodedStringValue : sendReq.getTo()) { if (isEmergencyNumber(encodedStringValue.getString())) { LogUtil.i(getRequestId(), "Notifying emergency contact. " + MmsService.formatCrossStackMessageId(mMessageId)); new AsyncTask() { @Override protected Void doInBackground(Void... voids) { try { BlockedNumberContract.SystemContract .notifyEmergencyContact(mContext); } catch (Exception e) { LogUtil.e(getRequestId(), "Exception notifying emergency contact. " + MmsService.formatCrossStackMessageId(mMessageId) + e); } return null; } }.execute(); return; } } } } private boolean isEmergencyNumber(String address) { if (!TextUtils.isEmpty(address)) { TelephonyManager telephonyManager = ((TelephonyManager) mContext .getSystemService(Context.TELEPHONY_SERVICE)).createForSubscriptionId(mSubId); return telephonyManager.isEmergencyNumber(address); } return false; } @Override protected PendingIntent getPendingIntent() { return mSentIntent; } @Override protected int getQueueType() { return MmsService.QUEUE_INDEX_SEND; } @Override protected Uri persistIfRequired(Context context, int result, byte[] response) { final String requestId = getRequestId(); SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class); UserHandle userHandle = null; long identity = Binder.clearCallingIdentity(); try { if ((subManager != null) && (subManager.isActiveSubscriptionId(mSubId))) { userHandle = subManager.getSubscriptionUserHandle(mSubId); } } finally { Binder.restoreCallingIdentity(identity); } if (!SmsApplication.shouldWriteMessageForPackageAsUser(mCreator, context, userHandle)) { return null; } LogUtil.d(requestId, "persistIfRequired. " + MmsService.formatCrossStackMessageId(mMessageId)); if (mPduData == null) { LogUtil.e(requestId, "persistIfRequired: empty PDU. " + MmsService.formatCrossStackMessageId(mMessageId)); return null; } identity = Binder.clearCallingIdentity(); try { final boolean supportContentDisposition = mMmsConfig.getBoolean(SmsManager.MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION); // Persist the request PDU first GenericPdu pdu = (new PduParser(mPduData, supportContentDisposition)).parse(); if (pdu == null) { LogUtil.e(requestId, "persistIfRequired: can't parse input PDU. " + MmsService.formatCrossStackMessageId(mMessageId)); return null; } if (!(pdu instanceof SendReq)) { LogUtil.d(requestId, "persistIfRequired: not SendReq. " + MmsService.formatCrossStackMessageId(mMessageId)); return null; } final PduPersister persister = PduPersister.getPduPersister(context); final Uri messageUri = persister.persist( pdu, Telephony.Mms.Sent.CONTENT_URI, true/*createThreadId*/, true/*groupMmsEnabled*/, null/*preOpenedFiles*/); if (messageUri == null) { LogUtil.e(requestId, "persistIfRequired: can not persist message. " + MmsService.formatCrossStackMessageId(mMessageId)); return null; } // Update the additional columns based on the send result final ContentValues values = new ContentValues(); SendConf sendConf = null; if (response != null && response.length > 0) { pdu = (new PduParser(response, supportContentDisposition)).parse(); if (pdu != null && pdu instanceof SendConf) { sendConf = (SendConf) pdu; } } if (result != Activity.RESULT_OK || sendConf == null || sendConf.getResponseStatus() != PduHeaders.RESPONSE_STATUS_OK) { // Since we can't persist a message directly into FAILED box, // we have to update the column after we persist it into SENT box. // The gap between the state change is tiny so I would not expect // it to cause any serious problem // TODO: we should add a "failed" URI for this in MmsProvider? values.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_FAILED); } if (sendConf != null) { values.put(Telephony.Mms.RESPONSE_STATUS, sendConf.getResponseStatus()); byte[] messageId = sendConf.getMessageId(); if (messageId != null) { values.put(Telephony.Mms.MESSAGE_ID, PduPersister.toIsoString(messageId)); } } values.put(Telephony.Mms.DATE, System.currentTimeMillis() / 1000L); values.put(Telephony.Mms.READ, 1); values.put(Telephony.Mms.SEEN, 1); if (!TextUtils.isEmpty(mCreator)) { values.put(Telephony.Mms.CREATOR, mCreator); } values.put(Telephony.Mms.SUBSCRIPTION_ID, mSubId); if (SqliteWrapper.update(context, context.getContentResolver(), messageUri, values, null/*where*/, null/*selectionArg*/) != 1) { LogUtil.e(requestId, "persistIfRequired: failed to update message. " + MmsService.formatCrossStackMessageId(mMessageId)); } return messageUri; } catch (MmsException e) { LogUtil.e(requestId, "persistIfRequired: can not persist message. " + MmsService.formatCrossStackMessageId(mMessageId), e); } catch (RuntimeException e) { LogUtil.e(requestId, "persistIfRequired: unexpected parsing failure. " + MmsService.formatCrossStackMessageId(mMessageId), e); } finally { Binder.restoreCallingIdentity(identity); } return null; } /** * Update the destination Address of MO MMS before sending. * This is special for VZW requirement. Follow the specificaitons of assisted dialing * of MO MMS while traveling on VZW CDMA, international CDMA or GSM markets. */ private void updateDestinationAddress(final GenericPdu pdu) { final String requestId = getRequestId(); if (pdu == null) { LogUtil.e(requestId, "updateDestinationAddress: can't parse input PDU. " + MmsService.formatCrossStackMessageId(mMessageId)); return ; } if (!(pdu instanceof SendReq)) { LogUtil.i(requestId, "updateDestinationAddress: not SendReq. " + MmsService.formatCrossStackMessageId(mMessageId)); return; } boolean isUpdated = updateDestinationAddressPerType((SendReq)pdu, PduHeaders.TO); isUpdated = updateDestinationAddressPerType((SendReq)pdu, PduHeaders.CC) || isUpdated; isUpdated = updateDestinationAddressPerType((SendReq)pdu, PduHeaders.BCC) || isUpdated; if (isUpdated) { mPduData = new PduComposer(mContext, (SendReq)pdu).make(); } } private boolean updateDestinationAddressPerType(SendReq pdu, int type) { boolean isUpdated = false; EncodedStringValue[] recipientNumbers = null; switch (type) { case PduHeaders.TO: recipientNumbers = pdu.getTo(); break; case PduHeaders.CC: recipientNumbers = pdu.getCc(); break; case PduHeaders.BCC: recipientNumbers = pdu.getBcc(); break; default: return false; } if (recipientNumbers != null) { int nNumberCount = recipientNumbers.length; if (nNumberCount > 0) { EncodedStringValue[] newNumbers = new EncodedStringValue[nNumberCount]; String toNumber; String newToNumber; for (int i = 0; i < nNumberCount; i++) { toNumber = recipientNumbers[i].getString(); newToNumber = SmsNumberUtils.filterDestAddr(mContext, mSubId, toNumber); if (!TextUtils.equals(toNumber, newToNumber)) { isUpdated = true; newNumbers[i] = new EncodedStringValue(newToNumber); } else { newNumbers[i] = recipientNumbers[i]; } } switch (type) { case PduHeaders.TO: pdu.setTo(newNumbers); break; case PduHeaders.CC: pdu.setCc(newNumbers); break; case PduHeaders.BCC: pdu.setBcc(newNumbers); break; } } } return isUpdated; } /** * Read the pdu from the file descriptor and cache pdu bytes in request * @return true if pdu read successfully */ private boolean readPduFromContentUri() { if (mPduData != null) { return true; } final int bytesTobeRead = mMmsConfig.getInt(SmsManager.MMS_CONFIG_MAX_MESSAGE_SIZE); mPduData = mRequestManager.readPduFromContentUri(mPduUri, bytesTobeRead); return (mPduData != null); } /** * Transfer the received response to the caller (for send requests the pdu is small and can * just include bytes as extra in the "returned" intent). * * @param fillIn the intent that will be returned to the caller * @param response the pdu to transfer */ @Override protected boolean transferResponse(Intent fillIn, byte[] response) { // SendConf pdus are always small and can be included in the intent if (response != null) { fillIn.putExtra(SmsManager.EXTRA_MMS_DATA, response); } return true; } /** * Read the data from the file descriptor if not yet done * @return whether data successfully read */ @Override protected boolean prepareForHttpRequest() { return readPduFromContentUri(); } /** * Try sending via the carrier app * * @param context the context * @param carrierMessagingServicePackage the carrier messaging service sending the MMS */ public void trySendingByCarrierApp(Context context, String carrierMessagingServicePackage) { final CarrierSendManager carrierSendManger = new CarrierSendManager(); final CarrierSendCompleteCallback sendCallback = new CarrierSendCompleteCallback( context, carrierSendManger); carrierSendManger.sendMms(context, carrierMessagingServicePackage, sendCallback); } @Override protected void revokeUriPermission(Context context) { if (mPduUri != null) { context.revokeUriPermission(mPduUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } } /** * Sends the MMS through through the carrier app. */ private final class CarrierSendManager { // Initialized in sendMms private volatile CarrierSendCompleteCallback mCarrierSendCompleteCallback; private final CarrierMessagingServiceWrapper mCarrierMessagingServiceWrapper = new CarrierMessagingServiceWrapper(); void disposeConnection(Context context) { mCarrierMessagingServiceWrapper.disconnect(); } void sendMms(Context context, String carrierMessagingServicePackage, CarrierSendCompleteCallback carrierSendCompleteCallback) { mCarrierSendCompleteCallback = carrierSendCompleteCallback; if (mCarrierMessagingServiceWrapper.bindToCarrierMessagingService( context, carrierMessagingServicePackage, Runnable::run, () -> onServiceReady())) { LogUtil.v("bindService() for carrier messaging service: " + carrierMessagingServicePackage + " succeeded. " + MmsService.formatCrossStackMessageId(mMessageId)); } else { LogUtil.e("bindService() for carrier messaging service: " + carrierMessagingServicePackage + " failed. " + MmsService.formatCrossStackMessageId(mMessageId)); carrierSendCompleteCallback.onSendMmsComplete( CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK, null /* no sendConfPdu */); } } private void onServiceReady() { try { Uri locationUri = null; if (mLocationUrl != null) { locationUri = Uri.parse(mLocationUrl); } mCarrierMessagingServiceWrapper.sendMms( mPduUri, mSubId, locationUri, Runnable::run, mCarrierSendCompleteCallback); } catch (RuntimeException e) { LogUtil.e("Exception sending MMS using the carrier messaging service. " + MmsService.formatCrossStackMessageId(mMessageId) + e, e); mCarrierSendCompleteCallback.onSendMmsComplete( CarrierMessagingService.SEND_STATUS_RETRY_ON_CARRIER_NETWORK, null /* no sendConfPdu */); } } } /** * A callback which notifies carrier messaging app send result. Once the result is ready, the * carrier messaging service connection is disposed. */ private final class CarrierSendCompleteCallback extends MmsRequest.CarrierMmsActionCallback { private final Context mContext; private final CarrierSendManager mCarrierSendManager; public CarrierSendCompleteCallback(Context context, CarrierSendManager carrierSendManager) { mContext = context; mCarrierSendManager = carrierSendManager; } @Override public void onSendMmsComplete(int result, byte[] sendConfPdu) { LogUtil.d("Carrier app result for sending " + MmsService.formatCrossStackMessageId(mMessageId) + ": " + result); mCarrierSendManager.disposeConnection(mContext); if (!maybeFallbackToRegularDelivery(result)) { processResult(mContext, toSmsManagerResult(result), sendConfPdu, 0/* httpStatusCode */, /* handledByCarrierApp= */ true); } } @Override public void onDownloadMmsComplete(int result) { LogUtil.e("Unexpected onDownloadMmsComplete call for " + MmsService.formatCrossStackMessageId(mMessageId) + " with result: " + result); } } protected long getPayloadSize() { if (mPduData == null) { return 0; } return mPduData.length; } }