Smartphone Button Hack
28.03.2015
Worum geht es in diesem Beitrag ? An vielen Headsets für Smartphones gibt es mindestens einen Knopf, mit dem zum Beispiel die Funktion Play/Pause des Musikspielers benutzt werden kann. Da diese Taste auch mehrmals hintereinander gedrückt oder gehalten werden kann, ließen sich durch die Headset-Taste weitere Aktionen ausführen - zum Beispiel bei dreimaligem Drücken zum vorherigen Lied springen.

Smartphone mit Headset
Für diesen Anwendungsfall gibt es einige Closed Source Apps. Als Open-Source-Anhänger habe ich mich auf die Suche nach einer Open-Source Alternative für die Closed Source App "Headset Button Controller" aus dem Google Play Store gemacht und bin leider nicht fündig geworden. Immerhin gibt es das Xposed-Modul "Xposed Additions", das einfache Aktionen für Buttons ausführen kann und Opensource ist. Die Pro-Version dieses Xposed-Moduls ist allerdings Closed Source.
Xposed Additions
Jetzt stand ich vor der Entscheidung mich in die Opensource-Variante von "Xposed Additions" einzuarbeiten und diese weiterzuentwickeln oder einen schnellen Hack zu schreiben. Aus Zeitgründen habe ich mich für einen schnellen Hack mit einem Shell-Skript und Xposed Additions entschieden. Zunächst benutze ich "Xposed Additions", um die Funktion des Headset Buttons auszuschalten. Dazu muss das Xposed Framework installiert und Xposed Additions über das Icon gestartet werden:

Symbole von Xposed Installer und Xposed Additions
Anschließend wird über "Add new Key" der "Headset Hook" eingerichtet, indem der Headset Button gedrückt wird.

Einstellungen von Xposed Additions Teil 1
Mit "Add new Condition" kann "Screen Off" ausgewählt und sowohl "Click" als auch "Long Press" disabled werden.

