Home | History | Annotate | Download | only in sms
      1 /*
      2  * Copyright (C) 2015 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.messaging.sms;
     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.os.SystemClock;
     25 import android.telephony.PhoneNumberUtils;
     26 import android.telephony.SmsManager;
     27 import android.text.TextUtils;
     28 
     29 import com.android.messaging.Factory;
     30 import com.android.messaging.R;
     31 import com.android.messaging.receiver.SendStatusReceiver;
     32 import com.android.messaging.util.Assert;
     33 import com.android.messaging.util.BugleGservices;
     34 import com.android.messaging.util.BugleGservicesKeys;
     35 import com.android.messaging.util.LogUtil;
     36 import com.android.messaging.util.PhoneUtils;
     37 import com.android.messaging.util.UiUtils;
     38 
     39 import java.util.ArrayList;
     40 import java.util.Random;
     41 import java.util.concurrent.ConcurrentHashMap;
     42 
     43 /**
     44  * Class that sends chat message via SMS.
     45  *
     46  * The interface emulates a blocking sending similar to making an HTTP request.
     47  * It calls the SmsManager to send a (potentially multipart) message and waits
     48  * on the sent status on each part. The waiting has a timeout so it won't wait
     49  * forever. Once the sent status of all parts received, the call returns.
     50  * A successful sending requires success status for all parts. Otherwise, we
     51  * pick the highest level of failure as the error for the whole message, which
     52  * is used to determine if we need to retry the sending.
     53  */
     54 public class SmsSender {
     55     private static final String TAG = LogUtil.BUGLE_TAG;
     56 
     57     public static final String EXTRA_PART_ID = "part_id";
     58 
     59     /*
     60      * A map for pending sms messages. The key is the random request UUID.
     61      */
     62     private static ConcurrentHashMap<Uri, SendResult> sPendingMessageMap =
     63             new ConcurrentHashMap<Uri, SendResult>();
     64 
     65     private static final Random RANDOM = new Random();
     66 
     67     // Whether we should send multipart SMS as separate messages
     68     private static Boolean sSendMultipartSmsAsSeparateMessages = null;
     69 
     70     /**
     71      * Class that holds the sent status for all parts of a multipart message sending
     72      */
     73     public static class SendResult {
     74         // Failure levels, used by the caller of the sender.
     75         // For temporary failures, possibly we could retry the sending
     76         // For permanent failures, we probably won't retry
     77         public static final int FAILURE_LEVEL_NONE = 0;
     78         public static final int FAILURE_LEVEL_TEMPORARY = 1;
     79         public static final int FAILURE_LEVEL_PERMANENT = 2;
     80 
     81         // Tracking the remaining pending parts in sending
     82         private int mPendingParts;
     83         // Tracking the highest level of failure among all parts
     84         private int mHighestFailureLevel;
     85 
     86         public SendResult(final int numOfParts) {
     87             Assert.isTrue(numOfParts > 0);
     88             mPendingParts = numOfParts;
     89             mHighestFailureLevel = FAILURE_LEVEL_NONE;
     90         }
     91 
     92         // Update the sent status of one part
     93         public void setPartResult(final int resultCode) {
     94             mPendingParts--;
     95             setHighestFailureLevel(resultCode);
     96         }
     97 
     98         public boolean hasPending() {
     99             return mPendingParts > 0;
    100         }
    101 
    102         public int getHighestFailureLevel() {
    103             return mHighestFailureLevel;
    104         }
    105 
    106         private int getFailureLevel(final int resultCode) {
    107             switch (resultCode) {
    108                 case Activity.RESULT_OK:
    109                     return FAILURE_LEVEL_NONE;
    110                 case SmsManager.RESULT_ERROR_NO_SERVICE:
    111                     return FAILURE_LEVEL_TEMPORARY;
    112                 case SmsManager.RESULT_ERROR_RADIO_OFF:
    113                     return FAILURE_LEVEL_PERMANENT;
    114                 case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
    115                     return FAILURE_LEVEL_PERMANENT;
    116                 default: {
    117                     LogUtil.e(TAG, "SmsSender: Unexpected sent intent resultCode = " + resultCode);
    118                     return FAILURE_LEVEL_PERMANENT;
    119                 }
    120             }
    121         }
    122 
    123         private void setHighestFailureLevel(final int resultCode) {
    124             final int level = getFailureLevel(resultCode);
    125             if (level > mHighestFailureLevel) {
    126                 mHighestFailureLevel = level;
    127             }
    128         }
    129 
    130         @Override
    131         public String toString() {
    132             final StringBuilder sb = new StringBuilder();
    133             sb.append("SendResult:");
    134             sb.append("Pending=").append(mPendingParts).append(",");
    135             sb.append("HighestFailureLevel=").append(mHighestFailureLevel);
    136             return sb.toString();
    137         }
    138     }
    139 
    140     public static void setResult(final Uri requestId, final int resultCode,
    141             final int errorCode, final int partId, int subId) {
    142         if (resultCode != Activity.RESULT_OK) {
    143             LogUtil.e(TAG, "SmsSender: failure in sending message part. "
    144                     + " requestId=" + requestId + " partId=" + partId
    145                     + " resultCode=" + resultCode + " errorCode=" + errorCode);
    146             if (errorCode != SendStatusReceiver.NO_ERROR_CODE) {
    147                 final Context context = Factory.get().getApplicationContext();
    148                 UiUtils.showToastAtBottom(getSendErrorToastMessage(context, subId, errorCode));
    149             }
    150         } else {
    151             if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    152                 LogUtil.v(TAG, "SmsSender: received sent result. " + " requestId=" + requestId
    153                         + " partId=" + partId + " resultCode=" + resultCode);
    154             }
    155         }
    156         if (requestId != null) {
    157             final SendResult result = sPendingMessageMap.get(requestId);
    158             if (result != null) {
    159                 synchronized (result) {
    160                     result.setPartResult(resultCode);
    161                     if (!result.hasPending()) {
    162                         result.notifyAll();
    163                     }
    164                 }
    165             } else {
    166                 LogUtil.e(TAG, "SmsSender: ignoring sent result. " + " requestId=" + requestId
    167                         + " partId=" + partId + " resultCode=" + resultCode);
    168             }
    169         }
    170     }
    171 
    172     private static String getSendErrorToastMessage(final Context context, final int subId,
    173             final int errorCode) {
    174         final String carrierName = PhoneUtils.get(subId).getCarrierName();
    175         if (TextUtils.isEmpty(carrierName)) {
    176             return context.getString(R.string.carrier_send_error_unknown_carrier, errorCode);
    177         } else {
    178             return context.getString(R.string.carrier_send_error, carrierName, errorCode);
    179         }
    180     }
    181 
    182     // This should be called from a RequestWriter queue thread
    183     public static SendResult sendMessage(final Context context,  final int subId, String dest,
    184             String message, final String serviceCenter, final boolean requireDeliveryReport,
    185             final Uri messageUri) throws SmsException {
    186         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    187             LogUtil.v(TAG, "SmsSender: sending message. " +
    188                     "dest=" + dest + " message=" + message +
    189                     " serviceCenter=" + serviceCenter +
    190                     " requireDeliveryReport=" + requireDeliveryReport +
    191                     " requestId=" + messageUri);
    192         }
    193         if (TextUtils.isEmpty(message)) {
    194             throw new SmsException("SmsSender: empty text message");
    195         }
    196         // Get the real dest and message for email or alias if dest is email or alias
    197         // Or sanitize the dest if dest is a number
    198         if (!TextUtils.isEmpty(MmsConfig.get(subId).getEmailGateway()) &&
    199                 (MmsSmsUtils.isEmailAddress(dest) || MmsSmsUtils.isAlias(dest, subId))) {
    200             // The original destination (email address) goes with the message
    201             message = dest + " " + message;
    202             // the new address is the email gateway #
    203             dest = MmsConfig.get(subId).getEmailGateway();
    204         } else {
    205             // remove spaces and dashes from destination number
    206             // (e.g. "801 555 1212" -> "8015551212")
    207             // (e.g. "+8211-123-4567" -> "+82111234567")
    208             dest = PhoneNumberUtils.stripSeparators(dest);
    209         }
    210         if (TextUtils.isEmpty(dest)) {
    211             throw new SmsException("SmsSender: empty destination address");
    212         }
    213         // Divide the input message by SMS length limit
    214         final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager();
    215         final ArrayList<String> messages = smsManager.divideMessage(message);
    216         if (messages == null || messages.size() < 1) {
    217             throw new SmsException("SmsSender: fails to divide message");
    218         }
    219         // Prepare the send result, which collects the send status for each part
    220         final SendResult pendingResult = new SendResult(messages.size());
    221         sPendingMessageMap.put(messageUri, pendingResult);
    222         // Actually send the sms
    223         sendInternal(
    224                 context, subId, dest, messages, serviceCenter, requireDeliveryReport, messageUri);
    225         // Wait for pending intent to come back
    226         synchronized (pendingResult) {
    227             final long smsSendTimeoutInMillis = BugleGservices.get().getLong(
    228                     BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS,
    229                     BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS_DEFAULT);
    230             final long beginTime = SystemClock.elapsedRealtime();
    231             long waitTime = smsSendTimeoutInMillis;
    232             // We could possibly be woken up while still pending
    233             // so make sure we wait the full timeout period unless
    234             // we have the send results of all parts.
    235             while (pendingResult.hasPending() && waitTime > 0) {
    236                 try {
    237                     pendingResult.wait(waitTime);
    238                 } catch (final InterruptedException e) {
    239                     LogUtil.e(TAG, "SmsSender: sending wait interrupted");
    240                 }
    241                 waitTime = smsSendTimeoutInMillis - (SystemClock.elapsedRealtime() - beginTime);
    242             }
    243         }
    244         // Either we timed out or have all the results (success or failure)
    245         sPendingMessageMap.remove(messageUri);
    246         if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
    247             LogUtil.v(TAG, "SmsSender: sending completed. " +
    248                     "dest=" + dest + " message=" + message + " result=" + pendingResult);
    249         }
    250         return pendingResult;
    251     }
    252 
    253     // Actually sending the message using SmsManager
    254     private static void sendInternal(final Context context, final int subId, String dest,
    255             final ArrayList<String> messages, final String serviceCenter,
    256             final boolean requireDeliveryReport, final Uri messageUri) throws SmsException {
    257         Assert.notNull(context);
    258         final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager();
    259         final int messageCount = messages.size();
    260         final ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(messageCount);
    261         final ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(messageCount);
    262         for (int i = 0; i < messageCount; i++) {
    263             // Make pending intents different for each message part
    264             final int partId = (messageCount <= 1 ? 0 : i + 1);
    265             if (requireDeliveryReport && (i == (messageCount - 1))) {
    266                 // TODO we only care about the delivery status of the last part
    267                 // Shall we have better tracking of delivery status of all parts?
    268                 deliveryIntents.add(PendingIntent.getBroadcast(
    269                         context,
    270                         partId,
    271                         getSendStatusIntent(context, SendStatusReceiver.MESSAGE_DELIVERED_ACTION,
    272                                 messageUri, partId, subId),
    273                         0/*flag*/));
    274             } else {
    275                 deliveryIntents.add(null);
    276             }
    277             sentIntents.add(PendingIntent.getBroadcast(
    278                     context,
    279                     partId,
    280                     getSendStatusIntent(context, SendStatusReceiver.MESSAGE_SENT_ACTION,
    281                             messageUri, partId, subId),
    282                     0/*flag*/));
    283         }
    284         if (sSendMultipartSmsAsSeparateMessages == null) {
    285             sSendMultipartSmsAsSeparateMessages = MmsConfig.get(subId)
    286                     .getSendMultipartSmsAsSeparateMessages();
    287         }
    288         try {
    289             if (sSendMultipartSmsAsSeparateMessages) {
    290                 // If multipart sms is not supported, send them as separate messages
    291                 for (int i = 0; i < messageCount; i++) {
    292                     smsManager.sendTextMessage(dest,
    293                             serviceCenter,
    294                             messages.get(i),
    295                             sentIntents.get(i),
    296                             deliveryIntents.get(i));
    297                 }
    298             } else {
    299                 smsManager.sendMultipartTextMessage(
    300                         dest, serviceCenter, messages, sentIntents, deliveryIntents);
    301             }
    302         } catch (final Exception e) {
    303             throw new SmsException("SmsSender: caught exception in sending " + e);
    304         }
    305     }
    306 
    307     private static Intent getSendStatusIntent(final Context context, final String action,
    308             final Uri requestUri, final int partId, final int subId) {
    309         // Encode requestId in intent data
    310         final Intent intent = new Intent(action, requestUri, context, SendStatusReceiver.class);
    311         intent.putExtra(SendStatusReceiver.EXTRA_PART_ID, partId);
    312         intent.putExtra(SendStatusReceiver.EXTRA_SUB_ID, subId);
    313         return intent;
    314     }
    315 }
    316