AI2 Component  (Version nb184)
Texting.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
2 // Copyright 2009-2011 Google, All Rights reserved
3 // Copyright 2011-2020 MIT, All rights reserved
4 // Released under the Apache License, Version 2.0
5 // http://www.apache.org/licenses/LICENSE-2.0
6 
7 package com.google.appinventor.components.runtime;
8 
9 import java.io.BufferedReader;
10 import java.io.FileNotFoundException;
11 import java.io.FileOutputStream;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.io.InputStreamReader;
15 import java.io.OutputStreamWriter;
16 import java.net.CookieManager;
17 import java.net.HttpCookie;
18 import java.net.HttpURLConnection;
19 import java.net.URL;
20 import java.net.URLEncoder;
21 import java.util.List;
22 import java.util.Queue;
23 import java.util.concurrent.ConcurrentLinkedQueue;
24 
25 import android.Manifest;
26 import android.net.Uri;
27 import android.text.TextUtils;
28 import org.json.JSONException;
29 import org.json.JSONObject;
30 import java.util.ArrayList;
31 
37 
56 
57 import android.app.Activity;
58 import android.app.NotificationManager;
59 import android.app.PendingIntent;
60 import android.content.BroadcastReceiver;
61 import android.content.Context;
62 import android.content.Intent;
63 import android.content.IntentFilter;
64 import android.content.SharedPreferences;
65 import android.os.AsyncTask;
66 import android.telephony.SmsManager;
67 import android.telephony.SmsMessage;
68 import android.util.Log;
69 import android.widget.Toast;
70 
113 @SuppressWarnings("deprecation")
114 @DesignerComponent(version = YaVersion.TEXTING_COMPONENT_VERSION,
115  description = "<p>A component that will, when the <code>SendMessage</code> method is " +
116  "called, send the text message specified in the <code>Message</code> " +
117  "property to the phone number specified in the <code>PhoneNumber</code> " +
118  "property.</p> " +
119  "<p>If the <code>ReceivingEnabled</code> property is set to 1 messages " +
120  "will <b>not</b> be received. If <code>ReceivingEnabled</code> is set " +
121  "to 2 messages will be received only when the application is " +
122  "running. Finally if <code>ReceivingEnabled</code> is set to 3, " +
123  "messages will be received when the application is running <b>and</b> " +
124  "when the application is not running they will be queued and a " +
125  "notification displayed to the user.</p> " +
126  "<p>When a message arrives, the <code>MessageReceived</code> event is " +
127  "raised and provides the sending number and message.</p> " +
128  "<p> An app that includes this component will receive messages even " +
129  "when it is in the background (i.e. when it's not visible on the " +
130  "screen) and, moreso, even if the app is not running, so long as it's " +
131  "installed on the phone. If the phone receives a text message when the " +
132  "app is not in the foreground, the phone will show a notification in " +
133  "the notification bar. Selecting the notification will bring up the " +
134  "app. As an app developer, you'll probably want to give your users the " +
135  "ability to control ReceivingEnabled so that they can make the phone " +
136  "ignore text messages.</p> " +
137  "<p>If the GoogleVoiceEnabled property is true, messages can be sent " +
138  "over Wifi using Google Voice. This option requires that the user have " +
139  "a Google Voice account and that the mobile Voice app is installed on " +
140  "the phone. The Google Voice option works only on phones that support " +
141  "Android 2.0 (Eclair) or higher.</p> " +
142  "<p>To specify the phone number (e.g., 650-555-1212), set the " +
143  "<code>PhoneNumber</code> property to a Text string with the specified " +
144  "digits (e.g., 6505551212). Dashes, dots, and parentheses may be " +
145  "included (e.g., (650)-555-1212) but will be ignored; spaces may not be " +
146  "included.</p> " +
147  "<p>Another way for an app to specify a phone number would be to " +
148  "include a <code>PhoneNumberPicker</code> component, which lets the " +
149  "users select a phone numbers from the ones stored in the the phone's " +
150  "contacts.</p>",
151  category = ComponentCategory.SOCIAL,
152  nonVisible = true,
153  iconName = "images/texting.png")
154 
155 @SimpleObject
156 @UsesPermissions(permissionNames =
157  "com.google.android.apps.googlevoice.permission.RECEIVE_SMS, " +
158  "com.google.android.apps.googlevoice.permission.SEND_SMS, " +
159  "android.permission.ACCOUNT_MANAGER, android.permission.MANAGE_ACCOUNTS, " +
160  "android.permission.GET_ACCOUNTS, android.permission.USE_CREDENTIALS")
161 @UsesLibraries(libraries =
162  "google-api-client-beta.jar," +
163  "google-api-client-android2-beta.jar," +
164  "google-http-client-beta.jar," +
165  "google-http-client-android2-beta.jar," +
166  "google-http-client-android3-beta.jar," +
167  "google-oauth-client-beta.jar," +
168  "guava-14.0.1.jar")
172 
173  public static final String TAG = "Texting Component";
174 
178  public static final int TEXTING_REQUEST_CODE = 0x54455854;
179 
180  public static final String SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED";
181  public static final String GV_SMS_RECEIVED = "com.google.android.apps.googlevoice.SMS_RECEIVED";
182  public static final String PHONE_NUMBER_TAG = "com.google.android.apps.googlevoice.PHONE_NUMBER";
183  public static final String MESSAGE_TAG = "com.google.android.apps.googlevoice.TEXT";
184  public static final String TELEPHONY_INTENT_FILTER = "android.provider.Telephony.SMS_RECEIVED";
185  public static final String GV_INTENT_FILTER = "com.google.android.apps.googlevoice.SMS_RECEIVED";
186  public static final String GV_PACKAGE_NAME = "com.google.android.apps.googlevoice";
187  public static final String GV_SMS_SEND_URL = "https://www.google.com/voice/b/0/sms/send/";
188  public static final String GV_URL = "https://www.google.com/voice/b/0/redirection/voice";
189 
190 
191  // Meta data key and value that identify an app for handling incoming SMS
192  // Used by Texting component
193  public static final String META_DATA_SMS_KEY = "sms_handler_component";
194  public static final String META_DATA_SMS_VALUE = "Texting";
195 
196  // private static final String GV_PHONES_INFO_URL = "https://www.google.com/voice/b/0/settings/tab/phones";
197  private static final String GV_SERVICE = "grandcentral";
198  private static final String USER_AGENT = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.A.B.C Safari/525.13";
199  private static final int SERVER_TIMEOUT_MS = 30000;
200  private static final String SENT = "SMS_SENT";
201  private static final String UTF8 = "UTF-8";
202  private static final String MESSAGE_DELIMITER = "\u0001";
203  private static final String PREF_GVENABLED = "gvenabled"; // Boolean flag for GV is enabled
204  private static final String PREF_RCVENABLED_LEGACY = "receiving"; // Is receiving enabled (Legacy boolean version)
205  private static final String PREF_RCVENABLED = "receiving2"; // Is receiving enabled
206  private static final String PREF_FILE = "TextingState"; // State of Texting component
207 
208 
209  // Google Voice oauth helper
210  private GoogleVoiceUtil gvHelper;
211  private static Activity activity;
212  private static Component component;
213  private String authToken;
214 
215  // Indicates whether the component is receiving messages or not
216  private static int receivingEnabled = ComponentConstants.TEXT_RECEIVING_FOREGROUND;
217  private SmsManager smsManager;
218 
219  // The phone number to send the text message to.
220  private String phoneNumber;
221  // The message to send
222  private String message;
223  // Whether or not Google Voice is enabled.
224  private boolean googleVoiceEnabled;
225 
226  // No messages can be received until Initialized
227  private boolean isInitialized;
228 
229  // True when resumed and false when paused.
230  // Messages are cached when app is not running
231  private static boolean isRunning;
232 
233  // Cache file for cached messages
234  private static final String CACHE_FILE = "textingmsgcache";
235  private static int messagesCached;
236  private static Object cacheLock = new Object();
237 
238  //Stores up to 50 pending messages awaiting authentication
239  private Queue<String> pendingQueue = new ConcurrentLinkedQueue<String>();
240 
241  private ComponentContainer container; // Need this for error reporting
242 
243  // Do we have SEND_SMS permission enabled?
244  private boolean havePermission = false;
245 
246  // Do we have RECEIVE_SMS permission enabled?
247  private boolean haveReceivePermission = false;
248 
254  public Texting(ComponentContainer container) {
255  super(container.$form());
256  Log.d(TAG, "Texting constructor");
257  this.container = container;
258  Texting.component = (Texting)this;
259  activity = container.$context();
260 
261  SharedPreferences prefs = activity.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
262  if (prefs != null) {
263  receivingEnabled = prefs.getInt(PREF_RCVENABLED, -1);
264  if (receivingEnabled == -1) {
265  if (prefs.getBoolean(PREF_RCVENABLED_LEGACY, true)) {
267  } else {
268  receivingEnabled = ComponentConstants.TEXT_RECEIVING_OFF;
269  }
270  }
271 
272  googleVoiceEnabled = prefs.getBoolean(PREF_GVENABLED, false);
273  Log.i(TAG, "Starting with receiving Enabled=" + receivingEnabled + " GV enabled=" + googleVoiceEnabled);
274  } else {
275  receivingEnabled = ComponentConstants.TEXT_RECEIVING_OFF;
276  googleVoiceEnabled = false;
277  }
278 
279  // Handles authenticating for GV feature. This sets the authToken.
280  if (googleVoiceEnabled)
281  new AsyncAuthenticate().execute();
282 
283  smsManager = SmsManager.getDefault();
284  PhoneNumber("");
285 
286  isInitialized = false; // Set true when the form is initialized and can dispatch
287  isRunning = false; // This will be set true in onResume and false in onPause
288 
289  // Register this component for lifecycle callbacks
290  container.$form().registerForOnInitialize(this);
291  container.$form().registerForOnResume(this);
292  container.$form().registerForOnPause(this);
293  container.$form().registerForOnStop(this);
294  }
295 
301  @Override
302  public void onInitialize() {
303  Log.i(TAG, "onInitialize()");
304  isInitialized = true;
305  isRunning = true; // Added b/c REPL does not call onResume when starting Texting component
306  processCachedMessages();
307  NotificationManager nm = (NotificationManager) activity.getSystemService(Context.NOTIFICATION_SERVICE);
309  }
310 
311  // Called from runtime.scm
312  public void Initialize() {
313  if (receivingEnabled > ComponentConstants.TEXT_RECEIVING_OFF && !haveReceivePermission) {
314  // Request receive SMS permission if we are configured to receive but do not have permission
315  requestReceiveSmsPermission("Initialize");
316  }
317  }
318 
324  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
326  public void PhoneNumber(String phoneNumber) {
327  Log.i(TAG, "PhoneNumber set: " + phoneNumber);
328  this.phoneNumber = phoneNumber;
329  }
330 
338  description = "The number that the message will be sent to when the SendMessage method " +
339  "is called. The " +
340  "number is a text string with the specified digits (e.g., 6505551212). Dashes, dots, " +
341  "and parentheses may be included (e.g., (650)-555-1212) but will be ignored; spaces " +
342  "should not be included.")
343  public String PhoneNumber() {
344  return phoneNumber;
345  }
346 
354  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
356  description = "The message that will be sent when the SendMessage method is called.")
357  public void Message(String message) {
358  Log.i(TAG, "Message set: " + message);
359  this.message = message;
360  }
361 
367  public String Message() {
368  return message;
369  }
370 
375  public void SendMessage() {
376  String phoneNumber = this.phoneNumber;
377  String message = this.message;
378 
379  Uri uri = Uri.parse("smsto:" + phoneNumber);
380  Intent i = new Intent(Intent.ACTION_SENDTO, uri);
381  i.putExtra("sms_body", message);
382  if (i.resolveActivity(form.getPackageManager()) != null) {
383  form.registerForActivityResult(this, TEXTING_REQUEST_CODE);
384  form.startActivityForResult(i, TEXTING_REQUEST_CODE);
385  }
386  }
387 
393  @UsesPermissions({Manifest.permission.SEND_SMS, Manifest.permission.READ_PHONE_STATE})
394  @SimpleFunction
395  public void SendMessageDirect() {
396  Log.i(TAG, "Sending message " + message + " to " + phoneNumber);
397 
398  // To avoid possible timing issues, save phoneNumber and message locally
399  String phoneNumber = this.phoneNumber;
400  String message = this.message;
401 
402  // If sending by Google Voice, we need authentication
403  if (this.googleVoiceEnabled) {
404 
405  // If no authToken, get one before trying to send the message.
406  if (authToken == null) {
407  Log.i(TAG, "Need to get an authToken -- enqueing " + phoneNumber + " " + message);
408  boolean ok = pendingQueue.offer(phoneNumber + ":::" + message);
409 
410  // Try enqueuing the message
411  if (!ok) {
412  Toast.makeText(activity, "Pending message queue full. Can't send message", Toast.LENGTH_SHORT).show();
413  return;
414  }
415 
416  // If this is the first pending message, start authentication;
417  // otherwise a previous message will have started it.
418 
419  if (pendingQueue.size() == 1)
420  new AsyncAuthenticate().execute();
421 
422  // If we already have the authToken, just send the message.
423  } else {
424  Log.i(TAG, "Creating AsyncSendMessage");
425  new AsyncSendMessage().execute(phoneNumber, message);
426  }
427 
428  // We're sending via built-in Sms
429  } else {
430  Log.i(TAG, "Sending via SMS");
431  this.sendViaSms("SendMessage");
432  }
433  }
434 
438  private void processPendingQueue() {
439  while (pendingQueue.size() != 0) {
440  String entry = (String)pendingQueue.remove();
441  String phoneNumber = entry.substring(0,entry.indexOf(":::"));
442  String message = entry.substring(entry.indexOf(":::") + 3);
443  Log.i(TAG, "Sending queued message " + phoneNumber + " " + message);
444  new AsyncSendMessage().execute(phoneNumber, message);
445  }
446  }
447 
456  @SimpleEvent
457  public static void MessageReceived(String number, String messageText) {
458  if (receivingEnabled > ComponentConstants.TEXT_RECEIVING_OFF) {
459  Log.i(TAG, "MessageReceived from " + number + ":" + messageText);
460  if (EventDispatcher.dispatchEvent(component, "MessageReceived", number, messageText)) {
461  Log.i(TAG, "Dispatch successful");
462  } else {
463  Log.i(TAG, "Dispatch failed, caching");
464  synchronized (cacheLock) {
465  addMessageToCache(activity, number, messageText);
466  }
467  }
468  }
469  }
470 
482  description = "If true, then SendMessage will attempt to send messages over Wifi " +
483  "using Google Voice. This requires that the Google Voice app must be installed " +
484  "and set up on the phone or tablet, with a Google Voice account. If GoogleVoiceEnabled " +
485  "is false, the device must have phone and texting service in order to send or " +
486  "receive messages with this component.")
487  public boolean GoogleVoiceEnabled() {
488  return googleVoiceEnabled;
489  }
490 
498  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "False")
499  @SimpleProperty()
500  @UsesBroadcastReceivers(receivers = {
501  @ReceiverElement(name = "com.google.appinventor.components.runtime.util.SmsBroadcastReceiver",
502  intentFilters = {
503  @IntentFilterElement(actionElements = {
504  @ActionElement(name = "com.google.android.apps.googlevoice.SMS_RECEIVED")
505  })
506  })
507  })
508  public void GoogleVoiceEnabled(boolean enabled) {
510  this.googleVoiceEnabled = enabled;
511  SharedPreferences prefs = activity.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
512  SharedPreferences.Editor editor = prefs.edit();
513  editor.putBoolean(PREF_GVENABLED, enabled);
514  editor.commit();
515  } else {
516  Toast.makeText(activity, "Sorry, your phone's system does not support this option.", Toast.LENGTH_LONG).show();
517  }
518  }
519 
531  description = "If set to 1 (OFF) no messages will be received. If set to 2 (FOREGROUND) or" +
532  "3 (ALWAYS) the component will respond to messages if it is running. If the " +
533  "app is not running then the message will be discarded if set to 2 " +
534  "(FOREGROUND). If set to 3 (ALWAYS) and the app is not running the phone will " +
535  "show a notification. Selecting the notification will bring up the app " +
536  "and signal the MessageReceived event. Messages received when the app " +
537  "is dormant will be queued, and so several MessageReceived events might " +
538  "appear when the app awakens. As an app developer, it would be a good " +
539  "idea to give your users control over this property, so they can make " +
540  "their phones ignore text messages when your app is installed.")
541  public int ReceivingEnabled() {
542  return receivingEnabled;
543  }
544 
559  @UsesPermissions(Manifest.permission.RECEIVE_SMS)
561  alwaysSend = true,
562  defaultValue = "1") // Default is OFF (was FOREGROUND prior to Jan 2019)
563  @SimpleProperty()
564  @UsesBroadcastReceivers(receivers = {
565  @ReceiverElement(name = "com.google.appinventor.components.runtime.util.SmsBroadcastReceiver",
566  intentFilters = {
567  @IntentFilterElement(actionElements = {
568  @ActionElement(name = "android.provider.Telephony.SMS_RECEIVED"),
569  })
570  })
571  })
572  public void ReceivingEnabled(int enabled) {
573  if ((enabled < ComponentConstants.TEXT_RECEIVING_OFF) ||
575  container.$form().dispatchErrorOccurredEvent(this, "Texting",
577  return;
578  }
579 
580  Texting.receivingEnabled = enabled;
581  SharedPreferences prefs = activity.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
582  SharedPreferences.Editor editor = prefs.edit();
583  editor.putInt(PREF_RCVENABLED, enabled);
584  editor.remove(PREF_RCVENABLED_LEGACY); // Remove any legacy value
585  editor.commit();
586  if (enabled > ComponentConstants.TEXT_RECEIVING_OFF && !haveReceivePermission) {
587  requestReceiveSmsPermission("ReceivingEnabled");
588  }
589  }
590 
591  public static int isReceivingEnabled(Context context) {
592  SharedPreferences prefs = context.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
593  int retval = prefs.getInt(PREF_RCVENABLED, -1);
594  if (retval == -1) { // Fetch legacy value
595  if (prefs.getBoolean(PREF_RCVENABLED_LEGACY, true))
596  return ComponentConstants.TEXT_RECEIVING_FOREGROUND; // Foreground
597  else
599  }
600  return retval;
601  }
602 
612  public static SmsMessage[] getMessagesFromIntent(
613  Intent intent) {
614  Object[] messages = (Object[]) intent.getSerializableExtra("pdus");
615  byte[][] pduObjs = new byte[messages.length][];
616 
617  for (int i = 0; i < messages.length; i++) {
618  pduObjs[i] = (byte[]) messages[i];
619  }
620  byte[][] pdus = new byte[pduObjs.length][];
621  int pduCount = pdus.length;
622  SmsMessage[] msgs = new SmsMessage[pduCount];
623  for (int i = 0; i < pduCount; i++) {
624  pdus[i] = pduObjs[i];
625  msgs[i] = SmsMessage.createFromPdu(pdus[i]);
626  }
627  return msgs;
628  }
629 
634  private void processCachedMessages() {
635  String[] messagelist = null;
636  synchronized (cacheLock) {
637  messagelist = retrieveCachedMessages();
638  }
639  if (messagelist == null)
640  return;
641  Log.i(TAG, "processing " + messagelist.length + " cached messages ");
642 
643  for (int k = 0; k < messagelist.length; k++) {
644  String phoneAndMessage = messagelist[k];
645  Log.i(TAG, "Message + " + k + " " + phoneAndMessage);
646 
647  int delim = phoneAndMessage.indexOf(":");
648 
649  // If receiving is not enabled, messages are not dispatched
650  if ((receivingEnabled > ComponentConstants.TEXT_RECEIVING_OFF) && delim != -1) {
651  MessageReceived(phoneAndMessage.substring(0,delim),
652  phoneAndMessage.substring(delim+1));
653  }
654  }
655  }
656 
662  private String[] retrieveCachedMessages() {
663  Log.i(TAG, "Retrieving cached messages");
664  String cache = "";
665  try {
666  byte[] bytes = FileUtil.readFile(form, CACHE_FILE);
667  cache = new String(bytes);
668  activity.deleteFile(CACHE_FILE);
669  messagesCached = 0;
670  Log.i(TAG, "Retrieved cache " + cache);
671  } catch (FileNotFoundException e) {
672  Log.e(TAG, "No Cache file found -- this is not (usually) an error");
673  return null;
674  } catch (IOException e) {
675  Log.e(TAG, "I/O Error reading from cache file");
676  e.printStackTrace();
677  return null;
678  }
679  String messagelist[] = cache.split(MESSAGE_DELIMITER);
680  return messagelist;
681  }
682 
687  public static boolean isRunning() {
688  return isRunning;
689  }
690 
695  public static int getCachedMsgCount() {
696  return messagesCached;
697  }
698 
702  @Override
703  public void onResume() {
704  Log.i(TAG, "onResume()");
705  isRunning = true;
706  if (isInitialized) {
707  processCachedMessages();
708  NotificationManager nm = (NotificationManager) activity.getSystemService(Context.NOTIFICATION_SERVICE);
710  }
711  }
712 
716  @Override
717  public void onPause() {
718  Log.i(TAG, "onPause()");
719  isRunning = false;
720  }
721 
727  public static void handledReceivedMessage(Context context, String phone, String msg) {
728  if (isRunning) {
729  MessageReceived(phone, msg);
730  } else {
731  synchronized (cacheLock) {
732  addMessageToCache(context, phone, msg);
733  }
734  }
735  }
736 
743  private static void addMessageToCache(Context context, String phone, String msg) {
744  try {
745  String cachedMsg = phone + ":" + msg + MESSAGE_DELIMITER;
746  Log.i(TAG, "Caching " + cachedMsg);
747  FileOutputStream fos = context.openFileOutput(CACHE_FILE, Context.MODE_APPEND);
748  fos.write(cachedMsg.getBytes());
749  fos.close();
750  ++messagesCached;
751  Log.i(TAG, "Cached " + cachedMsg);
752  } catch (FileNotFoundException e) {
753  Log.e(TAG, "File not found error writing to cache file");
754  e.printStackTrace();
755  } catch (IOException e) {
756  Log.e(TAG, "I/O Error writing to cache file");
757  e.printStackTrace();
758  }
759  }
760 
761  @Override
762  public void resultReturned(int requestCode, int resultCode, Intent data) {
763  if (requestCode == TEXTING_REQUEST_CODE) {
764  handleSentMessage(form, null, resultCode, data == null ? "" :
765  data.getStringExtra("sms_body"));
766  form.unregisterForActivityResult(this);
767  }
768  }
769 
778  class GoogleVoiceUtil {
779  private final int MAX_REDIRECTS = 5;
780  private static final String COOKIES_HEADER = "Set-Cookie";
781 
782  String general; // Google's GV page
783  String rnrSEE; // Value that passed into SMS's
784  String authToken;
785  int redirectCounter;
786  private boolean isInitialized;
787  CookieManager cookies = new CookieManager();
788 
793  public GoogleVoiceUtil(String authToken) {
794  Log.i(TAG, "Creating GV Util");
795  this.authToken = authToken;
796  try {
797  this.general = getGeneral();
798  Log.i(TAG, "general = " + this.general);
799  setRNRSEE();
800  isInitialized = true; // If we make it to here, we're good to go
801  } catch (IOException e) {
802  e.printStackTrace();
803  }
804  }
805 
806  public boolean isInitialized() {
807  return isInitialized;
808  }
809 
815  private String sendGvSms(String smsData) {
816  Log.i(TAG, "sendGvSms()");
817  StringBuilder response = new StringBuilder();
818  try {
819  // Add the RNR_SE to the message
820  smsData += "&" + URLEncoder.encode("_rnr_se", UTF8) + "=" + URLEncoder.encode(rnrSEE, UTF8);
821  Log.i(TAG, "smsData = " + smsData);
822  URL smsUrl = new URL(GV_SMS_SEND_URL);
823 
824  HttpURLConnection smsConn = (HttpURLConnection) smsUrl.openConnection();
825  smsConn.setRequestProperty( "Authorization", "GoogleLogin auth=" + authToken );
826  smsConn.setRequestProperty("User-agent", USER_AGENT);
827  setCookies(smsConn);
828  smsConn.setDoOutput(true);
829  smsConn.setConnectTimeout(SERVER_TIMEOUT_MS);
830 
831  Log.i(TAG, "sms request = " + smsConn);
832  OutputStreamWriter callwr = new OutputStreamWriter(smsConn.getOutputStream());
833  callwr.write(smsData);
834  callwr.flush();
835 
836  processCookies(smsConn);
837  BufferedReader callrd = new BufferedReader(new InputStreamReader(smsConn.getInputStream()));
838 
839  String line;
840  while ((line = callrd.readLine()) != null) {
841  response.append(line);
842  response.append("\n"); // HTTP uses \r\n, but Android is built on Linux, so use Unix line endings
843  }
844  Log.i(TAG, "sendGvSms: Sent SMS, response = " + response);
845 
846  callwr.close();
847  callrd.close();
848 
849  if (response.length() == 0) {
850  throw new IOException("No Response Data Received.");
851  } else {
852  return response.toString();
853  }
854  } catch (IOException e) {
855  Log.i(TAG, "IO Error on Send " + e.getMessage(), e);
856  return "IO Error Message not sent";
857  }
858  }
859 
860 
870  public String getGeneral() throws IOException {
871  Log.i(TAG, "getGeneral()");
872  return get(GV_URL);
873  }
874 
882  private void setRNRSEE() throws IOException {
883  Log.i(TAG, "setRNRSEE()");
884  if (general != null) {
885  if(general.contains("'_rnr_se': '")) {
886  String p1 = general.split("'_rnr_se': '", 2)[1];
887  rnrSEE = p1.split("',", 2)[0];
888  Log.i(TAG,"Successfully Received rnr_se.");
889  p1 = null;
890  } else {
891  Log.i(TAG, "Answer did not contain rnr_se! "+ general);
892  throw new IOException("Answer did not contain rnr_se! "+ general);
893  }
894  } else {
895  Log.i(TAG,"setRNRSEE(): Answer was null!");
896  throw new IOException("setRNRSEE(): Answer was null!");
897  }
898  }
899 
906  void setCookies(HttpURLConnection conn) {
907  if (cookies.getCookieStore().getCookies().size() > 0) {
908  conn.setRequestProperty("Cookie", TextUtils.join(";", cookies.getCookieStore().getCookies()));
909  }
910  }
911 
918  void processCookies(HttpURLConnection conn) {
919  List<String> cookiesHeader = conn.getHeaderFields().get(COOKIES_HEADER);
920  if (cookiesHeader != null) {
921  for (String cookie : cookiesHeader) {
922  cookies.getCookieStore().add(null, HttpCookie.parse(cookie).get(0));
923  }
924  }
925  }
926 
936  String get(String urlString) throws IOException {
937  URL url = new URL(urlString);
938  HttpURLConnection conn = (HttpURLConnection) url.openConnection();
939  int responseCode = 0;
940  try {
941  conn.setRequestProperty( "Authorization", "GoogleLogin auth="+authToken );
942  conn.setRequestProperty("User-agent", USER_AGENT);
943  conn.setInstanceFollowRedirects(false); // will follow redirects of same protocol http to http, but does not follow from http to https for example if set to true
944  setCookies(conn);
945 
946  // Get the response
947  conn.connect();
948  responseCode = conn.getResponseCode();
949  Log.i(TAG, urlString + " - " + conn.getResponseMessage());
950  } catch (Exception e) {
951  throw new IOException(urlString + " : " + conn.getResponseMessage() + "("+responseCode+") : IO Error.");
952  }
953 
954  processCookies(conn);
955 
956  InputStream is;
957  if(responseCode==200) {
958  is = conn.getInputStream();
959  } else if(responseCode==HttpURLConnection.HTTP_MOVED_PERM || responseCode==HttpURLConnection.HTTP_MOVED_TEMP || responseCode==HttpURLConnection.HTTP_SEE_OTHER || responseCode==307) {
960  redirectCounter++;
961  if(redirectCounter > MAX_REDIRECTS) {
962  redirectCounter = 0;
963  throw new IOException(urlString + " : " + conn.getResponseMessage() + "("+responseCode+") : Too many redirects. exiting.");
964  }
965  String location = conn.getHeaderField("Location");
966  if(location!=null && !location.equals("")) {
967  System.out.println(urlString + " - " + responseCode + " - new URL: " + location);
968  return get(location);
969  } else {
970  throw new IOException(urlString + " : " + conn.getResponseMessage() + "("+responseCode+") : Received moved answer but no Location. exiting.");
971  }
972  } else {
973  is = conn.getErrorStream();
974  }
975  redirectCounter = 0;
976 
977  if(is==null) {
978  throw new IOException(urlString + " : " + conn.getResponseMessage() + "("+responseCode+") : InputStream was null : exiting.");
979  }
980 
981  String result="";
982  try {
983  // Get the response
984  BufferedReader rd = new BufferedReader(new InputStreamReader(is));
985 
986  StringBuffer sb = new StringBuffer();
987  String line;
988  while ((line = rd.readLine()) != null) {
989  sb.append(line + "\n\r");
990  }
991  rd.close();
992  result = sb.toString();
993  } catch (Exception e) {
994  throw new IOException(urlString + " - " + conn.getResponseMessage() + "("+responseCode+") - " +e.getLocalizedMessage());
995  }
996  return result;
997  }
998  }
999 
1000 
1017  private synchronized void handleSentMessage(Context context,
1018  BroadcastReceiver receiver, int resultCode, String smsMsg) {
1019  switch (resultCode) {
1020  case Activity.RESULT_OK:
1021  Log.i(TAG, "Received OK, msg:" + smsMsg);
1022  Toast.makeText(activity, "Message sent", Toast.LENGTH_SHORT).show();
1023  break;
1024  case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
1025  Log.e(TAG, "Received generic failure, msg:" + smsMsg);
1026  Toast.makeText(activity, "Generic failure: message not sent", Toast.LENGTH_SHORT).show();
1027  break;
1028  case SmsManager.RESULT_ERROR_NO_SERVICE:
1029  Log.e(TAG, "Received no service error, msg:" + smsMsg);
1030  Toast.makeText(activity, "No Sms service available. Message not sent.", Toast.LENGTH_SHORT).show();
1031  break;
1032  case SmsManager.RESULT_ERROR_NULL_PDU:
1033  Log.e(TAG, "Received null PDU error, msg:" + smsMsg);
1034  Toast.makeText(activity, "Received null PDU error. Message not sent.", Toast.LENGTH_SHORT).show();
1035  break;
1036  case SmsManager.RESULT_ERROR_RADIO_OFF:
1037  Log.e(TAG, "Received radio off error, msg:" + smsMsg);
1038  Toast.makeText(activity, "Could not send SMS message: radio off.", Toast.LENGTH_LONG).show();
1039  break;
1040  }
1041  }
1042 
1047  private void sendViaSms(final String caller) {
1048  Log.i(TAG, "Sending via built-in Sms");
1049 
1050  // Need to make sure we have the SEND_SMS permission
1051  if (!havePermission) {
1052  final Form form = container.$form();
1053  final Texting me = this;
1054  form.runOnUiThread(new Runnable() {
1055  @Override
1056  public void run() {
1057  form.askPermission(Manifest.permission.SEND_SMS,
1058  new PermissionResultHandler() {
1059  @Override
1060  public void HandlePermissionResponse(String permission, boolean granted) {
1061  if (granted) {
1062  me.havePermission = true;
1063  me.sendViaSms(caller);
1064  } else {
1065  form.dispatchPermissionDeniedEvent(me, caller, Manifest.permission.SEND_SMS);
1066  }
1067  }
1068  });
1069  }
1070  });
1071  return;
1072  }
1073 
1074  ArrayList<String> parts = smsManager.divideMessage(message);
1075  int numParts = parts.size();
1076  ArrayList<PendingIntent> pendingIntents = new ArrayList<PendingIntent>();
1077  for (int i = 0; i < numParts; i++)
1078  pendingIntents.add(PendingIntent.getBroadcast(activity, 0, new Intent(SENT), 0));
1079 
1080  // Receiver for when the SMS is sent
1081  BroadcastReceiver sendReceiver = new BroadcastReceiver() {
1082  @Override
1083  public synchronized void onReceive(Context arg0, Intent arg1) {
1084  try {
1085  handleSentMessage(arg0, null, getResultCode(), message);
1086  activity.unregisterReceiver(this);
1087  } catch (Exception e) {
1088  Log.e("BroadcastReceiver",
1089  "Error in onReceive for msgId " + arg1.getAction());
1090  Log.e("BroadcastReceiver", e.getMessage());
1091  e.printStackTrace();
1092  }
1093  }
1094  };
1095  // This may result in an error -- a "sent" or "error" message will be displayed
1096  activity.registerReceiver(sendReceiver, new IntentFilter(SENT));
1097  smsManager.sendMultipartTextMessage(phoneNumber, null, parts, pendingIntents, null);
1098  }
1099 
1100  private void requestReceiveSmsPermission(final String caller) {
1101  form.runOnUiThread(new Runnable() {
1102  @Override
1103  public void run() {
1104  form.askPermission(Manifest.permission.RECEIVE_SMS,
1105  new PermissionResultHandler() {
1106  @Override
1107  public void HandlePermissionResponse(String permission, boolean granted) {
1108  if (granted) {
1109  haveReceivePermission = true;
1110  } else {
1111  form.dispatchPermissionDeniedEvent(Texting.this, caller, Manifest.permission.RECEIVE_SMS);
1112  }
1113  }
1114  });
1115  }
1116  });
1117 
1118  }
1119 
1123  class AsyncAuthenticate extends AsyncTask<Void, Void, String> {
1124 
1125  @Override
1126  protected String doInBackground(Void... arg0) {
1127  Log.i(TAG, "Authenticating");
1128 
1129  // Get and return the authtoken
1130  return new OAuth2Helper().getRefreshedAuthToken(activity, GV_SERVICE);
1131  }
1132 
1136  @Override
1137  protected void onPostExecute(String result) {
1138  Log.i(TAG, "authToken = " + result);
1139  authToken = result;
1140 
1141  Toast.makeText(activity, "Finished authentication", Toast.LENGTH_SHORT).show();
1142 
1143  // Send any pending messages
1144  processPendingQueue();
1145  }
1146  }
1147 
1155  class AsyncSendMessage extends AsyncTask<String, Void, String> {
1156 
1164  @Override
1165  protected String doInBackground(String... args) {
1166  String phoneNumber = args[0];
1167  String message = args[1];
1168  String response = "";
1169  String smsData = "";
1170 
1171  Log.i(TAG, "Async sending phoneNumber = " + phoneNumber + " message = " + message);
1172 
1173  try {
1174 
1175  // Set up the smsMessage for Google Voice
1176  smsData =
1177  URLEncoder.encode("phoneNumber", UTF8) + "=" + URLEncoder.encode(phoneNumber, UTF8) +
1178  "&" + URLEncoder.encode("text", UTF8) + "=" + URLEncoder.encode(message, UTF8);
1179 
1180  if (gvHelper == null) {
1181  gvHelper = new GoogleVoiceUtil(authToken);
1182  }
1183  if (gvHelper.isInitialized()) {
1184  response = gvHelper.sendGvSms(smsData);
1185  Log.i(TAG, "Sent SMS, response = " + response);
1186  } else {
1187  return "IO Error: unable to create GvHelper";
1188  }
1189  } catch (Exception e) {
1190  e.printStackTrace();
1191  }
1192  return response;
1193  }
1194 
1195  @Override
1196  protected void onPostExecute(String result) {
1197  super.onPostExecute(result);
1198 
1199  JSONObject json;
1200  boolean ok = false;
1201  int code = 0;
1202  try {
1203  json = new JSONObject(result);
1204  ok = json.getBoolean("ok");
1205  code = json.getJSONObject("data").getInt("code");
1206  } catch (JSONException e) {
1207  // TODO Auto-generated catch block
1208  e.printStackTrace();
1209  }
1210  if (ok)
1211  Toast.makeText(activity, "Message sent", Toast.LENGTH_SHORT).show();
1212  else if (code == 58)
1213  Toast.makeText(activity, "Errcode 58: SMS limit reached", Toast.LENGTH_SHORT).show();
1214  else if (result.contains("IO Error"))
1215  Toast.makeText(activity, result, Toast.LENGTH_SHORT).show();
1216  }
1217  }
1218 
1222  @Override
1223  public void onStop() {
1224  SharedPreferences prefs = activity.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
1225  SharedPreferences.Editor editor = prefs.edit();
1226  editor.putInt(PREF_RCVENABLED, receivingEnabled);
1227  editor.putBoolean(PREF_GVENABLED, googleVoiceEnabled);
1228  editor.commit();
1229  }
1230 
1231  @Override
1232  public void onDelete() {
1233  form.unregisterForActivityResult(this);
1234  }
1235 
1236 }
1237 
com.google.appinventor.components.runtime.EventDispatcher
Definition: EventDispatcher.java:22
com.google.appinventor.components.annotations.SimpleFunction
Definition: SimpleFunction.java:23
com.google.appinventor.components.runtime.util.SmsBroadcastReceiver.NOTIFICATION_ID
static final int NOTIFICATION_ID
Definition: SmsBroadcastReceiver.java:58
com.google.appinventor.components.annotations.UsesLibraries
Definition: UsesLibraries.java:21
com.google.appinventor.components.runtime.util.SmsBroadcastReceiver
Definition: SmsBroadcastReceiver.java:55
com.google.appinventor.components.runtime.util.ErrorMessages
Definition: ErrorMessages.java:17
com.google.appinventor.components.runtime.Texting.Texting
Texting(ComponentContainer container)
Definition: Texting.java:254
com.google.appinventor.components.runtime.Form.registerForOnInitialize
void registerForOnInitialize(OnInitializeListener component)
Definition: Form.java:754
com.google.appinventor.components.runtime.util
-*- mode: java; c-basic-offset: 2; -*-
Definition: AccountChooser.java:7
com.google.appinventor.components.runtime.util.FileUtil
Definition: FileUtil.java:37
com.google.appinventor.components.common.YaVersion
Definition: YaVersion.java:14
com.google.appinventor.components.runtime.Texting.SendMessageDirect
void SendMessageDirect()
Definition: Texting.java:395
com.google.appinventor.components.common.ComponentConstants.TEXT_RECEIVING_ALWAYS
static final int TEXT_RECEIVING_ALWAYS
Definition: ComponentConstants.java:68
com.google.appinventor.components.annotations.DesignerProperty
Definition: DesignerProperty.java:25
com.google.appinventor.components.runtime.Form.registerForOnResume
void registerForOnResume(OnResumeListener component)
Definition: Form.java:740
com.google.appinventor.components.common.ComponentConstants.TEXT_RECEIVING_FOREGROUND
static final int TEXT_RECEIVING_FOREGROUND
Definition: ComponentConstants.java:67
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_STRING
static final String PROPERTY_TYPE_STRING
Definition: PropertyTypeConstants.java:237
com.google.appinventor.components
com.google.appinventor.components.runtime.Texting.onResume
void onResume()
Definition: Texting.java:703
com.google.appinventor.components.runtime.Texting
Definition: Texting.java:169
com.google.appinventor.components.runtime.Texting.GoogleVoiceEnabled
void GoogleVoiceEnabled(boolean enabled)
Definition: Texting.java:508
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN
static final String PROPERTY_TYPE_BOOLEAN
Definition: PropertyTypeConstants.java:35
com.google.appinventor.components.annotations.androidmanifest.ActionElement
Definition: ActionElement.java:31
com.google.appinventor.components.annotations.DesignerComponent
Definition: DesignerComponent.java:22
com.google.appinventor.components.runtime.Texting.onDelete
void onDelete()
Definition: Texting.java:1232
com.google.appinventor.components.annotations.SimpleEvent
Definition: SimpleEvent.java:20
com.google.appinventor.components.annotations.PropertyCategory.BEHAVIOR
BEHAVIOR
Definition: PropertyCategory.java:15
com.google.appinventor.components.annotations.androidmanifest.ReceiverElement
Definition: ReceiverElement.java:34
com.google.appinventor.components.runtime.util.SdkLevel.LEVEL_ECLAIR
static final int LEVEL_ECLAIR
Definition: SdkLevel.java:22
com.google.appinventor.components.runtime.util.OAuth2Helper
Definition: OAuth2Helper.java:71
com.google.appinventor.components.runtime.Texting.handledReceivedMessage
static void handledReceivedMessage(Context context, String phone, String msg)
Definition: Texting.java:727
com.google.appinventor.components.annotations.androidmanifest.IntentFilterElement
Definition: IntentFilterElement.java:30
com.google.appinventor.components.runtime.Texting.resultReturned
void resultReturned(int requestCode, int resultCode, Intent data)
Definition: Texting.java:762
com.google.appinventor.components.runtime.Texting.isReceivingEnabled
static int isReceivingEnabled(Context context)
Definition: Texting.java:591
com.google.appinventor.components.annotations.UsesPermissions
Definition: UsesPermissions.java:21
com.google.appinventor.components.runtime.OnResumeListener
Definition: OnResumeListener.java:14
com.google.appinventor.components.runtime.Texting.isRunning
static boolean isRunning()
Definition: Texting.java:687
com.google.appinventor.components.runtime.Texting.onPause
void onPause()
Definition: Texting.java:717
com.google.appinventor.components.annotations.androidmanifest
Definition: ActionElement.java:6
com.google.appinventor.components.runtime.EventDispatcher.dispatchEvent
static boolean dispatchEvent(Component component, String eventName, Object...args)
Definition: EventDispatcher.java:188
com.google.appinventor.components.runtime.util.OnInitializeListener
Definition: OnInitializeListener.java:13
com.google.appinventor.components.runtime.AndroidNonvisibleComponent
Definition: AndroidNonvisibleComponent.java:17
com.google.appinventor.components.runtime.util.SdkLevel
Definition: SdkLevel.java:19
com.google.appinventor.components.runtime.OnPauseListener
Definition: OnPauseListener.java:14
com.google.appinventor.components.annotations.UsesBroadcastReceivers
Definition: UsesBroadcastReceivers.java:24
com.google.appinventor.components.annotations.SimpleProperty
Definition: SimpleProperty.java:23
com.google.appinventor.components.runtime.Texting.Message
String Message()
Definition: Texting.java:367
com.google.appinventor.components.annotations.PropertyCategory
Definition: PropertyCategory.java:13
com.google.appinventor.components.runtime.ComponentContainer
Definition: ComponentContainer.java:16
com.google.appinventor.components.common.ComponentConstants.TEXT_RECEIVING_OFF
static final int TEXT_RECEIVING_OFF
Definition: ComponentConstants.java:66
com.google.appinventor.components.runtime.util.SdkLevel.getLevel
static int getLevel()
Definition: SdkLevel.java:45
com.google.appinventor.components.runtime
Copyright 2009-2011 Google, All Rights reserved.
Definition: AccelerometerSensor.java:8
com.google.appinventor.components.runtime.Component
Definition: Component.java:17
com.google.appinventor.components.runtime.Deleteable
Definition: Deleteable.java:15
com.google.appinventor.components.runtime.Texting.ReceivingEnabled
void ReceivingEnabled(int enabled)
Definition: Texting.java:572
com.google.appinventor.components.common
Definition: ComponentCategory.java:7
com.google.appinventor.components.runtime.Texting.MessageReceived
static void MessageReceived(String number, String messageText)
Definition: Texting.java:457
com.google.appinventor.components.common.ComponentCategory
Definition: ComponentCategory.java:48
com.google.appinventor.components.runtime.OnStopListener
Definition: OnStopListener.java:15
com.google.appinventor.components.runtime.Texting.SendMessage
void SendMessage()
Definition: Texting.java:375
com.google.appinventor.components.runtime.Form.dispatchErrorOccurredEvent
void dispatchErrorOccurredEvent(final Component component, final String functionName, final int errorNumber, final Object... messageArgs)
Definition: Form.java:1011
com.google.appinventor.components.runtime.ActivityResultListener
Definition: ActivityResultListener.java:16
com.google.appinventor.components.runtime.Form.registerForOnPause
void registerForOnPause(OnPauseListener component)
Definition: Form.java:780
com.google.appinventor.components.runtime.Form.registerForOnStop
void registerForOnStop(OnStopListener component)
Definition: Form.java:793
com.google.appinventor.components.runtime.Texting.onInitialize
void onInitialize()
Definition: Texting.java:302
com.google.appinventor.components.annotations.SimpleObject
Definition: SimpleObject.java:23
com.google
com
com.google.appinventor.components.runtime.Texting.getCachedMsgCount
static int getCachedMsgCount()
Definition: Texting.java:695
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_BAD_VALUE_FOR_TEXT_RECEIVING
static final int ERROR_BAD_VALUE_FOR_TEXT_RECEIVING
Definition: ErrorMessages.java:163
com.google.appinventor.components.runtime.ComponentContainer.$form
Form $form()
com.google.appinventor.components.runtime.ComponentContainer.$context
Activity $context()
com.google.appinventor.components.common.ComponentConstants
Definition: ComponentConstants.java:13
com.google.appinventor.components.runtime.Texting.getMessagesFromIntent
static SmsMessage[] getMessagesFromIntent(Intent intent)
Definition: Texting.java:612
com.google.appinventor.components.common.PropertyTypeConstants.PROPERTY_TYPE_TEXT_RECEIVING
static final String PROPERTY_TYPE_TEXT_RECEIVING
Definition: PropertyTypeConstants.java:284
com.google.appinventor.components.common.PropertyTypeConstants
Definition: PropertyTypeConstants.java:14
com.google.appinventor.components.annotations
com.google.appinventor
com.google.appinventor.components.runtime.Texting.onStop
void onStop()
Definition: Texting.java:1223
com.google.appinventor.components.runtime.Texting.Initialize
void Initialize()
Definition: Texting.java:312