blob: 362a1b732847262f54ca883aa8e663e0e0eceec7 [file] [log] [blame]
/*
* Copyright (C) 2017 Google Inc.
*
* 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.google.android.mobly.snippet.bundled;
import static java.util.stream.Collectors.toCollection;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.provider.Telephony.Sms.Intents;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.mobly.snippet.Snippet;
import com.google.android.mobly.snippet.bundled.utils.Utils;
import com.google.android.mobly.snippet.event.EventCache;
import com.google.android.mobly.snippet.event.SnippetEvent;
import com.google.android.mobly.snippet.rpc.AsyncRpc;
import com.google.android.mobly.snippet.rpc.Rpc;
import java.util.ArrayList;
import java.util.stream.IntStream;
import org.json.JSONObject;
/** Snippet class for SMS RPCs. */
public class SmsSnippet implements Snippet {
private static class SmsSnippetException extends Exception {
private static final long serialVersionUID = 1L;
SmsSnippetException(String msg) {
super(msg);
}
}
private static final int MAX_CHAR_COUNT_PER_SMS = 160;
private static final String SMS_SENT_ACTION = ".SMS_SENT";
private static final int DEFAULT_TIMEOUT_MILLISECOND = 60 * 1000;
private static final String SMS_RECEIVED_EVENT_NAME = "ReceivedSms";
private static final String SMS_SENT_EVENT_NAME = "SentSms";
private static final String SMS_CALLBACK_ID_PREFIX = "sendSms-";
private static int mCallbackCounter = 0;
private final Context mContext;
private final SmsManager mSmsManager;
public SmsSnippet() {
this.mContext = InstrumentationRegistry.getInstrumentation().getContext();
this.mSmsManager = SmsManager.getDefault();
}
/**
* Send SMS and return after waiting for send confirmation (with a timeout of 60 seconds).
*
* @param phoneNumber A String representing phone number with country code.
* @param message A String representing the message to send.
* @throws SmsSnippetException on SMS send error.
*/
@Rpc(description = "Send SMS to a specified phone number.")
public void sendSms(String phoneNumber, String message) throws Throwable {
String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter);
OutboundSmsReceiver receiver = new OutboundSmsReceiver(mContext, callbackId);
if (message.length() > MAX_CHAR_COUNT_PER_SMS) {
ArrayList<String> parts = mSmsManager.divideMessage(message);
receiver.setExpectedMessageCount(parts.size());
if (Build.VERSION.SDK_INT >= 33) {
mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION), null,
null,
Context.RECEIVER_EXPORTED);
} else {
mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION));
}
mSmsManager.sendMultipartTextMessage(
/* destinationAddress= */ phoneNumber,
/* scAddress= */ null,
/* parts= */ parts,
/* sentIntents= */ IntStream.range(0, parts.size())
.mapToObj(
i ->
PendingIntent.getBroadcast(
/* context= */ mContext,
/* requestCode= */ 0,
/* intent= */ new Intent(SMS_SENT_ACTION),
/* flags= */ PendingIntent.FLAG_IMMUTABLE))
.collect(toCollection(ArrayList::new)),
/* deliveryIntents= */ null);
} else {
PendingIntent sentIntent =
PendingIntent.getBroadcast(
/* context= */ mContext,
/* requestCode= */ 0,
/* intent= */ new Intent(SMS_SENT_ACTION),
/* flags= */ PendingIntent.FLAG_IMMUTABLE);
receiver.setExpectedMessageCount(1);
if (Build.VERSION.SDK_INT >= 33) {
mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION), null,
null,
Context.RECEIVER_EXPORTED);
} else {
mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION));
}
mSmsManager.sendTextMessage(
/* destinationAddress= */ phoneNumber,
/* scAddress= */ null,
/* text= */ message,
/* sentIntent= */ sentIntent,
/* deliveryIntent= */ null);
}
SnippetEvent result =
Utils.waitForSnippetEvent(
callbackId, SMS_SENT_EVENT_NAME, DEFAULT_TIMEOUT_MILLISECOND);
if (result.getData().containsKey("error")) {
throw new SmsSnippetException(
"Failed to send SMS, error code: " + result.getData().getInt("error"));
}
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@AsyncRpc(description = "Async wait for incoming SMS message.")
public void asyncWaitForSms(String callbackId) {
SmsReceiver receiver = new SmsReceiver(mContext, callbackId);
mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION));
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Rpc(description = "Wait for incoming SMS message.")
public JSONObject waitForSms(int timeoutMillis) throws Throwable {
String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter);
SmsReceiver receiver = new SmsReceiver(mContext, callbackId);
mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION));
return Utils.waitForSnippetEvent(callbackId, SMS_RECEIVED_EVENT_NAME, timeoutMillis)
.toJson();
}
@Override
public void shutdown() {}
private static class OutboundSmsReceiver extends BroadcastReceiver {
private final String mCallbackId;
private Context mContext;
private final EventCache mEventCache;
private int mExpectedMessageCount;
public OutboundSmsReceiver(Context context, String callbackId) {
this.mCallbackId = callbackId;
this.mContext = context;
this.mEventCache = EventCache.getInstance();
mExpectedMessageCount = 0;
}
public void setExpectedMessageCount(int count) {
mExpectedMessageCount = count;
}
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (SMS_SENT_ACTION.equals(action)) {
SnippetEvent event = new SnippetEvent(mCallbackId, SMS_SENT_EVENT_NAME);
switch (getResultCode()) {
case Activity.RESULT_OK:
if (mExpectedMessageCount == 1) {
event.getData().putBoolean("sent", true);
mEventCache.postEvent(event);
mContext.unregisterReceiver(this);
}
if (mExpectedMessageCount > 0) {
mExpectedMessageCount--;
}
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
case SmsManager.RESULT_ERROR_NO_SERVICE:
case SmsManager.RESULT_ERROR_NULL_PDU:
case SmsManager.RESULT_ERROR_RADIO_OFF:
event.getData().putBoolean("sent", false);
event.getData().putInt("error_code", getResultCode());
mEventCache.postEvent(event);
mContext.unregisterReceiver(this);
break;
default:
event.getData().putBoolean("sent", false);
event.getData().putInt("error_code", -1 /* Unknown */);
mEventCache.postEvent(event);
mContext.unregisterReceiver(this);
break;
}
}
}
}
private static class SmsReceiver extends BroadcastReceiver {
private final String mCallbackId;
private Context mContext;
private final EventCache mEventCache;
public SmsReceiver(Context context, String callbackId) {
this.mCallbackId = callbackId;
this.mContext = context;
this.mEventCache = EventCache.getInstance();
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public void onReceive(Context receivedContext, Intent intent) {
if (Intents.SMS_RECEIVED_ACTION.equals(intent.getAction())) {
SnippetEvent event = new SnippetEvent(mCallbackId, SMS_RECEIVED_EVENT_NAME);
Bundle extras = intent.getExtras();
if (extras != null) {
SmsMessage[] msgs = Intents.getMessagesFromIntent(intent);
StringBuilder smsMsg = new StringBuilder();
SmsMessage sms = msgs[0];
String sender = sms.getOriginatingAddress();
event.getData().putString("OriginatingAddress", sender);
for (SmsMessage msg : msgs) {
smsMsg.append(msg.getMessageBody());
}
event.getData().putString("MessageBody", smsMsg.toString());
mEventCache.postEvent(event);
mContext.unregisterReceiver(this);
}
}
}
}
}