/*
* RapidPro Android Channel - Relay SMS messages where MNO connections aren't practical.
* Copyright (C) 2014 Nyaruka, UNICEF
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.rapidpro.androidchannel;
import java.util.*;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.*;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.util.Log;
import io.rapidpro.androidchannel.json.JSON;
public final class SMSModem extends BroadcastReceiver {
private static final String SMS_SENT_REPORT_ACTION = "io.rapidpro.androidchannel.SMS_SENT_REPORT";
private static final String SMS_SENT_REPORT_TOKEN_EXTRA = "token";
private static final String SMS_FAILED_REPORT_ACTION = "io.rapidpro.androidchannel.SMS_FAILED_REPORT";
private static final String SMS_FAILED_REPORT_TOKEN_EXTRA = "token";
private static final String SMS_DELIVERED_REPORT_ACTION = "io.rapidpro.androidchannel.SMS_DELIVERED_REPORT";
private static final String SMS_DELIVERED_REPORT_TOKEN_EXTRA = "token";
private static final String SENT_KEY = "SENT_KEY";
private static final String DELIVERED_KEY = "DELIVERED_KEY";
private static final String KEYS = "KEYS";
private static final String VALUES = "VALUES";
private final Context context;
private final SmsManager smsManager;
private final SmsModemListener listener;
private Map<String, Integer> m_pendingSent = new Hashtable<String, Integer>();
private Map<String, Integer> m_pendingDelivered = new Hashtable<String, Integer>();
public interface SmsModemListener {
public void onSMSSent(Context context, String token);
public void onSMSDelivered(Context context, String token);
public void onSMSSendError(Context context, String token, String errorDetails);
public void onSMSSendFailed(Context context, String token);
}
public SMSModem(Context c, SmsModemListener l) {
context = c;
listener = l;
smsManager = SmsManager.getDefault();
final IntentFilter receivedFilter = new IntentFilter();
receivedFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
context.registerReceiver(this, receivedFilter);
final IntentFilter deliveryFilter = new IntentFilter();
deliveryFilter.addAction(SMS_SENT_REPORT_ACTION);
deliveryFilter.addAction(SMS_DELIVERED_REPORT_ACTION);
deliveryFilter.addAction(SMS_FAILED_REPORT_ACTION);
deliveryFilter.addDataScheme("sms");
context.registerReceiver(this, deliveryFilter);
final IntentFilter shuttingDownFilter = new IntentFilter();
shuttingDownFilter.addAction("android.intent.action.ACTION_SHUTDOWN");
shuttingDownFilter.addAction("android.intent.action.QUICKBOOT_POWEROFF");
context.registerReceiver(this, shuttingDownFilter);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
SharedPreferences.Editor editor = prefs.edit();
String pendingSentString = prefs.getString(SENT_KEY, null);
String pendingDeliveredString = prefs.getString(DELIVERED_KEY, null);
if (pendingSentString != null) {
m_pendingSent = deserializer(pendingSentString);
editor.remove(SENT_KEY);
RapidPro.LOG.d(String.format("Getting pendingSent from SENT_KEY %s in SharedPreferences", pendingSentString));
}
if (pendingDeliveredString != null){
m_pendingDelivered = deserializer(pendingDeliveredString);
editor.remove(DELIVERED_KEY);
RapidPro.LOG.d(String.format("Getting pendingDelivered from DELIVERED_KEY %s in SharedPreferences", pendingDeliveredString));
}
editor.commit();
}
public void sendSms(Context c, String address, String message, String token, String pack) {
RapidPro app = ((RapidPro)c.getApplicationContext());
synchronized (app) {
if (message != null && address != null && token != null) {
final ArrayList<String> parts = smsManager.divideMessage(message);
Intent sendMessageIntent = new Intent("io.rapidpro.androidchannel.SendMessage");
sendMessageIntent.addCategory(pack);
sendMessageIntent.putExtra("address", address);
sendMessageIntent.putExtra("message", parts);
sendMessageIntent.putExtra("token", token);
m_pendingSent.put(token, parts.size());
// android only returns only one delivery reports for sendMultipleTextMessage
m_pendingDelivered.put(token, 1);
c.startService(sendMessageIntent);
}
}
}
public void clear() {
context.unregisterReceiver(this);
}
@Override
public void onReceive(Context c, Intent intent) {
final String action = intent.getAction();
RapidPro.LOG.d("SMSModem got action: " + action + intent.toString());
if (action.equalsIgnoreCase(SMS_DELIVERED_REPORT_ACTION)) {
final int resultCode = getResultCode();
final String token = intent.getStringExtra(SMS_DELIVERED_REPORT_TOKEN_EXTRA);
RapidPro.LOG.d("Deliver report, result code '" + resultCode + "', token '" + token + "' URI: " + intent.getData());
if (resultCode == Activity.RESULT_OK){
if (m_pendingDelivered.containsKey(token)) {
m_pendingDelivered.put(token, m_pendingDelivered.get(token).intValue() - 1);
if (m_pendingDelivered.get(token).intValue() == 0) {
m_pendingDelivered.remove(token);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(c).edit();
editor.putString(SENT_KEY, serializer(m_pendingSent));
editor.putString(DELIVERED_KEY, serializer(m_pendingDelivered));
editor.commit();
listener.onSMSDelivered(c, token);
}
}
}
} else if (action.equalsIgnoreCase(SMS_SENT_REPORT_ACTION)) {
final int resultCode = getResultCode();
final String token = intent.getStringExtra(SMS_SENT_REPORT_TOKEN_EXTRA);
RapidPro.LOG.d("Sent to queue report, result code '" + resultCode + "', token '" + token + "' URI: " + intent.getData());
if (resultCode == Activity.RESULT_OK){
if (m_pendingSent.containsKey(token)) {
m_pendingSent.put(token, m_pendingSent.get(token).intValue() - 1);
if (m_pendingSent.get(token).intValue() == 0) {
m_pendingSent.remove(token);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(c).edit();
editor.putString(SENT_KEY, serializer(m_pendingSent));
editor.putString(DELIVERED_KEY, serializer(m_pendingDelivered));
editor.commit();
listener.onSMSSent(c, token);
}
}
} else {
if (m_pendingSent.containsKey(token)) {
m_pendingSent.remove(token);
m_pendingDelivered.remove(token);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(c).edit();
editor.putString(SENT_KEY, serializer(m_pendingSent));
editor.putString(DELIVERED_KEY, serializer(m_pendingDelivered));
editor.commit();
listener.onSMSSendError(c, token, extractError(resultCode, intent));
}
}
} else if (action.equalsIgnoreCase(SMS_FAILED_REPORT_ACTION)) {
final int resultCode = getResultCode();
final String token = intent.getStringExtra(SMS_FAILED_REPORT_TOKEN_EXTRA);
RapidPro.LOG.d("Sent to queue report, result code '" + resultCode + "', token '" + token + "' URI: " + intent.getData());
if (m_pendingSent.containsKey(token)) {
m_pendingSent.remove(token);
m_pendingDelivered.remove(token);
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(c).edit();
editor.putString(SENT_KEY, serializer(m_pendingSent));
editor.putString(DELIVERED_KEY, serializer(m_pendingDelivered));
editor.commit();
listener.onSMSSendFailed(c, token);
}
} else if (action.equals("android.intent.action.ACTION_SHUTDOWN") ||
action.equals("android.intent.action.QUICKBOOT_POWEROFF")) {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(c).edit();
editor.putString(SENT_KEY, serializer(m_pendingSent));
editor.putString(DELIVERED_KEY, serializer(m_pendingDelivered));
RapidPro.LOG.d(String.format("Added the pendingSent %s and pendingDelivered %s to SharedPreferences", serializer(m_pendingSent), serializer(m_pendingDelivered)));
editor.commit();
}
}
// function to serialize keys and values of a HashMap into a string
private String serializer(Map<String, Integer> map) {
JSON json = new JSON();
String[] keys = new String[map.size()];
String[] values = new String[map.size()];
int i=0;
synchronized(map){
for (String key: map.keySet()){
keys[i] = key;
values[i] = map.get(key).toString();
i++;
}
}
json.put(KEYS, keys);
json.put(VALUES, values);
return json.toString();
}
// function to convert back a seriarized string into a HashMap
private HashMap<String, Integer> deserializer(String map_string) {
HashMap<String, Integer> output_map = new HashMap<String, Integer>();
JSON json = new JSON(map_string);
String[] keys = json.getStringArray(KEYS);
String[] values = json.getStringArray(VALUES);
for (int i=0; i < keys.length; i++){
output_map.put(keys[i], Integer.parseInt(values[i]));
}
return output_map;
}
private String extractError(int resultCode, Intent i) {
switch (resultCode) {
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
if (i.hasExtra("errorCode")) {
return String.valueOf(i.getIntExtra("errorCode",-1));
} else {
return "Unknown error. No 'errorCode' field.";
}
case SmsManager.RESULT_ERROR_NO_SERVICE:
return "No service";
case SmsManager.RESULT_ERROR_RADIO_OFF:
return "Radio off";
case SmsManager.RESULT_ERROR_NULL_PDU:
return "PDU null";
default:
return "really unknown error";
}
}
}