summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/sms/SmsSender.java
blob: 889973fe40824debafac9566474b1e3e493aad93 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.messaging.sms;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsManager;
import android.text.TextUtils;

import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.receiver.SendStatusReceiver;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.UiUtils;

import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Class that sends chat message via SMS.
 *
 * The interface emulates a blocking sending similar to making an HTTP request.
 * It calls the SmsManager to send a (potentially multipart) message and waits
 * on the sent status on each part. The waiting has a timeout so it won't wait
 * forever. Once the sent status of all parts received, the call returns.
 * A successful sending requires success status for all parts. Otherwise, we
 * pick the highest level of failure as the error for the whole message, which
 * is used to determine if we need to retry the sending.
 */
public class SmsSender {
    private static final String TAG = LogUtil.BUGLE_TAG;

    public static final String EXTRA_PART_ID = "part_id";

    /*
     * A map for pending sms messages. The key is the random request UUID.
     */
    private static ConcurrentHashMap<Uri, SendResult> sPendingMessageMap =
            new ConcurrentHashMap<Uri, SendResult>();

    private static final Random RANDOM = new Random();

    // Whether we should send multipart SMS as separate messages
    private static Boolean sSendMultipartSmsAsSeparateMessages = null;

    /**
     * Class that holds the sent status for all parts of a multipart message sending
     */
    public static class SendResult {
        // Failure levels, used by the caller of the sender.
        // For temporary failures, possibly we could retry the sending
        // For permanent failures, we probably won't retry
        public static final int FAILURE_LEVEL_NONE = 0;
        public static final int FAILURE_LEVEL_TEMPORARY = 1;
        public static final int FAILURE_LEVEL_PERMANENT = 2;

        // Tracking the remaining pending parts in sending
        private int mPendingParts;
        // Tracking the highest level of failure among all parts
        private int mHighestFailureLevel;

        public SendResult(final int numOfParts) {
            Assert.isTrue(numOfParts > 0);
            mPendingParts = numOfParts;
            mHighestFailureLevel = FAILURE_LEVEL_NONE;
        }

        // Update the sent status of one part
        public void setPartResult(final int resultCode) {
            mPendingParts--;
            setHighestFailureLevel(resultCode);
        }

        public boolean hasPending() {
            return mPendingParts > 0;
        }

        public int getHighestFailureLevel() {
            return mHighestFailureLevel;
        }

        private int getFailureLevel(final int resultCode) {
            switch (resultCode) {
                case Activity.RESULT_OK:
                    return FAILURE_LEVEL_NONE;
                case SmsManager.RESULT_ERROR_NO_SERVICE:
                    return FAILURE_LEVEL_TEMPORARY;
                case SmsManager.RESULT_ERROR_RADIO_OFF:
                    return FAILURE_LEVEL_PERMANENT;
                case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
                    return FAILURE_LEVEL_PERMANENT;
                default: {
                    LogUtil.e(TAG, "SmsSender: Unexpected sent intent resultCode = " + resultCode);
                    return FAILURE_LEVEL_PERMANENT;
                }
            }
        }

        private void setHighestFailureLevel(final int resultCode) {
            final int level = getFailureLevel(resultCode);
            if (level > mHighestFailureLevel) {
                mHighestFailureLevel = level;
            }
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder();
            sb.append("SendResult:");
            sb.append("Pending=").append(mPendingParts).append(",");
            sb.append("HighestFailureLevel=").append(mHighestFailureLevel);
            return sb.toString();
        }
    }

