| /* |
| * 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); |
| } |
| } |
| } |
| } |
| } |