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