    public static void setResult(final Uri requestId, final int resultCode,
            final int errorCode, final int partId, int subId) {
        if (resultCode != Activity.RESULT_OK) {
            LogUtil.e(TAG, "SmsSender: failure in sending message part. "
                    + " requestId=" + requestId + " partId=" + partId
                    + " resultCode=" + resultCode + " errorCode=" + errorCode);
            if (errorCode != SendStatusReceiver.NO_ERROR_CODE) {
                final Context context = Factory.get().getApplicationContext();
                UiUtils.showToastAtBottom(getSendErrorToastMessage(context, subId, errorCode));
            }
        } else {
            if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
                LogUtil.v(TAG, "SmsSender: received sent result. " + " requestId=" + requestId
                        + " partId=" + partId + " resultCode=" + resultCode);
            }
        }
        if (requestId != null) {
            final SendResult result = sPendingMessageMap.get(requestId);
            if (result != null) {
                synchronized (result) {
                    result.setPartResult(resultCode);
                    if (!result.hasPending()) {
                        result.notifyAll();
                    }
                }
            } else {
                LogUtil.e(TAG, "SmsSender: ignoring sent result. " + " requestId=" + requestId
                        + " partId=" + partId + " resultCode=" + resultCode);
            }
        }
    }

    private static String getSendErrorToastMessage(final Context context, final int subId,
            final int errorCode) {
        final String carrierName = PhoneUtils.get(subId).getCarrierName();
        if (TextUtils.isEmpty(carrierName)) {
            return context.getString(R.string.carrier_send_error_unknown_carrier, errorCode);
        } else {
            return context.getString(R.string.carrier_send_error, carrierName, errorCode);
        }
    }

    // This should be called from a RequestWriter queue thread
    public static SendResult sendMessage(final Context context,  final int subId, String dest,
            String message, final String serviceCenter, final boolean requireDeliveryReport,
            final Uri messageUri) throws SmsException {
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "SmsSender: sending message. " +
                    "dest=" + dest + " message=" + message +
                    " serviceCenter=" + serviceCenter +
                    " requireDeliveryReport=" + requireDeliveryReport +
                    " requestId=" + messageUri);
        }
        if (TextUtils.isEmpty(message)) {
            throw new SmsException("SmsSender: empty text message");
        }
        // Get the real dest and message for email or alias if dest is email or alias
        // Or sanitize the dest if dest is a number
        if (!TextUtils.isEmpty(MmsConfig.get(subId).getEmailGateway()) &&
                (MmsSmsUtils.isEmailAddress(dest) || MmsSmsUtils.isAlias(dest, subId))) {
            // The original destination (email address) goes with the message
            message = dest + " " + message;
            // the new address is the email gateway #
            dest = MmsConfig.get(subId).getEmailGateway();
        } else {
            // remove spaces and dashes from destination number
            // (e.g. "801 555 1212" -> "8015551212")
            // (e.g. "+8211-123-4567" -> "+82111234567")
            dest = PhoneNumberUtils.stripSeparators(dest);
        }
        if (TextUtils.isEmpty(dest)) {
            throw new SmsException("SmsSender: empty destination address");
        }
        // Divide the input message by SMS length limit
        final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager();
        final ArrayList<String> messages = smsManager.divideMessage(message);
        if (messages == null || messages.size() < 1) {
            throw new SmsException("SmsSender: fails to divide message");
        }
        // Prepare the send result, which collects the send status for each part
        final SendResult pendingResult = new SendResult(messages.size());
        sPendingMessageMap.put(messageUri, pendingResult);
        // Actually send the sms
        sendInternal(
                context, subId, dest, messages, serviceCenter, requireDeliveryReport, messageUri);
        // Wait for pending intent to come back
        synchronized (pendingResult) {
            final long smsSendTimeoutInMillis = BugleGservices.get().getLong(
                    BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS,
                    BugleGservicesKeys.SMS_SEND_TIMEOUT_IN_MILLIS_DEFAULT);
            final long beginTime = SystemClock.elapsedRealtime();
            long waitTime = smsSendTimeoutInMillis;
            // We could possibly be woken up while still pending
            // so make sure we wait the full timeout period unless
            // we have the send results of all parts.
            while (pendingResult.hasPending() && waitTime > 0) {
                try {
                    pendingResult.wait(waitTime);
                } catch (final InterruptedException e) {
                    LogUtil.e(TAG, "SmsSender: sending wait interrupted");
                }
                waitTime = smsSendTimeoutInMillis - (SystemClock.elapsedRealtime() - beginTime);
            }
        }
        // Either we timed out or have all the results (success or failure)
        sPendingMessageMap.remove(messageUri);
        if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
            LogUtil.v(TAG, "SmsSender: sending completed. " +
                    "dest=" + dest + " message=" + message + " result=" + pendingResult);
        }
        return pendingResult;
    }

    // Actually sending the message using SmsManager
    private static void sendInternal(final Context context, final int subId, String dest,
            final ArrayList<String> messages, final String serviceCenter,
            final boolean requireDeliveryReport, final Uri messageUri) throws SmsException {
        Assert.notNull(context);
        final SmsManager smsManager = PhoneUtils.get(subId).getSmsManager();
        final int messageCount = messages.size();
        final ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(messageCount);
        final ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(messageCount);
        for (int i = 0; i < messageCount; i++) {
            // Make pending intents different for each message part
            final int partId = (messageCount <= 1 ? 0 : i + 1);
            if (requireDeliveryReport && (i == (messageCount - 1))) {
                // TODO we only care about the delivery status of the last part
                // Shall we have better tracking of delivery status of all parts?
                deliveryIntents.add(PendingIntent.getBroadcast(
                        context,
                        partId,
                        getSendStatusIntent(context, SendStatusReceiver.MESSAGE_DELIVERED_ACTION,
                                messageUri, partId, subId),
                        0/*flag*/));
            } else {
                deliveryIntents.add(null);
            }
            sentIntents.add(PendingIntent.getBroadcast(
                    context,
                    partId,
                    getSendStatusIntent(context, SendStatusReceiver.MESSAGE_SENT_ACTION,
                            messageUri, partId, subId),
                    0/*flag*/));
        }
        if (sSendMultipartSmsAsSeparateMessages == null) {
            sSendMultipartSmsAsSeparateMessages = MmsConfig.get(subId)
                    .getSendMultipartSmsAsSeparateMessages();
        }
        try {
            if (sSendMultipartSmsAsSeparateMessages) {
                // If multipart sms is not supported, send them as separate messages
                for (int i = 0; i < messageCount; i++) {
                    smsManager.sendTextMessage(dest,
                            serviceCenter,
                            messages.get(i),
                            sentIntents.get(i),
                            deliveryIntents.get(i));
                }
            } else {
                smsManager.sendMultipartTextMessage(
                        dest, serviceCenter, messages, sentIntents, deliveryIntents);
            }
        } catch (final Exception e) {
            throw new SmsException("SmsSender: caught exception in sending " + e);
        }
    }

    private static Intent getSendStatusIntent(final Context context, final String action,
            final Uri requestUri, final int partId, final int subId) {
        // Encode requestId in intent data
        final Intent intent = new Intent(action, requestUri, context, SendStatusReceiver.class);
        intent.putExtra(SendStatusReceiver.EXTRA_PART_ID, partId);
        intent.putExtra(SendStatusReceiver.EXTRA_SUB_ID, subId);
        return intent;
    }
}