package com.umycode.replyToNotification; import android.view.Gravity; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.util.Log; import org.apache.cordova.PluginResult; import android.service.notification.StatusBarNotification; import android.os.Bundle; import java.util.Set; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.support.v4.app.RemoteInput; import android.app.Notification; import android.os.Build; import android.text.TextUtils; import com.umycode.replyToNotification.Action; import android.content.Context; import android.content.Intent; import android.widget.Toast; import android.os.Parcel; import java.util.Arrays; import android.util.Base64; import java.util.List; import java.util.LinkedList; import java.io.PrintWriter; import java.io.StringWriter; import android.app.PendingIntent; public class NotificationCommands extends CordovaPlugin { protected static LinkedList> intent_stacks = new LinkedList>(); protected static List intent_rooms = new LinkedList(); private static final String TAG = "NotificationCommands"; private static final String LISTEN = "listen"; private static final String[] REPLY_KEYWORDS = {"reply", "android.intent.extra.text","message_key"}; private static final CharSequence REPLY_KEYWORD = "reply"; private static final CharSequence INPUT_KEYWORD = "input"; // note that webView.isPaused() is not Xwalk compatible, so tracking it poor-man style private boolean isPaused; private static CallbackContext listener; private static Context current_context; @Override public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException { if (!NotificationManagerCompat.getEnabledListenerPackages (this.cordova.getActivity().getApplicationContext()).contains(this.cordova.getActivity().getApplicationContext().getPackageName())) { Toast.makeText(this.cordova.getActivity().getApplicationContext(), "Please Enable Notification Access", Toast.LENGTH_LONG).show(); //service is not enabled try to enabled by calling... this.cordova.getActivity().getApplicationContext().startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); callbackContext.error(TAG+". Need permissions to be granted."); System.exit(0); return false; } else { Log.i(TAG, "Received action " + action); current_context = this.cordova.getActivity().getApplicationContext(); if (LISTEN.equals(action)) { setListener(callbackContext); return true; }else if (action.equals("replytonotification")) { if(args.length() != 0){ String room_name = args.getJSONObject(0).getString("room_name"); String message = args.getJSONObject(0).getString("message"); // Do we have an existing intent for this int indexOfRoomName = intent_rooms.indexOf(room_name); if (indexOfRoomName > -1) { try{ replyToRoom(callbackContext,indexOfRoomName, message); PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } catch (Exception e){ Log.e(TAG, "Unable to send notification "+ e); PluginResult result = new PluginResult(PluginResult.Status.OK, "Unable to send notification "+ e); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } }else{ Log.e(TAG, "Unable to send notification: Room has not been queued yet"); PluginResult result = new PluginResult(PluginResult.Status.OK, "Unable to send notification: Room has not been queued yet"); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } } return true; } else { callbackContext.error(TAG+". " + action + " is not a supported function."); return false; } } } @Override public void onPause(boolean multitasking) { this.isPaused = true; } @Override public void onResume(boolean multitasking) { this.isPaused = false; } public void setListener(CallbackContext callbackContext) { Log.i("Notification", "Attaching callback context listener " + callbackContext); listener = callbackContext; PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } public static void notifyListener(StatusBarNotification n){ if (listener == null) { Log.e(TAG, "Must define listener first. Call notificationListener.listen(success,error) first"); return; } try { JSONObject json = parse(n); // Store the notification for later Action reply_result = getQuickReplyAction(n.getNotification(),json.getString("package")); if(reply_result!=null && json.getString("package").equals("org.vom8x8.sipua") ){ // Store the intent if (json.has("conversationTitle")) { int indexOfRoomName = intent_rooms.indexOf(json.getString("conversationTitle")); if (indexOfRoomName > -1) { addIntentToList(indexOfRoomName,reply_result); }else{ intent_rooms.add(json.getString("conversationTitle")); addIntentToList(-1,reply_result); } }else{ int indexOfRoomName = intent_rooms.indexOf(json.getString("title")); if (indexOfRoomName > -1) { addIntentToList(indexOfRoomName,reply_result); }else{ intent_rooms.add(json.getString("title")); addIntentToList(-1,reply_result); } } } PluginResult result = new PluginResult(PluginResult.Status.OK, json); Log.i(TAG, "Sending notification to listener " + json.toString()); result.setKeepCallback(true); listener.sendPluginResult(result); } catch (Exception e){ Log.e(TAG, "Unable to send notification "+ e); listener.error(TAG+". Unable to send message: "+e.getMessage()); } } private static JSONObject parse(StatusBarNotification n) throws JSONException{ JSONObject json = new JSONObject(); Bundle extras = n.getNotification().extras; json.put("package", n.getPackageName()); Set keys = extras.keySet(); /* Iterate over all keys of the bundle to give back all the information available */ for (String key : keys) { try { String printKey = key; /* If key has a prefix android., this will be removed. */ if(printKey.indexOf("android.")==0 && printKey.length()>8){ printKey = printKey.substring(8,key.length()); } // json.put(key, bundle.get(key)); see edit below json.put(printKey, JSONObject.wrap(extras.get(key))); } catch(JSONException e) { Log.d(TAG,e.getMessage()); } } return json; } private static String getExtraLines(Bundle extras, String extra){ try { CharSequence[] lines = extras.getCharSequenceArray(extra); return lines[lines.length-1].toString(); } catch( Exception e){ Log.d(TAG, "Unable to get extra lines " + extra); return ""; } } private static String getExtra(Bundle extras, String extra){ try { return extras.get(extra).toString(); } catch( Exception e){ return ""; } } public static Action getQuickReplyAction(Notification n, String packageName) { NotificationCompat.Action action = null; if(Build.VERSION.SDK_INT >= 24) action = getQuickReplyAction(n); if(action == null) action = getWearReplyAction(n); if(action == null) return null; return new Action(action, packageName, true); } private static NotificationCompat.Action getQuickReplyAction(Notification n) { for(int i = 0; i < NotificationCompat.getActionCount(n); i++) { NotificationCompat.Action action = NotificationCompat.getAction(n, i); if(action.getRemoteInputs() != null) { for (int x = 0; x < action.getRemoteInputs().length; x++) { RemoteInput remoteInput = action.getRemoteInputs()[x]; if (isKnownReplyKey(remoteInput.getResultKey())) return action; } } } return null; } private static NotificationCompat.Action getWearReplyAction(Notification n) { NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(n); for (NotificationCompat.Action action : wearableExtender.getActions()) { if(action.getRemoteInputs() != null) { for (int x = 0; x < action.getRemoteInputs().length; x++) { RemoteInput remoteInput = action.getRemoteInputs()[x]; if (isKnownReplyKey(remoteInput.getResultKey())) return action; else if (remoteInput.getResultKey().toLowerCase().contains(INPUT_KEYWORD)) return action; } } } return null; } private static boolean isKnownReplyKey(String resultKey) { if(TextUtils.isEmpty(resultKey)) return false; resultKey = resultKey.toLowerCase(); for(String keyword : REPLY_KEYWORDS) if(resultKey.contains(keyword)) return true; return false; } private static void addIntentToList(int indexOfRoomName, Action reply_result){ if(indexOfRoomName >= 0){ // we are adding to an existing stack intent_stacks.get(indexOfRoomName).add(reply_result); // is it too long now? if(intent_stacks.get(indexOfRoomName).size() > 10){ Log.i(TAG, "Removing intent from stack "); intent_stacks.get(indexOfRoomName).removeFirst(); } }else{ intent_stacks.add(new LinkedList<>()); intent_stacks.get(intent_stacks.size()-1).add(reply_result); // is it too long now? if(intent_stacks.get(intent_stacks.size()-1).size() > 10){ Log.i(TAG, "Removing intent from stack "); intent_stacks.get(intent_stacks.size()-1).removeFirst(); } } } public static void replyToRoom(CallbackContext callbackContext,int indexOfRoomName, String message) throws PendingIntent.CanceledException{ try{ if(intent_stacks.get(indexOfRoomName).size()>0){ Action reply_result = intent_stacks.get(indexOfRoomName).get(0); reply_result.sendReply(current_context, message); }else{ PluginResult result = new PluginResult(PluginResult.Status.OK, "Unable to send notification. No stored intents."); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } } catch (Exception e){ Log.e(TAG, "Unable to send notification "+ e); // Can we try the next one? StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); String exceptionAsString = sw.toString(); if(exceptionAsString.contains("CanceledException")){ Log.i(TAG, "Removing intent from stack "); intent_stacks.get(indexOfRoomName).removeFirst(); replyToRoom(callbackContext,indexOfRoomName, message); }else{ PluginResult result = new PluginResult(PluginResult.Status.OK, "Unable to send notification "+ e); result.setKeepCallback(true); callbackContext.sendPluginResult(result); } } } }