| app | ||
| gradle | ||
| .gitignore | ||
| build.gradle.kts | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| settings.gradle.kts | ||
Notifactor (Android App)
A lightweight native Android app that intercepts notifications on the device and triggers actions based on configurable rules.
The app uses Android's NotificationListenerService API. Once granted notification access
it receives a callback for every notification posted on the device. It then checks each notification
against configured rules and runs the configured action.
Why would you want this?
I don't know why you would want this but I can tell you why I made it: to make it easier to
control some functions on Android devices used by people I often help using them in some way. My
mother's Android TV (which I use to communicate with her through Linphone), phone and tablet sometimes
stop doing the right thing. I live about 1300 km to the north of where she lives so I can't just hop
on my bike to fix things. Thus far I relied on a set of Termux scripts on these devices to keep a
reverse ssh tunnel open to an endpoint on my server but this has a number of drawbacks: the tunnel
is not always there when I need it due to WiFi dropouts and other similar problems and the constant
connection uses battery power on the phone and tablet. If only I could cause the tunnel to be
created when I need it and brought down when it is not needed... Well, that is possible using
Notifactor by sending a notification on a specific channel (ntfy refers to these as topics)
whereupon Notifactor runs a Termux script which manages the tunnel (etc.).
Features
- Define rules to intercept notifications filtered by package name, title and/or body content, e.g. 'notification source io.heckel.ntfy, title contains the word NowListen'
- Per-rule actions:
- Run Termux script with the notification title as
$1, notification body as$2and notification source package as$3 - Launch app - open any installed app by package name, e.g. org.mozilla.firefox
- HTTP request - POST
{package, title, text}JSON to a (webhook) URL - Send custom Intent - broadcast a custom intent action with title/text/package as extras
- Run Termux script with the notification title as
Why not use Tasker or Macrodroid or ...
By all means use them if you have them. I do not and I tend to shun proprietary apps so I made this instead. They're also far more complicated to set up than Notifactor so if all you want is to fire up some actions based on received notifications Notifactor is more than sufficient.
But but security...
Yes, that is really something to think about. With great power - and Notifactor can give you a lot of power since it essentially is a notification-based remote procedure call transport - comes great responsibility. It would be quite foolish to just send commands through notifications to be executed on the device. Something like the following can be used to mitigate the risks:
controller
# on the sending (controller) side:
CHANNEL="j235RGHDR3465"
SECRET="57c403cda50e9151019f2343479b1f9d"
notification_url="https://ntfy.example.org/$CHANNEL"
notification_id_stack="$HOME/.config/notifactor/${CHANNEL}.stack"
# Use a random salt and the secret to create a command hash
# (Yes, md5 is supposedly broken but for this purpose it is just fine)
hash_command () {
cmd="$1"
salt=$(echo "$RANDOM$RANDOM$RANDOM"|md5sum)
salt=${salt:0:5}
data=$(echo -n "${salt}${SECRET}${cmd}"|md5sum)
data="$salt${data:0:32}"
echo -n "$data"
}
# Send the command hash to the notification channel
send_notification () {
cmd="$1"
message_id=$(curl -s -X POST -d $(hash_command "$cmd") $notification_url|jq -r '.id')
touch "$notification_id_stack"
echo $message_id >> $notification_id_stack
}
# Delete notifications _on the receiving devices_
delete_notifications () {
if [ -f "$notification_id_stack" ]; then
cat $notification_id_stack|while read id; do
curl -s -X DELETE $notification_url/$id
done
> $notification_id_stack
fi
}
case "$1" in
"command_1")
send_notification "$1"
# do stuff for command 1
...
# check whether the command is complete, then...
delete_notifications
;;
"command_2")
send_notification "$1"
# do stuff for command 2
...
# check whether the command is complete, then...
delete_notifications
;;
*)
echo "$1: unknown command"
;;
esac
controlled device
# on the receiving (controlled device) side
#!/data/data/com.termux/files/usr/bin/bash
# The channel title is set by the app which receives the notifcation. In this example using ntfy
# I have set the title to _Notifactor_ in 'Subscription settings' -> 'Appearance' -> 'Display name'
CHANNEL_TITLE="Notifactor"
SECRET="57c403cda50e9151019f2343479b1f9d"
AVAILABLE_COMMANDS="command_1 command_2"
LOG=$HOME/var/log/notifactor_script.log
# Notifactor calls scripts with the notification title as $1:
if [ "$CHANNEL_TITLE" != "$1" ]; then
echo -e "\nNot my channel: $1\n" >> $LOG
exit
fi
# ... and the notification body text as $2:
hashed_cmd="$2"
unhash () {
salt=${1:0:5}
cmdhash=${1:5}
for cmd in $AVAILABLE_COMMANDS; do
testhash=$(echo -n "${salt}${SECRET}${cmd}"|md5sum)
if [ "${testhash:0:32}" == "$cmdhash" ]; then
echo $cmd
break
fi
done
}
if [ -n "$hashed_cmd" ]; then
cmd=$(unhash "$hashed_cmd")
if [ -n "$cmd" ]; then
case "$cmd" in
"command_1")
printf "\ncommand_1" %s\n" "$(date)" >> $LOG
;;
"command_2")
printf "\ncommand_2 %s\n" "$(date)" >> $LOG
;;
esac
else
printf "\nBad hash at %s\n" "$(date)" >> $LOG
exit
fi
else
printf "\nNo hash at %s\n" "$(date)" >> $LOG
fi
Building the APK
The project currently uses Gradle 9.2.1, it should build with JDK 17 or higher and can be built from the command line or through Android Studio. How, you ask? Well, let's just say that those who need to be told how to build Android packages are better off learning how to do this from better sources than this README.md.
Testing, testing...
Granting notification access
On first run you'll be prompted to grant notification access (for showing error messages). Use the menu to grant notification listener access (mandatory) and Termux access (optional, needed to run Termux scripts).
Run Termux script
Termux script setup
By default Termux does not allow other apps to send it commands but that can be changed by adding
allow-external-apps = true to $HOME/.termux/termux.properties, like so:
In the Termux terminal:
echo "allow-external-apps = true" >> $HOME/.termux/termux.properties
Restart the Termux app (swipe it out of the task switcher or use forced stop) after adding this parameter.
FYI the Termux documentation is inconclusive on the necessity of this setting. The termux Tasker
documentation states it is optional
and only needed to launch tasks outside of $HOME/.termux/tasker while the RUN_COMMAND documentation states it
is mandatory
to execute commands. The latter seems to be closer to the truth than the former since you're greeted
with a fiery red error message if you forget to set it.
Create a script
# in a Termux terminal
mkdir -p $HOME/bin
cat > $HOME/bin/notifactor_test.sh << 'EOF'
#!/data/data/com.termux/files/usr/bin/bash
TITLE="$1" # notification title
TEXT="$2" # notification body text
PKG="$3" # notification source app
echo "$(date): [$PKG] $TITLE - $TEXT" >> $HOME/notifactor.log
EOF
chmod +x $HOME/bin/notifactor_test.sh
Create a rule in the app
This test is based on the assumption you're using the ntfy app (find it on F-Droid) as a notification source. If you're using something else - e.g. Nextpush or Gotify - you'll need to change the value in the Source app package field to match it.
| field | value |
|---|---|
| Source app package | io.heckel.ntfy |
| Title contains | (leave empty to match all) |
| Body contains | (leave empty to match all) |
| Action | Run Termux script |
| Script path | $HOME/bin/notifactor_test.sh |
Now send a few notifications on the configured channel/topic and check the $HMOE/notifactor.log file.
Custom Intent action
For the Send custom Intent action, enter any broadcast action string
(e.g. org.example.ACTION_REQUIRED). The broadcast will carry these extras:
| Extra key | Value |
|---|---|
title |
Notification title |
text |
Notification body |
package |
Source app package name |
A BroadcastReceiver in any other app on the device can listen for this action. As it stands this
action is mostly useful for those writing their own apps but I might extend it later to make it
possible to configure the extras for more flexibility.
HTTP request
The HTTP request option sends the package source, notification title and notification body as
a JSON payload for a POST request to the configured URL. It'll look something like this on the
receiving web server (using https://github.com/svenstaro/dummyhttp as the receiving server) with
the URL set to https://server.example.org:8080 upon receiving a notification through ntfy with
title Webtest and text foo bar baz:
┌─Incoming request
│ POST https://server.example.org:8080/ HTTP/2.0
│ Accept-Encoding: gzip
│ Content-Length: 64
│ Content-Type: application/json; charset=utf-8
│ User-Agent: okhttp/4.12.0
│ Body:
│ {"package":"io.heckel.ntfy","title":"Webtest","text":"foo bar baz"}
Launch app
This should be self-explanatory: any notification which triggers the rule causes the configured app to be launched. The contents of the notification are not forwarded to the app.
Used Permissions
| Permission | Purpose |
|---|---|
INTERNET |
HTTP request (webhook) action |
POST_NOTIFICATIONS |
Show action error notifications |
| Notification listener access | Intercept device notifications use the first menu entry to request this permission |
com.termux.permission.RUN_COMMAND |
Launch Termux scripts use the second menu entry to request this permission |