first commit

This commit is contained in:
Dave Umrysh 2021-03-09 21:10:33 -07:00
commit 0ab16f6747
9 changed files with 738 additions and 0 deletions

11
LICENSE.txt Normal file
View File

@ -0,0 +1,11 @@
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Reply to notifications plugin for Cordova
This plugin relies heavily on the great work competed by Javier Rengel and their [NotificationListenerService plugin for Cordova](https://git.umycode.com/dave/NotificationListener-cordova)
This plugin was built with the explicit intent of being used in my 8x8 to Matrix bridge application and therefore will only store the notification intents from app "org.vom8x8.sipua".
If you find it useful please feel free to use it for your own purposes.

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"version": "0.0.1",
"name": "cordova-plugin-reply-to-notification",
"cordova_name": "cordova-plugin-reply-to-notification",
"description": "Reply to notifications plugin for Cordova",
"license": "Apache 2.0",
"repo": "https://git.umycode.com/dave/cordova-plugin-reply-to-notification",
"issue": "https://git.umycode.com/dave/cordova-plugin-reply-to-notification/issues",
"keywords": [
"notification",
"listener",
"android"
],
"platforms": [
"android"
],
"engines": [
{
"name": "cordova",
"version": ">=3.1.0"
}
]
}

53
plugin.xml Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<plugin
xmlns="http://www.phonegap.com/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android"
id="cordova-plugin-reply-to-notification"
version="0.0.1">
<name>cordova-plugin-reply-to-notification</name>
<description>Reply to notifications plugin for Cordova</description>
<license>Apache 2.0</license>
<keywords>notification, listener, android</keywords>
<repo>https://git.umycode.com/dave/cordova-plugin-reply-to-notification</repo>
<issue>https://git.umycode.com/dave/cordova-plugin-reply-to-notification/issues</issue>
<engines>
<engine name="cordova" version=">=3.1.0" />
</engines>
<js-module src="www/reply-to-notification.js" name="ReplyToNotification">
<clobbers target="replyToNotification" />
</js-module>
<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="ReplyToNotification">
<param name="android-package" value="com.umycode.replyToNotification.NotificationCommands"/>
</feature>
</config-file>
<config-file target="AndroidManifest.xml" parent="/*">
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"/>
</config-file>
<config-file target="AndroidManifest.xml" parent="/manifest/application">
<service android:name="com.umycode.replyToNotification.NotificationService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" >
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" ></action>
</intent-filter>
</service>
</config-file>
<source-file src="src/android/NotificationService.java" target-dir="src/com/umycode/replyToNotification"/>
<source-file src="src/android/NotificationCommands.java" target-dir="src/com/umycode/replyToNotification"/>
<source-file src="src/android/Action.java" target-dir="src/com/umycode/replyToNotification"/>
<source-file src="src/android/RemoteInputParcel.java" target-dir="src/com/umycode/replyToNotification"/>
</platform>
</plugin>

117
src/android/Action.java Normal file
View File

@ -0,0 +1,117 @@
package com.umycode.replyToNotification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.RemoteInput;
import android.util.Log;
import java.util.ArrayList;
import com.umycode.replyToNotification.RemoteInputParcel;
public class Action implements Parcelable {
private final String text;
private final String packageName;
private final PendingIntent p;
private final boolean isQuickReply;
private final ArrayList<RemoteInputParcel> remoteInputs = new ArrayList<>();
public Action(Parcel in) {
text = in.readString();
packageName = in.readString();
p = in.readParcelable(PendingIntent.class.getClassLoader());
isQuickReply = in.readByte() != 0;
in.readTypedList(remoteInputs, RemoteInputParcel.CREATOR);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(text);
dest.writeString(packageName);
dest.writeParcelable(p, flags);
dest.writeByte((byte) (isQuickReply ? 1 : 0));
dest.writeTypedList(remoteInputs);
}
public Action(String text, String packageName, PendingIntent p, RemoteInput remoteInput, boolean isQuickReply) {
this.text = text;
this.packageName = packageName;
this.p = p;
this.isQuickReply = isQuickReply;
remoteInputs.add(new RemoteInputParcel(remoteInput));
}
public Action(NotificationCompat.Action action, String packageName, boolean isQuickReply) {
this.text = action.title.toString();
this.packageName = packageName;
this.p = action.actionIntent;
if(action.getRemoteInputs() != null) {
int size = action.getRemoteInputs().length;
for(int i = 0; i < size; i++)
remoteInputs.add(new RemoteInputParcel(action.getRemoteInputs()[i]));
}
this.isQuickReply = isQuickReply;
}
public void sendReply(Context context, String msg) throws PendingIntent.CanceledException {
Intent intent = new Intent();
Bundle bundle = new Bundle();
ArrayList<RemoteInput> actualInputs = new ArrayList<>();
for (RemoteInputParcel input : remoteInputs) {
Log.i("", "RemoteInput: " + input.getLabel());
bundle.putCharSequence(input.getResultKey(), msg);
RemoteInput.Builder builder = new RemoteInput.Builder(input.getResultKey());
builder.setLabel(input.getLabel());
builder.setChoices(input.getChoices());
builder.setAllowFreeFormInput(input.isAllowFreeFormInput());
builder.addExtras(input.getExtras());
actualInputs.add(builder.build());
}
RemoteInput[] inputs = actualInputs.toArray(new RemoteInput[actualInputs.size()]);
RemoteInput.addResultsToIntent(inputs, intent, bundle);
p.send(context, 0, intent);
}
public ArrayList<RemoteInputParcel> getRemoteInputs() {
return remoteInputs;
}
public boolean isQuickReply() {
return isQuickReply;
}
public String getText() {
return text;
}
public PendingIntent getQuickReplyIntent() {
return isQuickReply ? p : null;
}
public String getPackageName() {
return packageName;
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
public Action createFromParcel(Parcel in) {
return new Action(in);
}
public Action[] newArray(int size) {
return new Action[size];
}
};
}

