// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2016 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import com.google.appinventor.components.runtime.util.OAuth2Helper;
import com.google.appinventor.components.runtime.util.OnInitializeListener;
import com.google.appinventor.components.runtime.util.SdkLevel;
import com.google.appinventor.components.runtime.util.SmsBroadcastReceiver;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.annotations.UsesBroadcastReceivers;
import com.google.appinventor.components.annotations.androidmanifest.ActionElement;
import com.google.appinventor.components.annotations.androidmanifest.IntentFilterElement;
import com.google.appinventor.components.annotations.androidmanifest.ReceiverElement;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.ComponentConstants;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import android.app.Activity;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.util.Log;
import android.widget.Toast;
/**
* A component capable of sending and receiving text messages via SMS.
* @author markf@google.com (Mark Friedman)
* @author ram8647@gmail.com (Ralph Morelli)
*/
@SuppressWarnings("deprecation")
@DesignerComponent(version = YaVersion.TEXTING_COMPONENT_VERSION,
description = "<p>A component that will, when the <code>SendMessage</code> method is " +
"called, send the text message specified in the <code>Message</code> " +
"property to the phone number specified in the <code>PhoneNumber</code> " +
"property.</p> " +
"<p>If the <code>ReceivingEnabled</code> property is set to 1 messages " +
"will <b>not</b> be received. If <code>ReceivingEnabled</code> is set " +
"to 2 messages will be received only when the application is " +
"running. Finally if <code>ReceivingEnabled</code> is set to 3, " +
"messages will be received when the application is running <b>and</b> " +
"when the application is not running they will be queued and a " +
"notification displayed to the user.</p> " +
"<p>When a message arrives, the <code>MessageReceived</code> event is " +
"raised and provides the sending number and message.</p> " +
"<p> An app that includes this component will receive messages even " +
"when it is in the background (i.e. when it's not visible on the " +
"screen) and, moreso, even if the app is not running, so long as it's " +
"installed on the phone. If the phone receives a text message when the " +
"app is not in the foreground, the phone will show a notification in " +
"the notification bar. Selecting the notification will bring up the " +
"app. As an app developer, you'll probably want to give your users the " +
"ability to control ReceivingEnabled so that they can make the phone " +
"ignore text messages.</p> " +
"<p>If the GoogleVoiceEnabled property is true, messages can be sent " +
"over Wifi using Google Voice. This option requires that the user have " +
"a Google Voice account and that the mobile Voice app is installed on " +
"the phone. The Google Voice option works only on phones that support " +
"Android 2.0 (Eclair) or higher.</p> " +
"<p>To specify the phone number (e.g., 650-555-1212), set the " +
"<code>PhoneNumber</code> property to a Text string with the specified " +
"digits (e.g., 6505551212). Dashes, dots, and parentheses may be " +
"included (e.g., (650)-555-1212) but will be ignored; spaces may not be " +
"included.</p> " +
"<p>Another way for an app to specify a phone number would be to " +
"include a <code>PhoneNumberPicker</code> component, which lets the " +
"users select a phone numbers from the ones stored in the the phone's " +
"contacts.</p>",
category = ComponentCategory.SOCIAL,
nonVisible = true,
iconName = "images/texting.png")
@SimpleObject
@UsesPermissions(permissionNames =
"android.permission.RECEIVE_SMS, android.permission.SEND_SMS, " +
"com.google.android.apps.googlevoice.permission.RECEIVE_SMS, " +
"com.google.android.apps.googlevoice.permission.SEND_SMS, " +
"android.permission.ACCOUNT_MANAGER, android.permission.MANAGE_ACCOUNTS, " +
"android.permission.GET_ACCOUNTS, android.permission.USE_CREDENTIALS")
@UsesLibraries(libraries =
"google-api-client-beta.jar," +
"google-api-client-android2-beta.jar," +
"google-http-client-beta.jar," +
"google-http-client-android2-beta.jar," +
"google-http-client-android3-beta.jar," +
"google-oauth-client-beta.jar," +
"guava-14.0.1.jar")
@UsesBroadcastReceivers(receivers = {
@ReceiverElement(name = "com.google.appinventor.components.runtime.util.SmsBroadcastReceiver",
intentFilters = {
@IntentFilterElement(actionElements = {
@ActionElement(name = "android.provider.Telephony.SMS_RECEIVED"),
@ActionElement(name = "com.google.android.apps.googlevoice.SMS_RECEIVED")
})
})
})
public class Texting extends AndroidNonvisibleComponent
implements Component, OnResumeListener, OnPauseListener, OnInitializeListener, OnStopListener {
public static final String TAG = "Texting Component";
public static final String SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED";
public static final String GV_SMS_RECEIVED = "com.google.android.apps.googlevoice.SMS_RECEIVED";
public static final String PHONE_NUMBER_TAG = "com.google.android.apps.googlevoice.PHONE_NUMBER";
public static final String MESSAGE_TAG = "com.google.android.apps.googlevoice.TEXT";
public static final String TELEPHONY_INTENT_FILTER = "android.provider.Telephony.SMS_RECEIVED";
public static final String GV_INTENT_FILTER = "com.google.android.apps.googlevoice.SMS_RECEIVED";
public static final String GV_PACKAGE_NAME = "com.google.android.apps.googlevoice";
public static final String GV_SMS_SEND_URL = "https://www.google.com/voice/b/0/sms/send/";
public static final String GV_URL = "https://www.google.com/voice/b/0";
// Meta data key and value that identify an app for handling incoming SMS
// Used by Texting component
public static final String META_DATA_SMS_KEY = "sms_handler_component";
public static final String META_DATA_SMS_VALUE = "Texting";
// private static final String GV_PHONES_INFO_URL = "https://www.google.com/voice/b/0/settings/tab/phones";
private static final String GV_SERVICE = "grandcentral";
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";
private static final int SERVER_TIMEOUT_MS = 30000;
private static final String SENT = "SMS_SENT";
private static final String UTF8 = "UTF-8";
private static final String MESSAGE_DELIMITER = "\u0001";
private static final String PREF_GVENABLED = "gvenabled"; // Boolean flag for GV is enabled
private static final String PREF_RCVENABLED_LEGACY = "receiving"; // Is receiving enabled (Legacy boolean version)
private static final String PREF_RCVENABLED = "receiving2"; // Is receiving enabled
private static final String PREF_FILE = "TextingState"; // State of Texting component
// Google Voice oauth helper
private GoogleVoiceUtil gvHelper;
private static Activity activity;
private static Component component;
private String authToken;
// Indicates whether the component is receiving messages or not
private static int receivingEnabled = ComponentConstants.TEXT_RECEIVING_FOREGROUND;
private SmsManager smsManager;
// The phone number to send the text message to.
private String phoneNumber;
// The message to send
private String message;
// Whether or not Google Voice is enabled.
private boolean googleVoiceEnabled;
// No messages can be received until Initialized
private boolean isInitialized;
// True when resumed and false when paused.
// Messages are cached when app is not running
private static boolean isRunning;
// Cache file for cached messages
private static final String CACHE_FILE = "textingmsgcache";
private static int messagesCached;
private static Object cacheLock = new Object();
//Stores up to 50 pending messages awaiting authentication
private Queue<String> pendingQueue = new ConcurrentLinkedQueue<String>();
private ComponentContainer container; // Need this for error reporting
/**
* Creates a new TextMessage component.
*
* @param container ignored (because this is a non-visible component)
*/
public Texting(ComponentContainer container) {
super(container.$form());
Log.d(TAG, "Texting constructor");
this.container = container;
Texting.component = (Texting)this;
activity = container.$context();
SharedPreferences prefs = activity.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
if (prefs != null) {
receivingEnabled = prefs.getInt(PREF_RCVENABLED, -1);
if (receivingEnabled == -1) {
if (prefs.getBoolean(PREF_RCVENABLED_LEGACY, true)) {
receivingEnabled = ComponentConstants.TEXT_RECEIVING_FOREGROUND;
} else {
receivingEnabled = ComponentConstants.TEXT_RECEIVING_OFF;
}
}
googleVoiceEnabled = prefs.getBoolean(PREF_GVENABLED, false);
Log.i(TAG, "Starting with receiving Enabled=" + receivingEnabled + " GV enabled=" + googleVoiceEnabled);
} else {
receivingEnabled = ComponentConstants.TEXT_RECEIVING_FOREGROUND;
googleVoiceEnabled = false;
}
// Handles authenticating for GV feature. This sets the authToken.
if (googleVoiceEnabled)
new AsyncAuthenticate().execute();
smsManager = SmsManager.getDefault();
PhoneNumber("");
isInitialized = false; // Set true when the form is initialized and can dispatch
isRunning = false; // This will be set true in onResume and false in onPause
// Register this component for lifecycle callbacks
container.$form().registerForOnInitialize(this);
container.$form().registerForOnResume(this);
container.$form().registerForOnPause(this);
container.$form().registerForOnStop(this);
}
/**
* Callback from Form. No incoming messages can be processed through
* MessageReceived until the Form is initialized. Messages are cached
* until this method is called.
*/
@Override
public void onInitialize() {
Log.i(TAG, "onInitialize()");
isInitialized = true;
isRunning = true; // Added b/c REPL does not call onResume when starting Texting component
processCachedMessages();
NotificationManager nm = (NotificationManager) activity.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(SmsBroadcastReceiver.NOTIFICATION_ID);
}
/**
* Sets the phone number to send the text message to when the SendMessage function is called.
*
* @param phoneNumber a phone number to call
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
@SimpleProperty(category = PropertyCategory.BEHAVIOR)
public void PhoneNumber(String phoneNumber) {
Log.i(TAG, "PhoneNumber set: " + phoneNumber);
this.phoneNumber = phoneNumber;
}
/**
* The number that the message will be sent to when the SendMessage method is called. The
* number is a text string with the specified digits (e.g., 6505551212). Dashes, dots,
* and parentheses may be included (e.g., (650)-555-1212) but will be ignored; spaces
* should not be included.
*/
@SimpleProperty(category = PropertyCategory.BEHAVIOR,
description = "The number that the message will be sent to when the SendMessage method " +
"is called. The " +
"number is a text string with the specified digits (e.g., 6505551212). Dashes, dots, " +
"and parentheses may be included (e.g., (650)-555-1212) but will be ignored; spaces " +
"should not be included.")
public String PhoneNumber() {
return phoneNumber;
}
/**
* The text message to that will be sent when the SendMessage method is called.
*
* @param message the message to send when the SendMessage function is called.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = "")
@SimpleProperty(category = PropertyCategory.BEHAVIOR,
description = "The message that will be sent when the SendMessage method is called.")
public void Message(String message) {
Log.i(TAG, "Message set: " + message);
this.message = message;
}
/**
* The text message that will be sent when the SendMessage method is called.
*/
@SimpleProperty
public String Message() {
return message;
}
/**
* Send a text message
*/
@SimpleFunction
public void SendMessage() {
Log.i(TAG, "Sending message " + message + " to " + phoneNumber);
// To avoid possible timing issues, save phoneNumber and message locally
String phoneNumber = this.phoneNumber;
String message = this.message;
// If sending by Google Voice, we need authentication
if (this.googleVoiceEnabled) {
// If no authToken, get one before trying to send the message.
if (authToken == null) {
Log.i(TAG, "Need to get an authToken -- enqueing " + phoneNumber + " " + message);
boolean ok = pendingQueue.offer(phoneNumber + ":::" + message);
// Try enqueuing the message
if (!ok) {
Toast.makeText(activity, "Pending message queue full. Can't send message", Toast.LENGTH_SHORT).show();
return;
}
// If this is the first pending message, start authentication;
// otherwise a previous message will have started it.
if (pendingQueue.size() == 1)
new AsyncAuthenticate().execute();
// If we already have the authToken, just send the message.
} else {
Log.i(TAG, "Creating AsyncSendMessage");
new AsyncSendMessage().execute(phoneNumber, message);
}
// We're sending via built-in Sms
} else {
Log.i(TAG, "Sending via SMS");
this.sendViaSms();
}
}
/**
* Sends pending messages that have been queued awaiting authentication.
*/
private void processPendingQueue() {
while (pendingQueue.size() != 0) {
String entry = (String)pendingQueue.remove();
String phoneNumber = entry.substring(0,entry.indexOf(":::"));
String message = entry.substring(entry.indexOf(":::") + 3);
Log.i(TAG, "Sending queued message " + phoneNumber + " " + message);
new AsyncSendMessage().execute(phoneNumber, message);
}
}
/**
* Event that's raised when a text message is received by the phone.
*
*
* @param number the phone number that the text message was sent from.
* @param messageText the text of the message.
*/
@SimpleEvent
public static void MessageReceived(String number, String messageText) {
if (receivingEnabled > ComponentConstants.TEXT_RECEIVING_OFF) {
Log.i(TAG, "MessageReceived from " + number + ":" + messageText);
if (EventDispatcher.dispatchEvent(component, "MessageReceived", number, messageText)) {
Log.i(TAG, "Dispatch successful");
} else {
Log.i(TAG, "Dispatch failed, caching");
synchronized (cacheLock) {
addMessageToCache(activity, number, messageText);
}
}
}
}
/**
* If this property is true, then SendMessage will attempt to send messages using
* Google voice.
*
* @return 'true' or 'false' depending on whether you want to
* use Google Voice for sending/receiving messages.
*/
@SimpleProperty(category = PropertyCategory.BEHAVIOR,
description = "If true, then SendMessage will attempt to send messages over Wifi " +
"using Google Voice. This requires that the Google Voice app must be installed " +
"and set up on the phone or tablet, with a Google Voice account. If GoogleVoiceEnabled " +
"is false, the device must have phone and texting service in order to send or " +
"receive messages with this component.")
public boolean GoogleVoiceEnabled() {
return googleVoiceEnabled;
}
/**
* If this property is true, then SendMessage will attempt to send messages over
* WiFi, using Google voice.
*
* @param enabled Set to 'true' or 'false' depending on whether you want to
* use Google Voice to send/receive messages.
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, defaultValue = "False")
@SimpleProperty()
public void GoogleVoiceEnabled(boolean enabled) {
if (SdkLevel.getLevel() >= SdkLevel.LEVEL_ECLAIR) {
this.googleVoiceEnabled = enabled;
SharedPreferences prefs = activity.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(PREF_GVENABLED, enabled);
editor.commit();
} else {
Toast.makeText(activity, "Sorry, your phone's system does not support this option.", Toast.LENGTH_LONG).show();
}
}
/**
* Gets whether you want the {@link #MessageReceived(String,String)} event to
* get run when a new text message is received.
*
* @return 1,2 or 3 indicating that receiving is disabled (1) or foreground only
* (2) or always (3).
* {@link #MessageReceived(String,String)} event to get run when a
* new text message is received.
*/
@SimpleProperty(category = PropertyCategory.BEHAVIOR,
description = "If set to 1 (OFF) no messages will be received. If set to 2 (FOREGROUND) or" +
"3 (ALWAYS) the component will respond to messages if it is running. If the " +
"app is not running then the message will be discarded if set to 2 " +
"(FOREGROUND). If set to 3 (ALWAYS) and the app is not running the phone will " +
"show a notification. Selecting the notification will bring up the app " +
"and signal the MessageReceived event. Messages received when the app " +
"is dormant will be queued, and so several MessageReceived events might " +
"appear when the app awakens. As an app developer, it would be a good " +
"idea to give your users control over this property, so they can make " +
"their phones ignore text messages when your app is installed.")
public int ReceivingEnabled() {
return receivingEnabled;
}
/**
* Sets whether you want the {@link #MessageReceived(String,String)} event to
* get run when a new text message is received.
*
* @param enabled 0 = never receive, 1 = receive foreground only, 2 = receive always
*
*/
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_TEXT_RECEIVING, defaultValue = "2") // Default is FOREGROUND
@SimpleProperty()
public void ReceivingEnabled(int enabled) {
if ((enabled < ComponentConstants.TEXT_RECEIVING_OFF) ||
(enabled > ComponentConstants.TEXT_RECEIVING_ALWAYS)) {
container.$form().dispatchErrorOccurredEvent(this, "Texting",
ErrorMessages.ERROR_BAD_VALUE_FOR_TEXT_RECEIVING, enabled);
return;
}
Texting.receivingEnabled = enabled;
SharedPreferences prefs = activity.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PREF_RCVENABLED, enabled);
editor.remove(PREF_RCVENABLED_LEGACY); // Remove any legacy value
editor.commit();
}
public static int isReceivingEnabled(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
int retval = prefs.getInt(PREF_RCVENABLED, -1);
if (retval == -1) { // Fetch legacy value
if (prefs.getBoolean(PREF_RCVENABLED_LEGACY, true))
return ComponentConstants.TEXT_RECEIVING_FOREGROUND; // Foreground
else
return ComponentConstants.TEXT_RECEIVING_OFF; // Off
}
return retval;
}
/**
* Parse the messages out of the extra fields from the "android.permission.RECEIVE_SMS" broadcast
* intent.
*
* Note: This code was copied from the Android android.provider.Telephony.Sms.Intents class.
*
* @param intent the intent to read from
* @return an array of SmsMessages for the PDUs
*/
public static SmsMessage[] getMessagesFromIntent(
Intent intent) {
Object[] messages = (Object[]) intent.getSerializableExtra("pdus");
byte[][] pduObjs = new byte[messages.length][];
for (int i = 0; i < messages.length; i++) {
pduObjs[i] = (byte[]) messages[i];
}
byte[][] pdus = new byte[pduObjs.length][];
int pduCount = pdus.length;
SmsMessage[] msgs = new SmsMessage[pduCount];
for (int i = 0; i < pduCount; i++) {
pdus[i] = pduObjs[i];
msgs[i] = SmsMessage.createFromPdu(pdus[i]);
}
return msgs;
}
/**
* Sends all the messages in the cache through MessageReceived and
* clears the cache.
*/
private void processCachedMessages() {
String[] messagelist = null;
synchronized (cacheLock) {
messagelist = retrieveCachedMessages();
}
if (messagelist == null)
return;
Log.i(TAG, "processing " + messagelist.length + " cached messages ");
for (int k = 0; k < messagelist.length; k++) {
String phoneAndMessage = messagelist[k];
Log.i(TAG, "Message + " + k + " " + phoneAndMessage);
int delim = phoneAndMessage.indexOf(":");
// If receiving is not enabled, messages are not dispatched
if ((receivingEnabled > ComponentConstants.TEXT_RECEIVING_OFF) && delim != -1) {
MessageReceived(phoneAndMessage.substring(0,delim),
phoneAndMessage.substring(delim+1));
}
}
}
/**
* Retrieves cached messages from the cache file
* and deletes the file.
* @return
*/
private String[] retrieveCachedMessages() {
Log.i(TAG, "Retrieving cached messages");
String cache = "";
try {
FileInputStream fis = activity.openFileInput(CACHE_FILE);
byte[] bytes = new byte[8192];
if (fis == null) {
Log.e(TAG, "Null file stream returned from openFileInput");
return null;
}
int n = fis.read(bytes);
Log.i(TAG, "Read " + n + " bytes from " + CACHE_FILE);
cache = new String(bytes, 0, n);
fis.close();
activity.deleteFile(CACHE_FILE);
messagesCached = 0;
Log.i(TAG, "Retrieved cache " + cache);
} catch (FileNotFoundException e) {
Log.e(TAG, "No Cache file found -- this is not (usually) an error");
return null;
} catch (IOException e) {
Log.e(TAG, "I/O Error reading from cache file");
e.printStackTrace();
return null;
}
String messagelist[] = cache.split(MESSAGE_DELIMITER);
return messagelist;
}
/**
* Called by SmsBroadcastReceiver
* @return isRunning if the app is running in the foreground.
*/
public static boolean isRunning() {
return isRunning;
}
/**
* Used to keep count in Notifications.
* @return message count
*/
public static int getCachedMsgCount() {
return messagesCached;
}
/**
* Processes cached messages if the app is initialized
*/
@Override
public void onResume() {
Log.i(TAG, "onResume()");
isRunning = true;
if (isInitialized) {
processCachedMessages();
NotificationManager nm = (NotificationManager) activity.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(SmsBroadcastReceiver.NOTIFICATION_ID);
}
}
/**
* Messages received while paused will be cached
*/
@Override
public void onPause() {
Log.i(TAG, "onPause()");
isRunning = false;
}
/**
* This method is called by SmsBroadcastReceiver when a message is received.
* @param phone
* @param msg
*/
public static void handledReceivedMessage(Context context, String phone, String msg) {
if (isRunning) {
MessageReceived(phone, msg);
} else {
synchronized (cacheLock) {
addMessageToCache(context, phone, msg);
}
}
}
/**
* Messages a cached in a private file
* @param context
* @param phone
* @param msg
*/
private static void addMessageToCache(Context context, String phone, String msg) {
try {
String cachedMsg = phone + ":" + msg + MESSAGE_DELIMITER;
Log.i(TAG, "Caching " + cachedMsg);
FileOutputStream fos = context.openFileOutput(CACHE_FILE, Context.MODE_APPEND);
fos.write(cachedMsg.getBytes());
fos.close();
++messagesCached;
Log.i(TAG, "Cached " + cachedMsg);
} catch (FileNotFoundException e) {
Log.e(TAG, "File not found error writing to cache file");
e.printStackTrace();
} catch (IOException e) {
Log.e(TAG, "I/O Error writing to cache file");
e.printStackTrace();
}
}
/**
* Utility class built from Free Software (GPLv3 or later)
* by cannibalizing parts of Voice.java of the free software
* package, Google-Voice-Java:
*
* @see http://code.google.com/p/google-voice-java/
*
*/
class GoogleVoiceUtil {
private final int MAX_REDIRECTS = 5;
String general; // Google's GV page
String rnrSEE; // Value that passed into SMS's
String authToken;
int redirectCounter;
private boolean isInitialized;
/**
* The constructor sometimes fails to getGeneral
* @param authToken
*/
public GoogleVoiceUtil(String authToken) {
Log.i(TAG, "Creating GV Util");
this.authToken = authToken;
try {
this.general = getGeneral();
Log.i(TAG, "general = " + this.general);
setRNRSEE();
isInitialized = true; // If we make it to here, we're good to go
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean isInitialized() {
return isInitialized;
}
/**
* Free software method copied and adapted from Voice.java
* @see http://code.google.com/p/google-voice-java/
*
*/
private String sendGvSms(String smsData) {
Log.i(TAG, "sendGvSms()");
String response = "";
try {
// Add the RNR_SE to the message
smsData += "&" + URLEncoder.encode("_rnr_se", UTF8) + "=" + URLEncoder.encode(rnrSEE, UTF8);
Log.i(TAG, "smsData = " + smsData);
URL smsUrl = new URL(GV_SMS_SEND_URL);
URLConnection smsConn = smsUrl.openConnection();
smsConn.setRequestProperty( "Authorization", "GoogleLogin auth=" + authToken );
smsConn.setRequestProperty("User-agent", USER_AGENT);
smsConn.setDoOutput(true);
smsConn.setConnectTimeout(SERVER_TIMEOUT_MS);
Log.i(TAG, "sms request = " + smsConn);
OutputStreamWriter callwr = new OutputStreamWriter(smsConn.getOutputStream());
callwr.write(smsData);
callwr.flush();
BufferedReader callrd = new BufferedReader(new InputStreamReader(smsConn.getInputStream()));
String line;
while ((line = callrd.readLine()) != null) {
response += line + "\n\r";
}
Log.i(TAG, "sendGvSms: Sent SMS, response = " + response);
callwr.close();
callrd.close();
if (response.equals("")) {
throw new IOException("No Response Data Received.");
} else
return response;
} catch (IOException e) {
Log.i(TAG, "IO Error on Send " + e.getMessage());
e.printStackTrace();
return "IO Error Message not sent";
}
}
/**
* Fetches the page Source Code for the Voice homepage. This file contains
* most of the useful information for the Google Voice Account such as
* attached PhoneOld info and Contacts.
*
* @return the general
* @throws IOException
* Signals that an I/O exception has occurred.
*/
public String getGeneral() throws IOException {
Log.i(TAG, "getGeneral()");
return get(GV_URL);
}
/**
* Internal method which parses the Homepage source code to determine the
* rnrsee variable, this variable is passed into most functions for placing
* calls and sms.
*
* @throws IOException Signals that an I/O exception has occurred.
*/
private void setRNRSEE() throws IOException {
Log.i(TAG, "setRNRSEE()");
if (general != null) {
if(general.contains("'_rnr_se': '")) {
String p1 = general.split("'_rnr_se': '", 2)[1];
rnrSEE = p1.split("',", 2)[0];
Log.i(TAG,"Successfully Received rnr_se.");
p1 = null;
} else {
Log.i(TAG, "Answer did not contain rnr_se! "+ general);
throw new IOException("Answer did not contain rnr_se! "+ general);
}
} else {
Log.i(TAG,"setRNRSEE(): Answer was null!");
throw new IOException("setRNRSEE(): Answer was null!");
}
}
/**
* HTTP GET request for a given URL String.
*
* @param urlString
* the url string
* @return the string
* @throws IOException
* Signals that an I/O exception has occurred.
*/
String get(String urlString) throws IOException {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
int responseCode = 0;
try {
conn.setRequestProperty( "Authorization", "GoogleLogin auth="+authToken );
conn.setRequestProperty("User-agent", USER_AGENT);
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
// Get the response
conn.connect();
responseCode = conn.getResponseCode();
Log.i(TAG, urlString + " - " + conn.getResponseMessage());
} catch (Exception e) {
throw new IOException(urlString + " : " + conn.getResponseMessage() + "("+responseCode+") : IO Error.");
}
InputStream is;
if(responseCode==200) {
is = conn.getInputStream();
} else if(responseCode==HttpURLConnection.HTTP_MOVED_PERM || responseCode==HttpURLConnection.HTTP_MOVED_TEMP || responseCode==HttpURLConnection.HTTP_SEE_OTHER || responseCode==307) {
redirectCounter++;
if(redirectCounter > MAX_REDIRECTS) {
redirectCounter = 0;
throw new IOException(urlString + " : " + conn.getResponseMessage() + "("+responseCode+") : Too many redirects. exiting.");
}
String location = conn.getHeaderField("Location");
if(location!=null && !location.equals("")) {
System.out.println(urlString + " - " + responseCode + " - new URL: " + location);
return get(location);
} else {
throw new IOException(urlString + " : " + conn.getResponseMessage() + "("+responseCode+") : Received moved answer but no Location. exiting.");
}
} else {
is = conn.getErrorStream();
}
redirectCounter = 0;
if(is==null) {
throw new IOException(urlString + " : " + conn.getResponseMessage() + "("+responseCode+") : InputStream was null : exiting.");
}
String result="";
try {
// Get the response
BufferedReader rd = new BufferedReader(new InputStreamReader(is));
StringBuffer sb = new StringBuffer();
String line;
while ((line = rd.readLine()) != null) {
sb.append(line + "\n\r");
}
rd.close();
result = sb.toString();
} catch (Exception e) {
throw new IOException(urlString + " - " + conn.getResponseMessage() + "("+responseCode+") - " +e.getLocalizedMessage());
}
return result;
}
}
/**
* Callback method to handle the result of attempting to send a message.
* Each message is assigned a Broadcast receiver that is notified by
* the phone's radio regarding the status of the sent message. The
* receivers call this method. (See transmitMessage() method below.)
*
* @param context
* The context in which the calling BroadcastReceiver is running.
* @param receiver
* Currently unused. Intended as a special BroadcastReceiver to
* send results to. (For instance, if another plugin wanted to do
* its own handling.)
* @param resultCode, the code sent back by the phone's Radio
* @param seq, the message's sequence number
* @param smsMsg, the message being processed
*/
private synchronized void handleSentMessage(Context context,
BroadcastReceiver receiver, int resultCode, String smsMsg) {
switch (resultCode) {
case Activity.RESULT_OK:
Log.i(TAG, "Received OK, msg:" + smsMsg);
Toast.makeText(activity, "Message sent", Toast.LENGTH_SHORT).show();
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
Log.e(TAG, "Received generic failure, msg:" + smsMsg);
Toast.makeText(activity, "Generic failure: message not sent", Toast.LENGTH_SHORT).show();
break;
case SmsManager.RESULT_ERROR_NO_SERVICE:
Log.e(TAG, "Received no service error, msg:" + smsMsg);
Toast.makeText(activity, "No Sms service available. Message not sent.", Toast.LENGTH_SHORT).show();
break;
case SmsManager.RESULT_ERROR_NULL_PDU:
Log.e(TAG, "Received null PDU error, msg:" + smsMsg);
Toast.makeText(activity, "Received null PDU error. Message not sent.", Toast.LENGTH_SHORT).show();
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
Log.e(TAG, "Received radio off error, msg:" + smsMsg);
Toast.makeText(activity, "Could not send SMS message: radio off.", Toast.LENGTH_LONG).show();
break;
}
}
/**
* Sends a text message via SMS. No authentication required.
* This method is called only when the UseGoogleVoice option is disabled.
*/
private void sendViaSms() {
Log.i(TAG, "Sending via built-in Sms");
ArrayList<String> parts = smsManager.divideMessage(message);
int numParts = parts.size();
ArrayList<PendingIntent> pendingIntents = new ArrayList<PendingIntent>();
for (int i = 0; i < numParts; i++)
pendingIntents.add(PendingIntent.getBroadcast(activity, 0, new Intent(SENT), 0));
// Receiver for when the SMS is sent
BroadcastReceiver sendReceiver = new BroadcastReceiver() {
@Override
public synchronized void onReceive(Context arg0, Intent arg1) {
try {
handleSentMessage(arg0, null, getResultCode(), message);
activity.unregisterReceiver(this);
} catch (Exception e) {
Log.e("BroadcastReceiver",
"Error in onReceive for msgId " + arg1.getAction());
Log.e("BroadcastReceiver", e.getMessage());
e.printStackTrace();
}
}
};
// This may result in an error -- a "sent" or "error" message will be displayed
activity.registerReceiver(sendReceiver, new IntentFilter(SENT));
smsManager.sendMultipartTextMessage(phoneNumber, null, parts, pendingIntents, null);
}
/**
* Handles authentication needed for sending via Google Voice
*/
class AsyncAuthenticate extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... arg0) {
Log.i(TAG, "Authenticating");
// Get and return the authtoken
return new OAuth2Helper().getRefreshedAuthToken(activity, GV_SERVICE);
}
/**
* Sets the authToken instance variable and sends a message if one is waiting.
*/
@Override
protected void onPostExecute(String result) {
Log.i(TAG, "authToken = " + result);
authToken = result;
Toast.makeText(activity, "Finished authentication", Toast.LENGTH_SHORT).show();
// Send any pending messages
processPendingQueue();
}
}
/**
* Asynchronously Sends a text message via Google Voice. This requires authentication.
* This is used only when UseGoogleVoice option is enabled.
*
* NOTE: Because a background process is used, sending multiple messages with GoogleVoice
* cannot be done in a loop. The app must use a clock timer.
*/
class AsyncSendMessage extends AsyncTask<String, Void, String> {
/**
* Handles sending SMS over Google Voice or built-in SMS
* @param arg0 a String array containing the phoneNumber and message.
*
* NOTE: The authToken referenced here is a Texting instance variable.
* It is set when the Texting constructor.
*/
@Override
protected String doInBackground(String... args) {
String phoneNumber = args[0];
String message = args[1];
String response = "";
String smsData = "";
Log.i(TAG, "Async sending phoneNumber = " + phoneNumber + " message = " + message);
try {
// Set up the smsMessage for Google Voice
smsData =
URLEncoder.encode("phoneNumber", UTF8) + "=" + URLEncoder.encode(phoneNumber, UTF8) +
"&" + URLEncoder.encode("text", UTF8) + "=" + URLEncoder.encode(message, UTF8);
if (gvHelper == null) {
gvHelper = new GoogleVoiceUtil(authToken);
}
if (gvHelper.isInitialized()) {
response = gvHelper.sendGvSms(smsData);
Log.i(TAG, "Sent SMS, response = " + response);
} else {
return "IO Error: unable to create GvHelper";
}
} catch (Exception e) {
e.printStackTrace();
}
return response;
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
JSONObject json;
boolean ok = false;
int code = 0;
try {
json = new JSONObject(result);
ok = json.getBoolean("ok");
code = json.getJSONObject("data").getInt("code");
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (ok)
Toast.makeText(activity, "Message sent", Toast.LENGTH_SHORT).show();
else if (code == 58)
Toast.makeText(activity, "Errcode 58: SMS limit reached", Toast.LENGTH_SHORT).show();
else if (result.contains("IO Error"))
Toast.makeText(activity, result, Toast.LENGTH_SHORT).show();
}
}
/**
* Save the component's state in shared preference file before it is killed.
*/
@Override
public void onStop() {
SharedPreferences prefs = activity.getSharedPreferences(PREF_FILE, Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(PREF_RCVENABLED, receivingEnabled);
editor.putBoolean(PREF_GVENABLED, googleVoiceEnabled);
editor.commit();
}
}