Einstellungen von Xposed Additions Teil 2
Jetzt sollte nichts mehr passieren, wenn der Bildschirm des Smartphones ausgeschaltet ist und die Headset-Taste gedrückt wird. Aber wo ist jetzt die Steuerung für den Musikplayer ? Keine Panik, die kommt jetzt.
Der Shell-Skript-Hack
In Android läßt sich auch jenseits des Java-Frameworks Software entwickeln - zum Beispiel als Shell-Skript für Linux. Da Android und CyanogenMod auf Linux basieren sind alle Geräte unter dem Verzeichnis /dev verfügbar. Die verfügbaren Eingabegeräten lassen sich mit dem Befehl "getevent -l" auflisten. Mit Steuerung + c läßt sich getevent beenden. Die folgenden Befehle müssen in einem Terminal im Smartphone mit Root-Rechten ausgeführt werden. Das funktioniert zum Beispiel mit "adb root" und anschließend "adb shell". adb ist die Android Debug Bridge.
getevent -l
...
add device 7: /dev/input/event8
name: "Midas_WM1811 Midas Jack"
add device 8: /dev/input/event1
name: "gpio-keys"
In dem oberen Beispiel ist /dev/input/event8 das "Gerät" für das Headset. Bei event1 bedeutet GPIO: General Purpose Input Ouput - dahinter verbirgt sich zum Beispiel die Home-Taste des Smartphones. Der Befehl getevent -i listet noch detailliertere Informationen auf. Mit den Informationen von getevent lassen sich jetzt die Ereignisse - also die Tastendrücke des Headsets abfragen:
getevent -t -l /dev/input/event8
[ 16116.002037] EV_KEY KEY_MEDIA DOWN
[ 16116.002069] EV_SYN SYN_REPORT 00000000
[ 16116.213047] EV_KEY KEY_MEDIA UP
[ 16116.213087] EV_SYN SYN_REPORT 00000000
Die Option -t des Programms getevent schreibt einen Zeitstempel in Sekunden seit Start des Smartphones aus. Diese Informationen reichen aus, um zu bestimmen, ob die Headset-Taste gedrückt wurde (DOWN) und wie lang sie gedrückt wurde (UP). Zur Weiterverarbeitung der Ausschriften von getevent kann zum Beispiel eine Pipe benutzt werden:
getevent -t -l /dev/input/event8 | grep KEY_MEDIA
Eine Pipe (der senkrechte Strich |) hat die Eigenschaft, dass die Meldungen erst gepuffert werden, bevor sie an das nächste Programm (im oberen Beispiel grep) gegeben werden. Unter normalen Linux-Systemen gibt es ein Programm namens "unbuffer" um diesen Effekt der Pipe abzuschalten. Für Android bzw. CyanogenMod habe ich eine Minimalversion von unbuffer geschrieben:
#include <stdio.h>
__attribute__ ((constructor)) void
stdbuffer () {
setvbuf (stdout, NULL, _IOLBF, 0);
}
EOF
arm-linux-androideabi-gcc -Wall -x c -s stdbuf.c -fPIC -shared -o stdbuf.so
Das Miniprogramm kann mit dem Android Native Development Kit (NDK) auf einem Computer fürs Smartphone cross-compiliert werden. Wer sich den Compilier-Aufwand sparen möchte, kann meine fertige Version stdbuf.so bzw. unbuffer für Android herunterladen. Mit stdbuf.so werden die Ausgaben von getevent sofort an grep weitergereicht:
wget https://www.torsten-traenkner.de/cyanogenmod/hacks/stdbuf.so
adb root
adb push stdbuf.so /data/media/0/stdbuf.so
adb shell "chmod 755 /data/media/0/stdbuf.so"
# log in to the smartphone
adb shell
# run unbuffered getevent:
LD_PRELOAD=/data/media/0/stdbuf.so getevent -t -l /dev/input/event8 | grep KEY_MEDIA
[ 18500.420251] EV_KEY KEY_MEDIA DOWN
[ 18501.548734] EV_KEY KEY_MEDIA UP
Mit diesen Grundlagen habe ich ein kleines Shell-Skript zur Steuerung eines Musikplayers wie zum Beispiel VLC geschrieben.
wget https://www.torsten-traenkner.de/cyanogenmod/hacks/headset.sh
adb push headset.sh /data/media/0/headset.sh
adb shell "chmod 755 /data/media/0/headset.sh"
# create alias for start and stop of the script
cat > alias.txt<<EOF
alias he='/data/media/0/headset.sh </dev/null >/dev/null 2>&1 &'
alias hestop='bash /data/local/tmp/headset.pid ; rm /data/local/tmp/headset.pid'
EOF
adb push alias.txt /data/media/0/alias.txt
adb shell "cat /data/media/0/alias.txt >> /data/media/0/.bashrc"
Auf dem Smartphone kann das Headset-Skript in einem Terminal mit Root-Rechten durch den Alias "he" gestartet und "hestop" gestoppt werden. Das Shell-Skript und damit die Aktionen bei den Tastendrücken des Headsets können sogar auf dem Smartphone mit einem Texteditor geändert werden. Die Default-Einstellungen sind wie folgt:
- Headset-Knopf kurz drücken: Abspielen / Pause
- zweimal kurz drücken: Lautstärke runter
- dreimal kurz drücken: Lautstärke hoch
- lang + kurz: nächstes Lied
- kurz + lang: vorheriges Lied
- lang + zweimal kurz: viel lauter
- zweimal kurz + lang: viel leiser
Anhang: Listing des Shell-Skripts headset.sh
Download of headset.sh is here. The listing is just to read the script online. Technisches Detail am Rande: in meinem Skript habe ich statt einer Pipe einen File-Deskriptor auf ein FIFO benutzt. FIFO bedeutet first in first out und ist ein Zwischenspeicher für die Tastendruck-Ereignisse.
#
# Shell script to handle the key presses of a headset button.
# Works on CyanogenMod and maybe rooted Android.
#
# Precondition: disable the headset button function with Xposed Additions.
# see: http://repo.xposed.info/module/com.spazedog.xposed.additionsgb
#
# Author: Torsten Tränkner
# License: GPLv3
#
# save the process id to kill the process later
echo "Own process id: $$"
mkdir -p /data/local/tmp/
echo "pkill -9 -P $$; kill -9 $$" >> /data/local/tmp/headset.pid
# 0.25 seconds for long key press
THRESHOLD_LONG_PRESS=250000
# seconds for timeout after key press
THRESHOLD_TIMEOUT="0.3"
HEADSET_BUTTON_DEVICE=/dev/input/event8
function main() {
createNecessaryFiles
echo "Start headset button event handling."
codeString=""
while true; do
# read from file descriptor of the fifo
read $TIMEOUT line <&3
# check for timeout of read
if [ $? -eq 142 ];then
echo "Timeout"
TIMEOUT=""
echo "$codeString"
# s is short press, l is long press
case $codeString in
s)
echo "Media play / pause"
input keyevent KEYCODE_MEDIA_PLAY_PAUSE
;;
ss)
echo "Volume down"
input keyevent KEYCODE_VOLUME_DOWN
;;
sss)
echo "Volume up"
input keyevent KEYCODE_VOLUME_UP
;;
ls)
echo "Next song"
input keyevent KEYCODE_MEDIA_NEXT
;;
sl)
echo "Previous song"
input keyevent KEYCODE_MEDIA_PREVIOUS
;;
lss|ssss)
echo "Volume up up"
input keyevent KEYCODE_VOLUME_UP KEYCODE_VOLUME_UP
;;
ssl)
echo "Volume down down"
input keyevent KEYCODE_VOLUME_DOWN KEYCODE_VOLUME_DOWN
;;
*)
echo "Unkown combination."
;;
esac
codeString=""
else
if [[ "$line" =~ .*"KEY_MEDIA DOWN".* ]]; then
#echo "$line"
pressedDownTime=$(getTime "$line")
TIMEOUT=""
elif [[ "$line" =~ .*"KEY_MEDIA UP".* ]]; then
pressedUpTime=$(getTime "$line")
timeDifference=$(expr $pressedUpTime - $pressedDownTime)
# check for short or long key press
if [ $timeDifference -lt $THRESHOLD_LONG_PRESS ];then
echo "short press. $timeDifference"
codeString+="s"
else
echo "long press. $timeDifference"
codeString+="l"
fi
# start timeout for the next keypress
TIMEOUT="-t $THRESHOLD_TIMEOUT"
fi
fi
if [ $finished == true ]; then
echo "Finished"
exit 0
fi
done
}
# get time in microseconds from event string
function getTime() {
echo "$1" | sed 's|^\[ *\([0-9]*\)\.*\([0-9]*\)\].*|\1\2|g'
}
function createNecessaryFiles() {
# create fifo (first in first out) for the events
mkfifo /data/button.fifo > /dev/null 2>&1
# create binary "unbuffer" program
# source code see below
if [ ! -e /data/media/0/stdbuf.so ];then
echo "QlpoOTFBWSZTWSuU0FkAAwJ///////5h9054L+c2\
MP/n/3LsRtRFwFRUBwkQRABa+GBw0AMisKoajAaa\
kxCU9TMymnppEHqNNqep6TT1NMmho9QNGnqeoADQ\
0eptJtIPUMgAzUNlPU9QaiYTUwp6ZAgBoAAAAAAA\
AAAAANA0AAABIkgSbSIDEADQMgDTQYQDIaGgANAa\
AAAAGhoBBgCYTAmEwmmCYjAEwBGhkwCYAAABGAmA\
AAQYAmEwJhMJpgmIwBMARoZMAmAAAARgJgAAGNda\
K9cFEUCQ8DA/IBURwo+44/rw4/ljzl5E/CwKnK3M\
B3dZq9GcJ7o0ONltz4nqtOTwMbAhU6gA+2Dw2z31\
0aGHQi3CBKEhZkBR3TFSvg5xdCjBSlhsooiVsmSn\
J48qDg4dKRLnZkypFj0HqGU7HCw4VW7bTkwvNRGJ\
RVcZDzylFUYss5Ovq0xwq1XMeci2Vw2HXg9jaDNr\
SN1uRC/NZm1FL81rX91ObNxQ/Zyol40YAslBCQFG\
eQEEZAA1FmAqczCGCUMJDEWnDCGoczRXPA7EMe6o\
dMSCRTEzmbY2YLpchk8QDaBzwqWXvzrkqLaKHDJM\
UMCpohyiIcOTW7YsVrJYAF80BtHU75gjTtNpDlCI\
YFLtNIC+YZKUALHNOr566sjSbqWH3CNkgDLIvlEz\
NM6brFrc1dSq8mdVGTRE1vYljM7TDMu8WQCMthpk\
DCkL2CGLWNdGmFEMlRSF6PednEBK94EFSImCahhH\
yFHEWJxGSQAz6O0c91KxYZTQYJdF/Mt+y4YGhHsY\
Ihtiq+mBGXNTg38sxzMLjsQkNrVMQWmi8wD8SDVt\
XTEZj1LK2iwxbC7UNAxSbY3U0FmIAbuWDcNuzTCr\
7uVbYVVjvVIuWp/JCryoxB5TPO0m08KFZ60Hcd5F\
cSn4YTy9eN0I4lEkVPovVfcrLc0wDWUYYM3IJdjB\
1+cHEwT5NtFA5qq6CZW6M9PBf+jGiKEjImnRLqxd\
sx8SJTOcok8eXHMikIIs1Fww02CmXJ1JQwXHSliS\
aXBQ2dcyVBQFDHbRIApMrJZgHC0c5TMBCyyzbbQY\
YghgKSlwPB0OdOYyaIaCVRDmBgZ+KItPHZcLDXgA\
4MyQFUiEsRdCSP4QCSIgSZRJPRIbhILa2AmOY5So\
C2CWi5kImMRodWBJhEsil38+tVVtbSlkFaOrFthQ\
hTnVt61txjALge53LXcBTSITYIqkAYgax1egbpZ2\
GuAOlA8UT8lLwt3sVYsAp29sP7JVZI29sRpd2Abi\
r12f/bzYrRhpWpgCZsTZxIBIBAv357g8D3/F3JFO\
FCQK5TQWQA==" | base64 -d > /data/media/0/stdbuf.so.bz2
bunzip2 /data/media/0/stdbuf.so.bz2
chmod 755 /data/media/0/stdbuf.so
fi
# get events from the headset button and write them to the fifo
# stdbuf is something like unbuffer for immediate writing, see below
(LD_PRELOAD=/data/media/0/stdbuf.so getevent -t -l $HEADSET_BUTTON_DEVICE > /data/button.fifo) &
shellpid=$!
# open file descriptor 3 for reading the fifo
exec 3</data/button.fifo
echo "Process id to kill: $shellpid"
finished=false
# kill getevent and close the file descriptor of the fifo when this script is killed
trap "kill $shellpid > /dev/null 2>&1; finished=true; exec 3>&-" 0 2 3 5 10 13 15
}
main
#####
source code of stdbuf.c
#include <stdio.h>
__attribute__ ((constructor)) void
stdbuffer () {
setvbuf (stdout, NULL, _IOLBF, 0);
}
arm-linux-androideabi-gcc -Wall -x c -s stdbuf.c -fPIC -shared -o stdbuf.so
Update Mai 2015
Mittlerweile kann VLC für Android auch mit m3u-Playlisten umgehen. Dadurch läßt sich zum Beispiel eine Tastenkombination fürs Headset einrichten, bei der eine Playlist gestartet wird:
# start a playlist when headset button is pressed a bit longer twice
echo "Start playlist"
am start -n org.videolan.vlc/.gui.video.VideoPlayerActivity -a android.intent.action.VIEW -d /sdcard/music/playlist.m3u
;;
Position von Touchscreen-Events
Wer jetzt immer noch nicht genug von den technischen Details hat, kann auch die Position vom Tippen auf den Bildschirm (tap) von der Kommandozeile aus abfragen. Nähere Details dazu habe ich in einem weiteren Beitrag geschrieben.
Viel Spaß beim Experimentieren ! Falls noch etwas unklar sein sollte, dann kannst du die Kommentar-Funktion benutzen.