View File

@ -0,0 +1,326 @@
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<LinkedList<Action>> intent_stacks = new LinkedList<LinkedList<Action>>();
protected static List<String> intent_rooms = new LinkedList<String>();
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<String> 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);
}
}
}
}

View File

@ -0,0 +1,87 @@
package com.umycode.replyToNotification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
public class NotificationService extends NotificationListenerService {
//http://developer.android.com/reference/android/service/notification/NotificationListenerService.html
private static final String TAG = NotificationService.class.getSimpleName();
//TODO store this in config
private static final String IGNORE_PKG = "snapdragon,com.google.android.googlequicksearchbox";
private static int notificationId = 1;
private static List<StatusBarNotification> notifications ;
public static boolean enabled = false;
private static Context context = null;
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "onCreate");
enabled = true;
notifications = new ArrayList<StatusBarNotification>();
context = this;
}
@Override
public void onDestroy() {
Log.i(TAG, "onDestroy");
enabled = false;
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
//Do not send notifications from this app (can cause an infinite loop)
Log.d(TAG, "notification package name " + sbn.getPackageName());
String pk = sbn.getPackageName();
if (pk.equals("android") || ignorePkg(pk) || sbn.isOngoing()) Log.d(TAG, "Ignore notification from pkg " + pk);
else {
NotificationCommands.notifyListener(sbn);
addNotification(sbn);
}
}
private boolean ignorePkg(String pk){
for(String s: IGNORE_PKG.split(",")) if (pk.contains(s)) return true;
return false;
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
//debugNotification(sbn);
}
private void addNotification(StatusBarNotification msg){
notifications.add(msg);
}
public static void removeAll(){
try {
for (StatusBarNotification n : notifications) remove(n);
notifications.clear();
} catch (Exception e){
Log.e(TAG, "Unable to remove notifications",e);
}
}
private static void remove(StatusBarNotification n){
String ns = Context.NOTIFICATION_SERVICE;
NotificationManager nMgr = (NotificationManager) context.getApplicationContext().getSystemService(ns);
int id = n.getId();
String tag = n.getTag();
Log.i("Cancelling notification ", tag + ", " + id);
nMgr.cancel(tag, id);
}
}

View File

@ -0,0 +1,88 @@
package com.umycode.replyToNotification;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.app.RemoteInput;
/**
* Created by JJ on 05/08/15.
*/
public class RemoteInputParcel implements Parcelable {
private String label;
private String resultKey;
private String[] choices = new String[0];
private boolean allowFreeFormInput;
private Bundle extras;
public RemoteInputParcel(RemoteInput input) {
label = input.getLabel().toString();
resultKey = input.getResultKey();
charSequenceToStringArray(input.getChoices());
allowFreeFormInput = input.getAllowFreeFormInput();
extras = input.getExtras();
}
public RemoteInputParcel(Parcel in) {
label = in.readString();
resultKey = in.readString();
choices = in.createStringArray();
allowFreeFormInput = in.readByte() != 0;
extras = in.readParcelable(Bundle.class.getClassLoader());
}
public void charSequenceToStringArray(CharSequence[] charSequence) {
if(charSequence != null) {
int size = charSequence.length;
choices = new String[charSequence.length];
for (int i = 0; i < size; i++)
choices[i] = charSequence[i].toString();
}
}
public String getResultKey() {
return resultKey;
}
public String getLabel() {
return label;
}
public CharSequence[] getChoices() {
return choices;
}
public boolean isAllowFreeFormInput() {
return allowFreeFormInput;
}
public Bundle getExtras() {
return extras;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(label);
dest.writeString(resultKey);
dest.writeStringArray(choices);
dest.writeByte((byte) (allowFreeFormInput ? 1 : 0));
dest.writeParcelable(extras, flags);
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
public RemoteInputParcel createFromParcel(Parcel in) {
return new RemoteInputParcel(in);
}
public RemoteInputParcel[] newArray(int size) {
return new RemoteInputParcel[size];
}
};
}

View File

@ -0,0 +1,26 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
"use strict";
module.exports = {
// value must be an ArrayBuffer
listen: function (success, failure) {
console.log("Calling cordova listen method");
cordova.exec(success, failure, 'ReplyToNotification', 'listen', []);
},
replytonotification: function (arg0, success, error){
console.log("Calling replytonotification method");
cordova.exec(success, error, 'ReplyToNotification', 'replytonotification', [arg0]);
}
};