diff options
author | Mike Dodd <mdodd@google.com> | 2015-08-11 11:16:59 -0700 |
---|---|---|
committer | Mike Dodd <mdodd@google.com> | 2015-08-12 12:47:26 -0700 |
commit | d3b009ae55651f1e60950342468e3c37fdeb0796 (patch) | |
tree | bc4b489af52d0e2521e21167d2ad76a47256f348 /src/com/android/messaging/sms/SmsSender.java | |
parent | ef8c7abbcfc9c770385d6609a4b4bc70240ebdc4 (diff) | |
download | android_packages_apps_Messaging-d3b009ae55651f1e60950342468e3c37fdeb0796.tar.gz android_packages_apps_Messaging-d3b009ae55651f1e60950342468e3c37fdeb0796.tar.bz2 android_packages_apps_Messaging-d3b009ae55651f1e60950342468e3c37fdeb0796.zip |
Initial checkin of AOSP Messaging app.
b/23110861
Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
Diffstat (limited to 'src/com/android/messaging/sms/SmsSender.java')
-rw-r--r-- | src/com/android/messaging/sms/SmsSender.java | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/src/com/android/messaging/sms/SmsSender.java b/src/com/android/messaging/sms/SmsSender.java new file mode 100644 index 0000000..889973f --- /dev/null +++ b/src/com/android/messaging/sms/SmsSender.java @@ -0,0 +1,315 @@ +/* + * 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<Uri, SendResult> sPendingMessageMap = + new ConcurrentHashMap<Uri, SendResult>(); + + 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<String> 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<String> 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<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(messageCount); + final ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(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; + } +} |