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 mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); 88 mSmsManager.sendMultipartTextMessage( 89 /* destinationAddress= */ phoneNumber, 90 /* scAddress= */ null, 91 /* parts= */ parts, 92 /* sentIntents= */ IntStream.range(0, parts.size()) 93 .mapToObj( 94 i -> 95 PendingIntent.getBroadcast( 96 /* context= */ mContext, 97 /* requestCode= */ 0, 98 /* intent= */ new Intent(SMS_SENT_ACTION), 99 /* flags= */ PendingIntent.FLAG_IMMUTABLE)) 100 .collect(toCollection(ArrayList::new)), 101 /* deliveryIntents= */ null); 102 } else { 103 PendingIntent sentIntent = 104 PendingIntent.getBroadcast( 105 /* context= */ mContext, 106 /* requestCode= */ 0, 107 /* intent= */ new Intent(SMS_SENT_ACTION), 108 /* flags= */ PendingIntent.FLAG_IMMUTABLE); 109 receiver.setExpectedMessageCount(1); 110 mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION)); 111 mSmsManager.sendTextMessage( 112 /* destinationAddress= */ phoneNumber, 113 /* scAddress= */ null, 114 /* text= */ message, 115 /* sentIntent= */ sentIntent, 116 /* deliveryIntent= */ null); 117 } 118 119 SnippetEvent result = 120 Utils.waitForSnippetEvent( 121 callbackId, SMS_SENT_EVENT_NAME, DEFAULT_TIMEOUT_MILLISECOND); 122 123 if (result.getData().containsKey("error")) { 124 throw new SmsSnippetException( 125 "Failed to send SMS, error code: " + result.getData().getInt("error")); 126 } 127 } 128 129 @TargetApi(Build.VERSION_CODES.KITKAT) 130 @AsyncRpc(description = "Async wait for incoming SMS message.") asyncWaitForSms(String callbackId)131 public void asyncWaitForSms(String callbackId) { 132 SmsReceiver receiver = new SmsReceiver(mContext, callbackId); 133 mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); 134 } 135 136 @TargetApi(Build.VERSION_CODES.KITKAT) 137 @Rpc(description = "Wait for incoming SMS message.") waitForSms(int timeoutMillis)138 public JSONObject waitForSms(int timeoutMillis) throws Throwable { 139 String callbackId = SMS_CALLBACK_ID_PREFIX + (++mCallbackCounter); 140 SmsReceiver receiver = new SmsReceiver(mContext, callbackId); 141 mContext.registerReceiver(receiver, new IntentFilter(Intents.SMS_RECEIVED_ACTION)); 142 return Utils.waitForSnippetEvent(callbackId, SMS_RECEIVED_EVENT_NAME, timeoutMillis) 143 .toJson(); 144 } 145 146 @Override shutdown()147 public void shutdown() {} 148 149 private static class OutboundSmsReceiver extends BroadcastReceiver { 150 private final String mCallbackId; 151 private Context mContext; 152 private final EventCache mEventCache; 153 private int mExpectedMessageCount; 154 OutboundSmsReceiver(Context context, String callbackId)155 public OutboundSmsReceiver(Context context, String callbackId) { 156 this.mCallbackId = callbackId; 157 this.mContext = context; 158 this.mEventCache = EventCache.getInstance(); 159 mExpectedMessageCount = 0; 160 } 161 setExpectedMessageCount(int count)162 public void setExpectedMessageCount(int count) { 163 mExpectedMessageCount = count; 164 } 165 166 @Override onReceive(Context context, Intent intent)167 public void onReceive(Context context, Intent intent) { 168 String action = intent.getAction(); 169 170 if (SMS_SENT_ACTION.equals(action)) { 171 SnippetEvent event = new SnippetEvent(mCallbackId, SMS_SENT_EVENT_NAME); 172 switch (getResultCode()) { 173 case Activity.RESULT_OK: 174 if (mExpectedMessageCount == 1) { 175 event.getData().putBoolean("sent", true); 176 mEventCache.postEvent(event); 177 mContext.unregisterReceiver(this); 178 } 179 180 if (mExpectedMessageCount > 0) { 181 mExpectedMessageCount--; 182 } 183 break; 184 case SmsManager.RESULT_ERROR_GENERIC_FAILURE: 185 case SmsManager.RESULT_ERROR_NO_SERVICE: 186 case SmsManager.RESULT_ERROR_NULL_PDU: 187 case SmsManager.RESULT_ERROR_RADIO_OFF: 188 event.getData().putBoolean("sent", false); 189 event.getData().putInt("error_code", getResultCode()); 190 mEventCache.postEvent(event); 191 mContext.unregisterReceiver(this); 192 break; 193 default: 194 event.getData().putBoolean("sent", false); 195 event.getData().putInt("error_code", -1 /* Unknown */); 196 mEventCache.postEvent(event); 197 mContext.unregisterReceiver(this); 198 break; 199 } 200 } 201 } 202 } 203 204 private static class SmsReceiver extends BroadcastReceiver { 205 private final String mCallbackId; 206 private Context mContext; 207 private final EventCache mEventCache; 208 SmsReceiver(Context context, String callbackId)209 public SmsReceiver(Context context, String callbackId) { 210 this.mCallbackId = callbackId; 211 this.mContext = context; 212 this.mEventCache = EventCache.getInstance(); 213 } 214 215 @TargetApi(Build.VERSION_CODES.KITKAT) 216 @Override onReceive(Context receivedContext, Intent intent)217 public void onReceive(Context receivedContext, Intent intent) { 218 if (Intents.SMS_RECEIVED_ACTION.equals(intent.getAction())) { 219 SnippetEvent event = new SnippetEvent(mCallbackId, SMS_RECEIVED_EVENT_NAME); 220 Bundle extras = intent.getExtras(); 221 if (extras != null) { 222 SmsMessage[] msgs = Intents.getMessagesFromIntent(intent); 223 StringBuilder smsMsg = new StringBuilder(); 224 225 SmsMessage sms = msgs[0]; 226 String sender = sms.getOriginatingAddress(); 227 event.getData().putString("OriginatingAddress", sender); 228 229 for (SmsMessage msg : msgs) { 230 smsMsg.append(msg.getMessageBody()); 231 } 232 event.getData().putString("MessageBody", smsMsg.toString()); 233 mEventCache.postEvent(event); 234 mContext.unregisterReceiver(this); 235 } 236 } 237 } 238 } 239 } 240