Java tutorial
/* * 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 android.app.Application; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.net.ConnectivityManager; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.preference.PreferenceManager; import android.provider.CallLog.Calls; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.telephony.TelephonyManager; import com.commonsware.cwac.wakeful.WakefulIntentService; import io.rapidpro.androidchannel.data.DBCommandHelper; import io.rapidpro.androidchannel.payload.MTTextMessage; import io.rapidpro.androidchannel.payload.ResetCommand; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.util.*; public class RapidPro extends Application { public final int NOTIFICATION_ID = 1; public static Logger LOG = new Logger(); public static final boolean SHOW_WIRE = true; private static RapidPro s_this; public static final String LAST_PACK = "lastPackUsed"; /** how many messages we are willing to send per pack per 30 minutes */ public static int MESSAGE_THROTTLE = 30; public static int MESSAGE_THROTTLE_MINUTES = 30; public static long MESSAGE_THROTTLE_WINDOW = 1000 * 60 * (MESSAGE_THROTTLE_MINUTES + 2); public static final long MESSAGE_RATE_LIMITER = 1000; public static final String PREF_LAST_UPDATE = "lastUpdate"; private SMSModem m_modem; private CallObserver m_callObserver; private IncomingSMSObserver m_incomingSMSObserver; private List<String> m_installedPacks = new ArrayList<String>(); private HashMap<String, ArrayList<Long>> m_sendReports = new HashMap<String, ArrayList<Long>>(); @Override public void onCreate() { super.onCreate(); PreferenceManager.setDefaultValues(this, R.layout.settings, false); // earlier versions of android are allowed to have higher message throughput // before Build.VERSION_CODES.ICE_CREAM_SANDWICH which is 14 if (Build.VERSION.SDK_INT < 14) { MESSAGE_THROTTLE = 100; MESSAGE_THROTTLE_MINUTES = 60; MESSAGE_THROTTLE_WINDOW = 1000 * 60 * (MESSAGE_THROTTLE_MINUTES + 2); } s_this = this; // register our sms modem m_modem = new SMSModem(this, new SMSListener()); // register our Incoming SMS listener m_incomingSMSObserver = new IncomingSMSObserver(); getContentResolver().registerContentObserver(Uri.parse("content://sms"), true, m_incomingSMSObserver); // register our call listener m_callObserver = new CallObserver(); getContentResolver().registerContentObserver(Calls.CONTENT_URI, true, m_callObserver); // register for device details IntentFilter statusChanged = new IntentFilter(); statusChanged.addAction(Intent.ACTION_BATTERY_CHANGED); statusChanged.addAction(ConnectivityManager.CONNECTIVITY_ACTION); StatusReceiver receiver = new StatusReceiver(); getBaseContext().registerReceiver(receiver, statusChanged); WakefulIntentService.cancelAlarms(this); WakefulIntentService.scheduleAlarms(new RapidProAlarmListener(), this); refreshInstalledPacks(); updateNotification(); } public boolean isResetting() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); return prefs.getBoolean(SettingsActivity.RESET, false); } public boolean isClaimed() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); return !isResetting() && (prefs.getInt(SettingsActivity.RELAYER_ORG, -1) != -1); } public boolean isPaused() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); return prefs.getBoolean(SettingsActivity.IS_PAUSED, false); } public boolean isRegistered() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); return !isResetting() && (prefs.getString(SettingsActivity.RELAYER_ID, null) != null); } public boolean hasGCM() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); return !isResetting() && (prefs.getString(SettingsActivity.GCM_ID, "").length() > 0); } public void refreshInstalledPacks() { final PackageManager pm = getPackageManager(); List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA); List<String> packs = new ArrayList<String>(); for (ApplicationInfo packageInfo : packages) { if (packageInfo.packageName.startsWith("io.rapidpro.androidchannel")) { packs.add(packageInfo.packageName); } } LOG.d("Found " + packs.size() + " installed messaging packs"); for (String pack : packs) { LOG.d(" - " + pack); } m_installedPacks = packs; } public void updateNotification() { if (isPaused() || !isClaimed()) { hideNotification(); } else { showNotification(); } } public void showNotification() { NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.ic_notification).setContentTitle("RapidPro") .setContentText("RapidPro is active and relaying messages.").setOngoing(true); Intent resultIntent = new Intent(this, HomeActivity.class); TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); stackBuilder.addParentStack(HomeActivity.class); stackBuilder.addNextIntent(resultIntent); PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(resultPendingIntent); NotificationManager notificationManager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, builder.build()); } public void hideNotification() { NotificationManager notificationManager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE); notificationManager.cancel(NOTIFICATION_ID); } public ArrayList<Long> getSendsForPack(String pack) { ArrayList<Long> sends = m_sendReports.get(pack); if (sends == null) { sends = new ArrayList<Long>(); m_sendReports.put(pack, sends); } prunePack(sends); return sends; } public void addSendForPack(String pack, int numSends) { for (int i = 0; i < numSends; i++) { getSendsForPack(pack).add(System.currentTimeMillis()); } } public int getTotalSent() { int totalSent = 0; List<String> packs = getInstalledPacks(); for (int i = 0; i < packs.size(); i++) { String candidate = packs.get(i); totalSent += getSendsForPack(candidate).size(); } return totalSent; } public int getSendCapacity() { List<String> packs = getInstalledPacks(); return packs.size() * MESSAGE_THROTTLE; } private void prunePack(ArrayList<Long> sends) { // prune our list of messages which are too old long cutoff = System.currentTimeMillis() - MESSAGE_THROTTLE_WINDOW; for (Iterator<Long> it = sends.iterator(); it.hasNext();) { long send = it.next(); if (send < cutoff) { it.remove(); } else { break; } } } public String getNextPack(int numMessages) { List<String> packs = getInstalledPacks(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); // start with the next pack in line int packNumber = (prefs.getInt(LAST_PACK, 0) + 1) % packs.size(); for (int i = 0; i < packs.size(); i++) { String candidate = packs.get(packNumber); if (getSendsForPack(candidate).size() + numMessages < RapidPro.MESSAGE_THROTTLE) { SharedPreferences.Editor editor = prefs.edit(); editor.putInt(LAST_PACK, packNumber); editor.commit(); return candidate; } packNumber = (packNumber + 1) % packs.size(); } return null; } public List<String> getInstalledPacks() { return m_installedPacks; } public static RapidPro get() { return s_this; } public void refreshHome() { // trigger our home view to refresh Intent intent = new Intent(Intents.UPDATE_STATUS); sendBroadcast(intent); } public void sync() { sync(false); } public void sync(boolean force) { // Stop if RapidPro is paused if (isPaused()) return; Intent intent = new Intent(Intents.START_SYNC); intent.putExtra(Intents.SYNC_TIME, System.currentTimeMillis()); if (force) { intent.putExtra(Intents.FORCE_EXTRA, true); } WakefulIntentService.sendWakefulWork(this, intent); } public void pingGCM() { WakefulIntentService.sendWakefulWork(this, new Intent(Intents.PING_GCM)); } public void runCommands() { WakefulIntentService.sendWakefulWork(this, new Intent(Intents.RUN_LOCAL_COMMANDS)); } public SMSModem getModem() { return m_modem; } @Override public void onTerminate() { getContentResolver().unregisterContentObserver(m_callObserver); getContentResolver().unregisterContentObserver(m_incomingSMSObserver); } public int getTotalSentInWindow() { int count = 0; for (String pack : getInstalledPacks()) { count += getSendsForPack(pack).size(); } return count; } /** * Pauses the RapidPro application */ public void pause() { if (!isPaused()) { // set a flag in settings to pause SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); editor.putBoolean(SettingsActivity.IS_PAUSED, true); editor.commit(); // hide the notification in status bar updateNotification(); RapidPro.broadcastUpdatedCounts(this); } } /** * Resume the RapidPro application */ public void resume() { if (isPaused()) { // set the pause flag to resume SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); editor.putBoolean(SettingsActivity.IS_PAUSED, false); editor.commit(); // show notification updateNotification(); RapidPro.broadcastUpdatedCounts(this); // force trigger a sync sync(true); } } /** * Triggers our relayer to be reset. */ public void reset() { // remove all our previous commands DBCommandHelper.clearCommands(this); SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(this).edit(); // trigger a reset editor.putBoolean(SettingsActivity.RESET, true); editor.putBoolean(SettingsActivity.IS_PAUSED, false); editor.commit(); // queue our reset command DBCommandHelper.queueCommand(this, new ResetCommand()); sync(true); } /** * Releases our relayer, resetting all messages and data. * * @param context */ public static void release(Context context) { SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); editor.remove(SettingsActivity.RELAYER_SECRET); editor.remove(SettingsActivity.RELAYER_ID); editor.remove(SettingsActivity.RELAYER_ORG); editor.commit(); // remove all commands DBCommandHelper.clearCommands(context); // notify everybody that our state has changed Intent intent = new Intent(Intents.UPDATE_RELAYER_STATE); context.sendBroadcast(intent); } public static void broadcastUpdatedCounts(Context context) { Intent intent = new Intent(); intent.setAction(Intents.UPDATE_COUNTS); intent.addCategory(Intent.CATEGORY_DEFAULT); int sent = RapidPro.get().getTotalSent(); int capacity = RapidPro.get().getSendCapacity(); int outgoing = DBCommandHelper.getCommandCount(context, DBCommandHelper.IN, DBCommandHelper.BORN, MTTextMessage.CMD) + DBCommandHelper.getCommandCount(context, DBCommandHelper.IN, MTTextMessage.PENDING, MTTextMessage.CMD); int incoming = DBCommandHelper.getMessagesReceivedInWindow(context); int retry = DBCommandHelper.getCommandCount(context, DBCommandHelper.IN, MTTextMessage.RETRY, MTTextMessage.CMD); int sync = DBCommandHelper.getCommandCount(context, DBCommandHelper.OUT, DBCommandHelper.BORN, null); intent.putExtra(Intents.SENT_EXTRA, sent); intent.putExtra(Intents.CAPACITY_EXTRA, capacity); intent.putExtra(Intents.OUTGOING_EXTRA, outgoing); intent.putExtra(Intents.INCOMING_EXTRA, incoming); intent.putExtra(Intents.RETRY_EXTRA, retry); intent.putExtra(Intents.SYNC_EXTRA, sync); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); intent.putExtra(Intents.CONNECTION_UP_EXTRA, prefs.getBoolean(SettingsActivity.CONNECTION_UP, true)); intent.putExtra(Intents.LAST_SMS_SENT, prefs.getLong(SettingsActivity.LAST_SMS_SENT, 0)); intent.putExtra(Intents.LAST_SMS_RECEIVED, prefs.getLong(SettingsActivity.LAST_SMS_RECEIVED, 0)); intent.putExtra(Intents.IS_PAUSED, prefs.getBoolean(SettingsActivity.IS_PAUSED, false)); context.sendBroadcast(intent); } public void installPack(Context context) { List<String> packs = getInstalledPacks(); int packToInstall = 0; for (int i = 1; i <= 10; i++) { if (!packs.contains("io.rapidpro.androidchannel.pack" + i)) { packToInstall = i; break; } } if (packToInstall > 0) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("market://details?id=io.rapidpro.androidchannel.pack" + packToInstall)); context.startActivity(intent); } } public String getUUID() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); String uuid = prefs.getString(SettingsActivity.UUID, null); if (uuid == null) { uuid = generateUUID(); SharedPreferences.Editor editor = prefs.edit(); editor.putString(SettingsActivity.UUID, uuid); editor.commit(); } return uuid; } /** * Generates a UUID that should be constant across devices. This uses a combination of the IMEI if available * and the AndroidId. Note that both of these could be empty but it is very unlikely both are. * * @return */ public String generateUUID() { final TelephonyManager tm = (TelephonyManager) getBaseContext().getSystemService(Context.TELEPHONY_SERVICE); final String tmDevice, androidId; tmDevice = "" + tm.getDeviceId(); androidId = "" + android.provider.Settings.Secure.getString(getContentResolver(), android.provider.Settings.Secure.ANDROID_ID); UUID deviceUUID = new UUID(androidId.hashCode(), tmDevice.hashCode()); return deviceUUID.toString(); } public void printDebug() { // some debug metrics int allotted = ((RapidPro) getApplicationContext()).getInstalledPacks().size() * RapidPro.MESSAGE_THROTTLE; int sent = RapidPro.get().getTotalSentInWindow(); int born = DBCommandHelper .getPendingCommands(this, DBCommandHelper.IN, DBCommandHelper.BORN, -1, MTTextMessage.CMD, false) .size(); int pending = DBCommandHelper .getPendingCommands(this, DBCommandHelper.IN, MTTextMessage.PENDING, -1, MTTextMessage.CMD, false) .size(); int retry = DBCommandHelper .getPendingCommands(this, DBCommandHelper.IN, MTTextMessage.RETRY, -1, MTTextMessage.CMD, false) .size(); RapidPro.LOG.d("\n\n============================================================================"); RapidPro.LOG.d(sent + " of " + allotted + " messages in last 30 minutes."); RapidPro.LOG.d(" Born : " + born); RapidPro.LOG.d(" Pending : " + pending); RapidPro.LOG.d(" Retry : " + retry); RapidPro.LOG.d("\n"); for (String pack : RapidPro.get().getInstalledPacks()) { RapidPro.LOG.d(" > " + RapidPro.get().getSendsForPack(pack).size() + ": " + pack); } RapidPro.LOG.d("\n\n"); } }