1 /* 2 * Copyright (C) 2017 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.google.android.mobly.snippet.bundled; 18 19 import static java.util.stream.Collectors.toCollection; 20 21 import android.annotation.TargetApi; 22 import android.app.Activity; 23 import android.app.PendingIntent; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.os.Build; 29 import android.os.Bundle; 30 import android.provider.Telephony.Sms.Intents; 31 import android.telephony.SmsManager; 32 import android.telephony.SmsMessage; 33 import androidx.test.platform.app.InstrumentationRegistry; 34 import com.google.android.mobly.snippet.Snippet; 35 import com.google.android.mobly.snippet.bundled.utils.Utils; 36 import com.google.android.mobly.snippet.event.EventCache; 37 import com.google.android.mobly.snippet.event.SnippetEvent; 38 import com.google.android.mobly.snippet.rpc.AsyncRpc; 39 import com.google.android.mobly.snippet.rpc.Rpc; 40 import java.util.ArrayList; 41 import java.util.stream.IntStream; 42 import org.json.JSONObject; 43 44 /** Snippet class for SMS RPCs. */ 45 public class SmsSnippet implements Snippet { 46 47 private static class SmsSnippetException extends Exception { 48 private static final long serialVersionUID = 1L; 49 SmsSnippetException(String msg)50 SmsSnippetException(String msg) { 51 super(msg); 52 } 53 } 54 55 private static final int MAX_CHAR_COUNT_PER_SMS = 160; 56 private static final String SMS_SENT_ACTION = ".SMS_SENT"; 57 private static final int DEFAULT_TIMEOUT_MILLISECOND = 60 * 1000; 58 private static final String SMS_RECEIVED_EVENT_NAME = "ReceivedSms"; 59 private static final String SMS_SENT_EVENT_NAME = "SentSms"; 60 private static final String SMS_CALLBACK_ID_PREFIX = "sendSms-"; 61 62 private static int mCallbackCounter = 0; 63 64 private final Context mContext; 65 private final SmsManager mSmsManager; 66 SmsSnippet()67 public SmsSnippet() { 68 this.mContext = InstrumentationRegistry.getInstrumentation().getContext(); 69 this.mSmsManager = SmsManager.getDefault(); 70 } 71 72 /** 73 * Send SMS and return after waiting for send confirmation (with a timeout of 60 seconds). 74 * 75 * @param phoneNumber A String representing phone number with country code. 76 * @param message A String representing the message to send. 77 * @throws SmsSnippetException on SMS send error. 78 */ 79 @Rpc(description = "Send SMS to a specified phone number.") sendSms(String phoneNumber, String message)80 public void sendSms(String phoneNumber, String message) throws Throwable { 81 String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter); 82 OutboundSmsReceiver receiver = new OutboundSmsReceiver(mContext, callbackId); 83 84 if (message.length() > MAX_CHAR_COUNT_PER_SMS) { 85 ArrayList<String> parts = mSmsManager.divideMessage(message); 86 receiver.setExpectedMessageCount(parts.size()); 87 if (Build.VERSION.SDK_INT >= 33) { 88 mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION), null, 89 null, 90 Context.RECEIVER_EXPORTED); 91 } else { 92 mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); 93 } 94 mSmsManager.sendMultipartTextMessage( 95 /* destinationAddress= */ phoneNumber, 96 /* scAddress= */ null, 97 /* parts= */ parts, 98 /* sentIntents= */ IntStream.range(0, parts.size()) 99 .mapToObj( 100 i -> 101 PendingIntent.getBroadcast( 102 /* context= */ mContext, 103 /* requestCode= */ 0, 104 /* intent= */ new Intent(SMS_SENT_ACTION), 105 /* flags= */ PendingIntent.FLAG_IMMUTABLE)) 106 .collect(toCollection(ArrayList::new)), 107 /* deliveryIntents= */ null); 108 } else { 109 PendingIntent sentIntent = 110 PendingIntent.getBroadcast( 111 /* context= */ mContext, 112 /* requestCode= */ 0, 113 /* intent= */ new Intent(SMS_SENT_ACTION), 114 /* flags= */ PendingIntent.FLAG_IMMUTABLE); 115 receiver.setExpectedMessageCount(1); 116 if (Build.VERSION.SDK_INT >= 33) { 117 mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION), null, 118 null, 119 Context.RECEIVER_EXPORTED); 120 } else { 121 mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); 122 } 123 mSmsManager.sendTextMessage( 124 /* destinationAddress= */ phoneNumber, 125 /* scAddress= */ null, 126 /* text= */ message, 127 /* sentIntent= */ sentIntent, 128 /* deliveryIntent= */ null); 129 } 130 131 SnippetEvent result = 132 Utils.waitForSnippetEvent( 133 callbackId, SMS_SENT_EVENT_NAME, DEFAULT_TIMEOUT_MILLISECOND); 134 135 if (result.getData().containsKey("error")) { 136 throw new SmsSnippetException( 137 "Failed to send SMS, error code: " + result.getData().getInt("error")); 138 } 139 } 140 141 @TargetApi(Build.VERSION_CODES.KITKAT) 142 @AsyncRpc(description = "Async wait for incoming SMS message.") asyncWaitForSms(String callbackId)143 public void asyncWaitForSms(String callbackId) { 144 SmsReceiver receiver = new SmsReceiver(mContext, callbackId); 145 mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); 146 } 147 148 @TargetApi(Build.VERSION_CODES.KITKAT) 149 @Rpc(description = "Wait for incoming SMS message.") waitForSms(int timeoutMillis)150 public JSONObject waitForSms(int timeoutMillis) throws Throwable { 151 String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter); 152 SmsReceiver receiver = new SmsReceiver(mContext, callbackId); 153 mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); 154 return Utils.waitForSnippetEvent(callbackId, SMS_RECEIVED_EVENT_NAME, timeoutMillis) 155 .toJson(); 156 } 157 158 @Override shutdown()159 public void shutdown() {} 160 161 private static class OutboundSmsReceiver extends BroadcastReceiver { 162 private final String mCallbackId; 163 private Context mContext; 164 private final EventCache mEventCache; 165 private int mExpectedMessageCount; 166 OutboundSmsReceiver(Context context, String callbackId)167 public OutboundSmsReceiver(Context context, String callbackId) { 168 this.mCallbackId = callbackId; 169 this.mContext = context; 170 this.mEventCache = EventCache.getInstance(); 171 mExpectedMessageCount = 0; 172 } 173 setExpectedMessageCount(int count)174 public void setExpectedMessageCount(int count) { 175 mExpectedMessageCount = count; 176 } 177 178 @Override onReceive(Context context, Intent intent)179 public void onReceive(Context context, Intent intent) { 180 String action = intent.getAction(); 181 182 if (SMS_SENT_ACTION.equals(action)) { 183 SnippetEvent event = new SnippetEvent(mCallbackId, SMS_SENT_EVENT_NAME); 184 switch (getResultCode()) { 185 case Activity.RESULT_OK: 186 if (mExpectedMessageCount == 1) { 187 event.getData().putBoolean("sent", true); 188 mEventCache.postEvent(event); 189 mContext.unregisterReceiver(this); 190 } 191 192 if (mExpectedMessageCount > 0) { 193 mExpectedMessageCount--; 194 } 195 break; 196 case SmsManager.RESULT_ERROR_GENERIC_FAILURE: 197 case SmsManager.RESULT_ERROR_NO_SERVICE: 198 case SmsManager.RESULT_ERROR_NULL_PDU: 199 case SmsManager.RESULT_ERROR_RADIO_OFF: 200 event.getData().putBoolean("sent", false); 201 event.getData().putInt("error_code", getResultCode()); 202 mEventCache.postEvent(event); 203 mContext.unregisterReceiver(this); 204 break; 205 default: 206 event.getData().putBoolean("sent", false); 207 event.getData().putInt("error_code", -1 /* Unknown */); 208 mEventCache.postEvent(event); 209 mContext.unregisterReceiver(this); 210 break; 211 } 212 } 213 } 214 } 215 216 private static class SmsReceiver extends BroadcastReceiver { 217 private final String mCallbackId; 218 private Context mContext; 219 private final EventCache mEventCache; 220 SmsReceiver(Context context, String callbackId)221 public SmsReceiver(Context context, String callbackId) { 222 this.mCallbackId = callbackId; 223 this.mContext = context; 224 this.mEventCache = EventCache.getInstance(); 225 } 226 227 @TargetApi(Build.VERSION_CODES.KITKAT) 228 @Override onReceive(Context receivedContext, Intent intent)229 public void onReceive(Context receivedContext, Intent intent) { 230 if (Intents.SMS_RECEIVED_ACTION.equals(intent.getAction())) { 231 SnippetEvent event = new SnippetEvent(mCallbackId, SMS_RECEIVED_EVENT_NAME); 232 Bundle extras = intent.getExtras(); 233 if (extras != null) { 234 SmsMessage[] msgs = Intents.getMessagesFromIntent(intent); 235 StringBuilder smsMsg = new StringBuilder(); 236 237 SmsMessage sms = msgs[0]; 238 String sender = sms.getOriginatingAddress(); 239 event.getData().putString("OriginatingAddress", sender); 240 241 for (SmsMessage msg : msgs) { 242 smsMsg.append(msg.getMessageBody()); 243 } 244 event.getData().putString("MessageBody", smsMsg.toString()); 245 mEventCache.postEvent(event); 246 mContext.unregisterReceiver(this); 247 } 248 } 249 } 250 } 251 } 252