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
17package com.android.messaging.sms;
18
19import android.app.Activity;
20import android.app.PendingIntent;
21import android.content.Context;
22import android.content.Intent;
23import android.net.Uri;
24import android.os.SystemClock;
25import android.telephony.PhoneNumberUtils;
26import android.telephony.SmsManager;
27import android.text.TextUtils;
28
29import com.android.messaging.Factory;
30import com.android.messaging.R;
31import com.android.messaging.receiver.SendStatusReceiver;
32import com.android.messaging.util.Assert;
33import com.android.messaging.util.BugleGservices;
34import com.android.messaging.util.BugleGservicesKeys;
35import com.android.messaging.util.LogUtil;
36import com.android.messaging.util.PhoneUtils;
37import com.android.messaging.util.UiUtils;
38
39import java.util.ArrayList;
40import java.util.Random;
41import 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 */
54public 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