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 SendResult(final int numOfParts)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 setPartResult(final int resultCode)93 public void setPartResult(final int resultCode) { 94 mPendingParts--; 95 setHighestFailureLevel(resultCode); 96 } 97 hasPending()98 public boolean hasPending() { 99 return mPendingParts > 0; 100 } 101 getHighestFailureLevel()102 public int getHighestFailureLevel() { 103 return mHighestFailureLevel; 104 } 105 getFailureLevel(final int resultCode)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 setHighestFailureLevel(final int resultCode)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 toString()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 setResult(final Uri requestId, final int resultCode, final int errorCode, final int partId, int subId)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 getSendErrorToastMessage(final Context context, final int subId, final int errorCode)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 sendMessage(final Context context, final int subId, String dest, String message, final String serviceCenter, final boolean requireDeliveryReport, final Uri messageUri)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 sendInternal(final Context context, final int subId, String dest, final ArrayList<String> messages, final String serviceCenter, final boolean requireDeliveryReport, final Uri messageUri)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 getSendStatusIntent(final Context context, final String action, final Uri requestUri, final int partId, final int subId)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