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