• 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             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