Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
This commit is contained in:
315
src/com/android/messaging/sms/SmsSender.java
Normal file
315
src/com/android/messaging/sms/SmsSender.java
Normal file
@@ -0,0 +1,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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user