/* * Copyright (C) 2015 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.messaging.sms; import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.SystemClock; import android.telephony.PhoneNumberUtils; import android.telephony.SmsManager; import android.text.TextUtils; import com.android.messaging.Factory; import com.android.messaging.R; import com.android.messaging.receiver.SendStatusReceiver; import com.android.messaging.util.Assert; import com.android.messaging.util.BugleGservices; import com.android.messaging.util.BugleGservicesKeys; import com.android.messaging.util.LogUtil; import com.android.messaging.util.PhoneUtils; import com.android.messaging.util.UiUtils; import java.util.ArrayList; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; /** * Class that sends chat message via SMS. * * The interface emulates a blocking sending similar to making an HTTP request. * It calls the SmsManager to send a (potentially multipart) message and waits * on the sent status on each part. The waiting has a timeout so it won't wait * forever. Once the sent status of all parts received, the call returns. * A successful sending requires success status for all parts. Otherwise, we * pick the highest level of failure as the error for the whole message, which * is used to determine if we need to retry the sending. */ public class SmsSender { private static final String TAG = LogUtil.BUGLE_TAG; public static final String EXTRA_PART_ID = "part_id"; /* * A map for pending sms messages. The key is the random request UUID. */ private static ConcurrentHashMap sPendingMessageMap = new ConcurrentHashMap(); private static final Random RANDOM = new Random(); // Whether we should send multipart SMS as separate messages private static Boolean sSendMultipartSmsAsSeparateMessages = null; /** * Class that holds the sent status for all parts of a multipart message sending */ public static class SendResult { // Failure levels, used by the caller of the sender. // For temporary failures, possibly we could retry the sending // For permanent failures, we probably won't retry public static final int FAILURE_LEVEL_NONE = 0; public static final int FAILURE_LEVEL_TEMPORARY = 1; public static final int FAILURE_LEVEL_PERMANENT = 2; // Tracking the remaining pending parts in sending private int mPendingParts; // Tracking the highest level of failure among all parts private int mHighestFailureLevel; public SendResult(final int numOfParts) { Assert.isTrue(numOfParts > 0); mPendingParts = numOfParts; mHighestFailureLevel = FAILURE_LEVEL_NONE; } // Update the sent status of one part public void setPartResult(final int resultCode) { mPendingParts--; setHighestFailureLevel(resultCode); } public boolean hasPending() { return mPendingParts > 0; } public int getHighestFailureLevel() { return mHighestFailureLevel; } private int getFailureLevel(final int resultCode) { switch (resultCode) { case Activity.RESULT_OK: return FAILURE_LEVEL_NONE; case SmsManager.RESULT_ERROR_NO_SERVICE: return FAILURE_LEVEL_TEMPORARY; case SmsManager.RESULT_ERROR_RADIO_OFF: return FAILURE_LEVEL_PERMANENT; case SmsManager.RESULT_ERROR_GENERIC_FAILURE: return FAILURE_LEVEL_PERMANENT; default: { LogUtil.e(TAG, "SmsSender: Unexpected sent intent resultCode = " + resultCode); return FAILURE_LEVEL_PERMANENT; } } } private void setHighestFailureLevel(final int resultCode) { final int level = getFailureLevel(resultCode); if (level > mHighestFailureLevel) { mHighestFailureLevel = level; } } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("SendResult:"); sb.append("Pending=").append(mPendingParts).append(","); sb.append("HighestFailureLevel=").append(mHighestFailureLevel); return sb.toString(); } } public static void setResult(final Uri requestId, final int resultCode, final int errorCode, final int partId, int subId) { if (resultCode != Activity.RESULT_OK) { LogUtil.e(TAG, "SmsSender: failure in sending message part. " + " requestId=" + requestId + " partId=" + partId + " resultCode=" + resultCode + " errorCode=" + errorCode); if (errorCode != SendStatusReceiver.NO_ERROR_CODE) { final Context context = Factory.get().getApplicationContext(); UiUtils.showToastAtBottom(getSendErrorToastMessage(context, subId, errorCode)); } } else { if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { LogUtil.v(TAG, "SmsSender: received sent result. " + " requestId=" + requestId + " partId=" + partId + " resultCode=" + resultCode); } } if (requestId != null) { final SendResult result = sPendingMessageMap.get(requestId); if (result != null) { synchronized (result) { result.setPartResult(resultCode); if (!result.hasPending()) { result.notifyAll(); } } } else { LogUtil.e(TAG, "SmsSender: ignoring sent result. " + " requestId=" + requestId + " partId=" + partId + " resultCode=" + resultCode); } } } private static String getSendErrorToastMessage(final Context context, final int subId, final int errorCode) { final String carrierName = PhoneUtils.get(subId).getCarrierName(); if (TextUtils.isEmpty(carrierName)) { return context.getString(R.string.carrier_send_error_unknown_carrier, errorCode); } else { return context.getString(R.string.carrier_send_error, carrierName, errorCode); } } // This should be called from a RequestWriter queue thread public static SendResult sendMessage(final Context context, final int subId, String dest, String message, final String serviceCenter, final boolean requireDeliveryReport, final Uri messageUri) throws SmsException { if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { LogUtil.v(TAG, "SmsSender: sending message. " + "dest=" + dest + " message=" + message + " serviceCenter=" + serviceCenter + " requireDeliveryReport=" + requireDeliveryReport + " requestId=" + messageUri); } if (TextUtils.isEmpty(message)) { throw new SmsException("SmsSender: empty text message"); } // Get the real dest and message for email or alias if dest is email or alias // Or sanitize the dest if dest is a number if (!TextUtils.isEmpty(MmsConfig.get(subId).getEmailGateway()) && (MmsSmsUtils.isEmailAddress(dest) || MmsSmsUtils.isAlias(dest, subId))) { // The original destination (email address) goes with the message message = dest + " " + message; // the new address is the email gateway # dest = MmsConfig.get(subId).getEmailGateway(); } else { // remove spaces and dashes from destination number // (e.g. "801 555 1212" -> "8015551212") // (e.g. "+8211-123-4567" -> "+82111234567") dest = PhoneNumberUtils.stripSeparators(dest); } if (TextUtils.isEmpty(dest)) { throw new SmsException("SmsSender: empty destination address"); } // Divide the input message by SMS length limit final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager(); final ArrayList messages = smsManager.divideMessage(message); if (messages == null || messages.size() < 1) { throw new SmsException("SmsSender: fails to divide message"); } // Prepare the send result, which collects the send status for each part final SendResult pendingResult = new SendResult(messages.size()); sPendingMessageMap.put(messageUri, pendingResult); // Actually send the sms sendInternal( context, subId, dest, messages, serviceCenter, requireDeliveryReport, messageUri); // Wait for pending intent to come back synchronized (pendingResult) { final long smsSendTimeoutInMillis = BugleGservices.get().getLong( BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS, BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS_DEFAULT); final long beginTime = SystemClock.elapsedRealtime(); long waitTime = smsSendTimeoutInMillis; // We could possibly be woken up while still pending // so make sure we wait the full timeout period unless // we have the send results of all parts. while (pendingResult.hasPending() && waitTime > 0) { try { pendingResult.wait(waitTime); } catch (final InterruptedException e) { LogUtil.e(TAG, "SmsSender: sending wait interrupted"); } waitTime = smsSendTimeoutInMillis - (SystemClock.elapsedRealtime() - beginTime); } } // Either we timed out or have all the results (success or failure) sPendingMessageMap.remove(messageUri); if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { LogUtil.v(TAG, "SmsSender: sending completed. " + "dest=" + dest + " message=" + message + " result=" + pendingResult); } return pendingResult; } // Actually sending the message using SmsManager private static void sendInternal(final Context context, final int subId, String dest, final ArrayList messages, final String serviceCenter, final boolean requireDeliveryReport, final Uri messageUri) throws SmsException { Assert.notNull(context); final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager(); final int messageCount = messages.size(); final ArrayList deliveryIntents = new ArrayList(messageCount); final ArrayList sentIntents = new ArrayList(messageCount); for (int i = 0; i < messageCount; i++) { // Make pending intents different for each message part final int partId = (messageCount <= 1 ? 0 : i + 1); if (requireDeliveryReport && (i == (messageCount - 1))) { // TODO we only care about the delivery status of the last part // Shall we have better tracking of delivery status of all parts? deliveryIntents.add(PendingIntent.getBroadcast( context, partId, getSendStatusIntent(context, SendStatusReceiver.MESSAGE_DELIVERED_ACTION, messageUri, partId, subId), 0/*flag*/)); } else { deliveryIntents.add(null); } sentIntents.add(PendingIntent.getBroadcast( context, partId, getSendStatusIntent(context, SendStatusReceiver.MESSAGE_SENT_ACTION, messageUri, partId, subId), 0/*flag*/)); } if (sSendMultipartSmsAsSeparateMessages == null) { sSendMultipartSmsAsSeparateMessages = MmsConfig.get(subId) .getSendMultipartSmsAsSeparateMessages(); } try { if (sSendMultipartSmsAsSeparateMessages) { // If multipart sms is not supported, send them as separate messages for (int i = 0; i < messageCount; i++) { smsManager.sendTextMessage(dest, serviceCenter, messages.get(i), sentIntents.get(i), deliveryIntents.get(i)); } } else { smsManager.sendMultipartTextMessage( dest, serviceCenter, messages, sentIntents, deliveryIntents); } } catch (final Exception e) { throw new SmsException("SmsSender: caught exception in sending " + e); } } private static Intent getSendStatusIntent(final Context context, final String action, final Uri requestUri, final int partId, final int subId) { // Encode requestId in intent data final Intent intent = new Intent(action, requestUri, context, SendStatusReceiver.class); intent.putExtra(SendStatusReceiver.EXTRA_PART_ID, partId); intent.putExtra(SendStatusReceiver.EXTRA_SUB_ID, subId); return intent; } }