• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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