aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorarammelk <arammelk@gmail.com>2017-07-24 18:52:36 -0700
committerDavid T.H. Kao <dthkao@gmail.com>2017-07-24 18:52:36 -0700
commit4dbbe064143465c11a5bd0e78386bf6f06532934 (patch)
tree723f613b938e5dbea81fc508ac9c8d3f9f4d2c9f /src
parent3b48f2df00a2204e9ce3cba7ec538d4097e42018 (diff)
downloadmobly-bundled-snippets-4dbbe064143465c11a5bd0e78386bf6f06532934.tar.gz
Add SMS snippet (#69)
* add sms snippet * Remove unnecessary sleep * Add missing copyright to IncomingSmsBroadcastReceiver * Add sent confirmation for sendSms, this makes it an AsyncRpc. - Added OutboundSmsReceiver to post events when SMS are send action is done. Posts error back if action is not successful. Also wait for all parts of a multipart message to be sent before posting event. Example usage now: event = s.sendSms('+15555678912', 'message message message').waitAndGet('SentSms') - Renamed IncomingSmsBroardcastReceiver to SmsReceiver and make it a private class of SmsSnippet. Converted sendSms to AsyncRpc so it will post event back when message is sent * Add sent confirmation for sendSms, this makes it an AsyncRpc. - Added OutboundSmsReceiver to post events when SMS are send action is done. Posts error back if action is not successful. Also wait for all parts of a multipart message to be sent before posting event. Example usage now: event = s.sendSms('+15555678912', 'message message message').waitAndGet('SentSms') - Renamed IncomingSmsBroardcastReceiver to SmsReceiver and make it a private class of SmsSnippet. Converted sendSms to AsyncRpc so it will post event back when message is sent * Cleanup naming * Align event key names with Java object property names. * Use EventCache + built in mobly SnippetEvents to wait for SMS sent confirmation. This shares code with EventSnippet in Mobly Snippet Lib. I'm not sure what the accepted practice is for sharing code across github projects? Should I refactor some of the event code in Mobly Bundle Lib into EventUtils, then use that in Mobly Bundled Snippets to avoid duplicating this? I still want to check this is in as is, to have a working version earlier. * Return data from event instead of fulll SnippetEvent. This way it works like so: >>> result = s.sendSms("15551234567", "Hello world") >>> pprint.pprint(result) {u'sent': True} >>> * throw exception on error instead and some javadocs. * cleanup
Diffstat (limited to 'src')
-rw-r--r--src/main/AndroidManifest.xml4
-rw-r--r--src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java212
2 files changed, 216 insertions, 0 deletions
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 1f6744b..7651182 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -17,10 +17,13 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+ <uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+ <uses-permission android:name="android.permission.SEND_SMS" />
<application>
<meta-data
android:name="mobly-snippets"
@@ -34,6 +37,7 @@
com.google.android.mobly.snippet.bundled.MediaSnippet,
com.google.android.mobly.snippet.bundled.NotificationSnippet,
com.google.android.mobly.snippet.bundled.TelephonySnippet,
+ com.google.android.mobly.snippet.bundled.SmsSnippet,
com.google.android.mobly.snippet.bundled.WifiManagerSnippet" />
</application>
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java
new file mode 100644
index 0000000..1fdb409
--- /dev/null
+++ b/src/main/java/com/google/android/mobly/snippet/bundled/SmsSnippet.java
@@ -0,0 +1,212 @@
+/*
+ * 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 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.support.test.InstrumentationRegistry;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+
+import com.google.android.mobly.snippet.Snippet;
+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.JsonBuilder;
+import com.google.android.mobly.snippet.rpc.Rpc;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+
+/** Snippet class for SMS RPCs. */
+public class SmsSnippet implements Snippet {
+
+ private static class SmsSnippetException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public 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.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 InterruptedException
+ * @throws SmsSnippetException
+ * @throws JSONException
+ */
+ @Rpc(description = "Send SMS to a specified phone number.")
+ public void sendSms(String phoneNumber, String message)
+ throws InterruptedException, SmsSnippetException, JSONException {
+ String callbackId = new StringBuilder().append(SMS_CALLBACK_ID_PREFIX)
+ .append(++mCallbackCounter).toString();
+ OutboundSmsReceiver receiver = new OutboundSmsReceiver(mContext, callbackId);
+
+ if (message.length() > MAX_CHAR_COUNT_PER_SMS) {
+ ArrayList<String> parts = mSmsManager.divideMessage(message);
+ ArrayList<PendingIntent> sIntents = new ArrayList<>();
+ for (String part : parts) {
+ sIntents.add(PendingIntent.getBroadcast(
+ mContext, 0, new Intent(SMS_SENT_ACTION), 0));
+ }
+ receiver.setExpectedMessageCount(parts.size());
+ mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION));
+ mSmsManager.sendMultipartTextMessage(phoneNumber, null, parts, sIntents, null);
+ } else {
+ PendingIntent sentIntent = PendingIntent.getBroadcast(
+ mContext, 0, new Intent(SMS_SENT_ACTION), 0);
+ receiver.setExpectedMessageCount(1);
+ mContext.registerReceiver(receiver, new IntentFilter(SMS_SENT_ACTION));
+ mSmsManager.sendTextMessage(phoneNumber, null, message, sentIntent, null);
+ }
+
+ String qId = EventCache.getQueueId(callbackId, SMS_SENT_EVENT_NAME);
+ LinkedBlockingDeque<SnippetEvent> q = EventCache.getInstance().getEventDeque(qId);
+ SnippetEvent result = q.pollFirst(DEFAULT_TIMEOUT_MILLISECOND, TimeUnit.MILLISECONDS);
+ if (result == null) {
+ throw new SmsSnippetException("Timed out waiting for SMS sent confirmation.");
+ } else 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));
+ }
+
+ @Override
+ public void shutdown() {}
+
+ private 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;
+ }
+ }
+ }
+ }
+
+ private 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);
+ }
+ }
+ }
+ }
+}