package com.eveningoutpost.dexdrip; /** * Created by jamorham on 11/01/16. */ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; import android.util.Base64; import com.eveningoutpost.dexdrip.Models.BgReading; import com.eveningoutpost.dexdrip.Models.BloodTest; import com.eveningoutpost.dexdrip.Models.Calibration; import com.eveningoutpost.dexdrip.Models.DesertSync; import com.eveningoutpost.dexdrip.Models.JoH; import com.eveningoutpost.dexdrip.Models.RollCall; import com.eveningoutpost.dexdrip.Models.Sensor; import com.eveningoutpost.dexdrip.Models.TransmitterData; import com.eveningoutpost.dexdrip.Models.Treatments; import com.eveningoutpost.dexdrip.Models.UserError; import com.eveningoutpost.dexdrip.Models.UserError.Log; import com.eveningoutpost.dexdrip.Services.ActivityRecognizedService; import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer; import com.eveningoutpost.dexdrip.UtilityModels.Constants; import com.eveningoutpost.dexdrip.UtilityModels.NanoStatus; import com.eveningoutpost.dexdrip.UtilityModels.Pref; import com.eveningoutpost.dexdrip.UtilityModels.PumpStatus; import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; import com.eveningoutpost.dexdrip.UtilityModels.WholeHouse; import com.eveningoutpost.dexdrip.utils.CheckBridgeBattery; import com.eveningoutpost.dexdrip.utils.CipherUtils; import com.eveningoutpost.dexdrip.utils.Preferences; import com.eveningoutpost.dexdrip.utils.WebAppHelper; import com.eveningoutpost.dexdrip.utils.bt.Mimeograph; import com.eveningoutpost.dexdrip.wearintegration.ExternalStatusService; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.RemoteMessage; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; import static android.support.v4.content.WakefulBroadcastReceiver.completeWakefulIntent; import static com.eveningoutpost.dexdrip.Models.JoH.isAnyNetworkConnected; import static com.eveningoutpost.dexdrip.Models.JoH.showNotification; public class GcmListenerSvc extends JamListenerSvc { private static final String TAG = "jamorham GCMlis"; private static final String EXTRA_WAKE_LOCK_ID = "android.support.content.wakelockid"; public static long lastMessageReceived = 0; private static byte[] staticKey; public static int lastMessageMinutesAgo() { return (int) ((JoH.tsl() - GcmListenerSvc.lastMessageReceived) / 60000); } // data for MegaStatus public static List<StatusItem> megaStatus() { final List<StatusItem> l = new ArrayList<>(); if (lastMessageReceived > 0) l.add(new StatusItem("Network traffic", JoH.niceTimeSince(lastMessageReceived) + " ago")); return l; } @Override protected Intent zzD(Intent inteceptedIntent) { // intercept and fix google play services wakelocking bug try { if (!Pref.getBooleanDefaultFalse("excessive_wakelocks")) { completeWakefulIntent(inteceptedIntent); final Bundle extras = inteceptedIntent.getExtras(); if (extras != null) extras.remove(EXTRA_WAKE_LOCK_ID); } } catch (Exception e) { UserError.Log.wtf(TAG, "Error patching play services: " + e); } return super.zzD(inteceptedIntent); } @Override public void onSendError(String msgID, Exception exception) { boolean unexpected = true; if (exception.getMessage().equals("TooManyMessages")) { if (isAnyNetworkConnected() && googleReachable()) { GcmActivity.coolDown(); } unexpected = false; } if (unexpected || JoH.ratelimit("gcm-expected-error", 86400)) { Log.e(TAG, "onSendError called" + msgID, exception); } } private static boolean googleReachable() { return false; // TODO we need a method for this to properly handle cooldown default to false to disable functionality } @Override public void onDeletedMessages() { Log.e(TAG, "onDeletedMessages: "); } @Override public void onMessageSent(String msgID) { Log.i(TAG, "onMessageSent: " + msgID); } @Override public void onMessageReceived(RemoteMessage rmessage) { final PowerManager.WakeLock wl = JoH.getWakeLock("xdrip-onMsgRec", 120000); try { if (rmessage == null) return; if (GcmActivity.cease_all_activity) return; String from = rmessage.getFrom(); final Bundle data = new Bundle(); for (Map.Entry<String, String> entry : rmessage.getData().entrySet()) { data.putString(entry.getKey(), entry.getValue()); } if (from == null) { if (isInjectable()) { from = data.getString("yfrom"); } if (from == null) { from = "null"; } } String message = data.getString("message"); Log.d(TAG, "From: " + from); if (message != null) { Log.d(TAG, "Message: " + message); } else { message = "null"; } final Bundle notification = data.getBundle("notification"); if (notification != null) { Log.d(TAG, "Processing notification bundle"); try { sendNotification(notification.getString("body"), notification.getString("title")); } catch (NullPointerException e) { Log.d(TAG, "Null pointer exception within sendnotification"); } } if (from.startsWith(getString(R.string.gcmtpc))) { String xfrom = data.getString("xfrom"); String payload = data.getString("datum", data.getString("payload")); String action = data.getString("action"); if ((xfrom != null) && (xfrom.equals(GcmActivity.token))) { GcmActivity.queueAction(action + payload); return; } String[] tpca = from.split("/"); if ((tpca[2] != null) && (tpca[2].length() > 30) && (!tpca[2].equals(GcmActivity.myIdentity()))) { Log.e(TAG, "Received invalid channel: " + from + " instead of: " + GcmActivity.myIdentity()); if ((GcmActivity.myIdentity() != null) && (GcmActivity.myIdentity().length() > 30)) { try { FirebaseMessaging.getInstance().unsubscribeFromTopic(tpca[2]); } catch (Exception e) { Log.e(TAG, "Exception unsubscribing: " + e.toString()); } } return; } if (!isInjectable()) { if (!DesertSync.fromGCM(data)) { UserError.Log.d(TAG, "Skipping inbound data due to duplicate detection"); return; } } byte[] bpayload = null; if (payload == null) payload = ""; if (action == null) action = "null"; if (payload.length() > 16) { if (GoogleDriveInterface.keyInitialized()) { // handle binary message types switch (action) { case "btmm": case "bgmm": bpayload = CipherUtils.decryptStringToBytes(payload); if (JoH.checkChecksum(bpayload)) { bpayload = Arrays.copyOfRange(bpayload, 0, bpayload.length - 4); Log.d(TAG, "Binary payload received: length: " + bpayload.length + " orig: " + payload.length()); } else { Log.e(TAG, "Invalid binary payload received, possible key mismatch: "); bpayload = null; } payload = "binary"; break; default: if (action.equals("sensorupdate")) { try { Log.i(TAG, "payload for sensorupdate " + payload); byte[] inbytes = Base64.decode(payload, Base64.NO_WRAP); byte[] inbytes1 = JoH.decompressBytesToBytes(CipherUtils.decryptBytes(inbytes)); payload = new String(inbytes1, "UTF-8"); Log.d(TAG, "inbytes size = " + inbytes.length + " inbytes1 size " + inbytes1.length + "payload len " + payload.length()); } catch (UnsupportedEncodingException e) { Log.e(TAG, "Got unsupported encoding on UTF8 " + e.toString()); payload = ""; } } else { String decrypted_payload = CipherUtils.decryptString(payload); if (decrypted_payload.length() > 0) { payload = decrypted_payload; } else { Log.e(TAG, "Couldn't decrypt payload!"); payload = ""; Home.toaststaticnext("Having problems decrypting incoming data - check keys"); } } } } else { Log.e(TAG, "Couldn't decrypt as key not initialized"); payload = ""; } } else { if (payload.length() > 0) UserError.Log.wtf(TAG, "Got short payload: " + payload + " on action: " + action); } Log.i(TAG, "Got action: " + action + " with payload: " + payload); lastMessageReceived = JoH.tsl(); // new treatment if (action.equals("nt")) { Log.i(TAG, "Attempting GCM push to Treatment"); if (Home.get_master_or_follower() && Home.follower_or_accept_follower()) GcmActivity.pushTreatmentFromPayloadString(payload); } else if (action.equals("dat")) { Log.i(TAG, "Attempting GCM delete all treatments"); if (Home.get_master_or_follower() && Home.follower_or_accept_follower()) Treatments.delete_all(); } else if (action.equals("dt")) { Log.i(TAG, "Attempting GCM delete specific treatment"); if (Home.get_master_or_follower() && Home.follower_or_accept_follower()) Treatments.delete_by_uuid(filter(payload)); } else if (action.equals("clc")) { Log.i(TAG, "Attempting to clear last calibration"); if (Home.get_master_or_follower() && Home.follower_or_accept_follower()) { if (payload.length() > 0) { Calibration.clearCalibrationByUUID(payload); } else { Calibration.clearLastCalibration(); } } } else if (action.equals("cal")) { if (Home.get_master_or_follower() && Home.follower_or_accept_follower()) { String[] message_array = filter(payload).split("\\s+"); if ((message_array.length == 3) && (message_array[0].length() > 0) && (message_array[1].length() > 0) && (message_array[2].length() > 0)) { // [0]=timestamp [1]=bg_String [2]=bgAge Intent calintent = new Intent(); calintent.setClassName(getString(R.string.local_target_package), "com.eveningoutpost.dexdrip.AddCalibration"); calintent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); long timediff = (long) ((new Date().getTime() - Double.parseDouble(message_array[0])) / 1000); Log.i(TAG, "Remote calibration latency calculated as: " + Long.toString(timediff) + " seconds"); if (timediff > 0) { message_array[2] = Long.toString(Long.parseLong(message_array[2]) + timediff); } Log.i(TAG, "Processing remote CAL " + message_array[1] + " age: " + message_array[2]); calintent.putExtra("timestamp", JoH.tsl()); calintent.putExtra("bg_string", message_array[1]); calintent.putExtra("bg_age", message_array[2]); calintent.putExtra("cal_source", "gcm cal packet"); if (timediff < 3600) { getApplicationContext().startActivity(calintent); } } else { Log.e(TAG, "Invalid CAL payload"); } } } else if (action.equals("cal2")) { Log.i(TAG, "Received cal2 packet"); if (Home.get_master() && Home.follower_or_accept_follower()) { final NewCalibration newCalibration = GcmActivity.getNewCalibration(payload); if (newCalibration != null) { final Intent calintent = new Intent(); calintent.setClassName(getString(R.string.local_target_package), "com.eveningoutpost.dexdrip.AddCalibration"); calintent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); long timediff = (long) ((new Date().getTime() - newCalibration.timestamp) / 1000); Log.i(TAG, "Remote calibration latency calculated as: " + timediff + " seconds"); Long bg_age = newCalibration.offset; if (timediff > 0) { bg_age += timediff; } Log.i(TAG, "Processing remote CAL " + newCalibration.bgValue + " age: " + bg_age); calintent.putExtra("timestamp", JoH.tsl()); calintent.putExtra("bg_string", "" + (Pref.getString("units", "mgdl").equals("mgdl") ? newCalibration.bgValue : newCalibration.bgValue * Constants.MGDL_TO_MMOLL)); calintent.putExtra("bg_age", "" + bg_age); calintent.putExtra("cal_source", "gcm cal2 packet"); if (timediff < 3600) { getApplicationContext().startActivity(calintent); } else { Log.w(TAG, "warninig ignoring calibration because timediff is " + timediff); } } } else { Log.e(TAG, "Received cal2 packet packet but we are not a master, so ignoring it"); } } else if (action.equals("ping")) { if (payload.length() > 0) { RollCall.Seen(payload); } // don't respond to wakeup pings } else if (action.equals("rlcl")) { if (Home.get_master_or_follower()) { if (payload.length() > 0) { RollCall.Seen(payload); } GcmActivity.requestPing(); } } else if (action.equals("p")) { GcmActivity.send_ping_reply(); } else if (action.equals("q")) { Home.toaststatic("Received ping reply"); } else if (action.equals("plu")) { // process map update if (Home.get_follower()) { MapsActivity.newMapLocation(payload, (long) JoH.ts()); } } else if (action.equals("sbu")) { if (Home.get_follower()) { Log.i(TAG, "Received sensor battery level update"); Sensor.updateBatteryLevel(Integer.parseInt(payload), true); TransmitterData.updateTransmitterBatteryFromSync(Integer.parseInt(payload)); } } else if (action.equals("bbu")) { if (Home.get_follower()) { Log.i(TAG, "Received bridge battery level update"); Pref.setInt("bridge_battery", Integer.parseInt(payload)); CheckBridgeBattery.checkBridgeBattery(); } } else if (action.equals("pbu")) { if (Home.get_follower()) { Log.i(TAG, "Received parakeet battery level update"); Pref.setInt("parakeet_battery", Integer.parseInt(payload)); CheckBridgeBattery.checkParakeetBattery(); } } else if (action.equals("psu")) { if (Home.get_follower()) { Log.i(TAG, "Received pump status update"); PumpStatus.fromJson(payload); } } else if (action.startsWith("nscu")) { if (Home.get_follower()) { Log.i(TAG, "Received nanostatus update: " + action); NanoStatus.setRemote(action.replaceAll("^nscu", ""), payload); } } else if (action.equals("not")) { if (Home.get_follower()) { try { final int GCM_NOTIFICATION_ITEM = 543; final String[] payloadA = payload.split("\\^"); final String title = payloadA[0]; final String body = payloadA[1]; final PendingIntent pendingIntent = android.app.PendingIntent.getActivity(xdrip.getAppContext(), 0, new Intent(xdrip.getAppContext(), Home.class), android.app.PendingIntent.FLAG_UPDATE_CURRENT); showNotification(title, body, pendingIntent, GCM_NOTIFICATION_ITEM, true, true, false); } catch (Exception e) { UserError.Log.e(TAG, "Error showing follower notification with payload: " + payload); } } } else if (action.equals("sbr")) { if ((Home.get_master()) && JoH.ratelimit("gcm-sbr", 300)) { Log.i(TAG, "Received sensor battery request"); if (Sensor.currentSensor() != null) { try { TransmitterData td = TransmitterData.last(); if ((td != null) && (td.sensor_battery_level != 0)) { GcmActivity.sendSensorBattery(td.sensor_battery_level); } else { GcmActivity.sendSensorBattery(Sensor.currentSensor().latest_battery_level); } } catch (NullPointerException e) { Log.e(TAG, "Cannot send sensor battery as sensor is null"); } } else { Log.d(TAG, "No active sensor so not sending anything."); } } } else if (action.equals("amu")) { if ((Pref.getBoolean("motion_tracking_enabled", false)) && (Pref.getBoolean("use_remote_motion", false))) { if (!Pref.getBoolean("act_as_motion_master", false)) { ActivityRecognizedService.spoofActivityRecogniser(getApplicationContext(), payload); } else { Home.toaststaticnext("Receiving motion updates from a different master! Make only one the master!"); } } } else if (action.equals("sra")) { if ((Home.get_follower() || Home.get_master())) { if (Pref.getBooleanDefaultFalse("accept_remote_snoozes")) { try { long snoozed_time = 0; String sender_ssid = ""; try { snoozed_time = Long.parseLong(payload); } catch (NumberFormatException e) { String ii[] = payload.split("\\^"); snoozed_time = Long.parseLong(ii[0]); if (ii.length > 1) sender_ssid = JoH.base64decode(ii[1]); } if (!Pref.getBooleanDefaultFalse("remote_snoozes_wifi_match") || JoH.getWifiFuzzyMatch(sender_ssid, JoH.getWifiSSID())) { if (Math.abs(JoH.tsl() - snoozed_time) < 300000) { if (JoH.pratelimit("received-remote-snooze", 30)) { AlertPlayer.getPlayer().Snooze(xdrip.getAppContext(), -1, false); UserError.Log.ueh(TAG, "Accepted remote snooze"); JoH.static_toast_long("Received remote snooze!"); } else { Log.e(TAG, "Rate limited remote snooze"); } } else { UserError.Log.uel(TAG, "Ignoring snooze as outside 5 minute window, sync lag or clock difference"); } } else { UserError.Log.uel(TAG, "Ignoring snooze as wifi network names do not match closely enough"); } } catch (Exception e) { UserError.Log.e(TAG, "Exception processing remote snooze: " + e); } } else { UserError.Log.uel(TAG, "Rejecting remote snooze"); } } } else if (action.equals("bgs")) { Log.i(TAG, "Received BG packet(s)"); if (Home.get_follower() || WholeHouse.isEnabled()) { final String bgs[] = payload.split("\\^"); for (String bgr : bgs) { BgReading.bgReadingInsertFromJson(bgr); } if (Pref.getBooleanDefaultFalse("follower_chime") && JoH.pratelimit("bgs-notify", 1200)) { JoH.showNotification("New glucose data @" + JoH.hourMinuteString(), "Follower Chime: will alert whenever it has been more than 20 minutes since last", null, 60311, true, true, true); } } else { Log.e(TAG, "Received remote BG packet but we are not set as a follower"); } // Home.staticRefreshBGCharts(); } else if (action.equals("bfb")) { final String bfb[] = payload.split("\\^"); if (Pref.getString("dex_collection_method", "").equals("Follower")) { Log.i(TAG, "Processing backfill location packet as we are a follower"); staticKey = CipherUtils.hexToBytes(bfb[1]); final Handler mainHandler = new Handler(getMainLooper()); final Runnable myRunnable = new Runnable() { @Override public void run() { try { new WebAppHelper(new GcmListenerSvc.ServiceCallback()).executeOnExecutor(xdrip.executor, getString(R.string.wserviceurl) + "/joh-getsw/" + bfb[0]); } catch (Exception e) { Log.e(TAG, "Exception processing run on ui thread: " + e); } } }; mainHandler.post(myRunnable); } else { Log.i(TAG, "Ignoring backfill location packet as we are not follower"); } } else if (action.equals("bfr")) { if (Pref.getBooleanDefaultFalse("plus_follow_master")) { Log.i(TAG, "Processing backfill location request as we are master"); final long remoteRecent = JoH.tolerantParseLong(payload, 0); final BgReading bgReading = BgReading.last(); if (bgReading != null && bgReading.timestamp > remoteRecent) { GcmActivity.syncBGTable2(); } else { // TODO reduce logging priority UserError.Log.e(TAG, "We do not have any more recent data to offer than: " + (bgReading != null ? JoH.dateTimeText(bgReading.timestamp) : "no data")); } } } else if (action.equals("sensorupdate")) { Log.i(TAG, "Received sensorupdate packet(s)"); if (Home.get_follower() || WholeHouse.isEnabled()) { GcmActivity.upsertSensorCalibratonsFromJson(payload); } else { Log.e(TAG, "Received sensorupdate packets but we are not set as a follower"); } } else if (action.equals("sensor_calibrations_update")) { if (Home.get_master()) { Log.i(TAG, "Received request for sensor calibration update"); GcmActivity.syncSensor(Sensor.currentSensor(), false); } } else if (action.equals("mimg")) { if (Home.get_master() && WholeHouse.isLive()) { Mimeograph.putXferFromJson(payload); } } else if (action.equals("btmm")) { if (Home.get_master_or_follower() && Home.follower_or_accept_follower()) { BloodTest.processFromMultiMessage(bpayload); } else { Log.i(TAG, "Receive multi blood test but we are neither master or follower"); } } else if (action.equals("bgmm")) { if (Home.get_follower()) { BgReading.processFromMultiMessage(bpayload); } else { Log.i(TAG, "Receive multi glucose readings but we are not a follower"); } } else if (action.equals("esup")) { if (Home.get_master_or_follower()) { final String[] segments = payload.split("\\^"); try { ExternalStatusService.update(Long.parseLong(segments[0]), segments[1], false); } catch (ArrayIndexOutOfBoundsException | NumberFormatException e) { UserError.Log.wtf(TAG, "Could not split esup payload"); } } } else if (action.equals("ssom")) { if (Home.get_master()) { if (payload.equals("challenge string")) { UserError.Log.e(TAG, "Stopping sensor by remote"); StopSensor.stop(); } else { UserError.Log.wtf(TAG, "Challenge string failed in ssom"); } } } else if (action.equals("rsom")) { if (Home.get_master()) { try { final Long timestamp = Long.parseLong(payload); StartNewSensor.startSensorForTime(timestamp); } catch (NumberFormatException | NullPointerException e) { UserError.Log.wtf(TAG, "Exception processing rsom timestamp"); } } } else { Log.e(TAG, "Received message action we don't know about: " + action); } } else { // direct downstream message. Log.i(TAG, "Received downstream message: " + message); } } finally { JoH.releaseWakeLock(wl); } } private void sendNotification(String body, String title) { Intent intent = new Intent(this, Home.class); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent, PendingIntent.FLAG_ONE_SHOT); Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); Notification.Builder notificationBuilder = (Notification.Builder) new Notification.Builder(this) .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(title) .setContentText(body) .setAutoCancel(true) .setSound(defaultSoundUri) .setContentIntent(pendingIntent); NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0 /* ID of notification */, notificationBuilder.build()); } private String filter(String source) { if (source == null) return null; return source.replaceAll("[^a-zA-Z0-9 _.-]", ""); } public class ServiceCallback implements Preferences.OnServiceTaskCompleted { @Override public void onTaskCompleted(byte[] result) { final PowerManager.WakeLock wl = JoH.getWakeLock("xdrip-gcm-callback", 60000); try { if (result.length > 0) { if ((staticKey == null) || (staticKey.length != 16)) { Log.e(TAG, "Error processing security key"); } else { byte[] plainbytes = JoH.decompressBytesToBytes(CipherUtils.decryptBytes(result, staticKey)); staticKey = null; UserError.Log.d(TAG, "Plain bytes size: " + plainbytes.length); if (plainbytes.length > 0) { GcmActivity.processBFPbundle(new String(plainbytes, 0, plainbytes.length, "UTF-8")); } else { Log.e(TAG, "Error processing data - empty"); } } } else { Log.e(TAG, "Error processing - no data - try again?"); } } catch (Exception e) { Log.e(TAG, "Got error in BFP callback: " + e.toString()); } finally { JoH.releaseWakeLock(wl); } } } }