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