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