Du bist hier:Start»CyanogenMod»Hacks»Button-Hack

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

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:

Xposed Additions Icon

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

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

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.

# run this command as root on the smartphone

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:

# list key presses of the headset button

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:

# filter output of getevent

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:

cat > stdbuf.c<<EOF
#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:

# copy stdbuf.so to the smartphone from the computer:

wget http://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.

# copy headset.sh to the smartphone:

wget http://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.

#!/system/xbin/bash

#
# 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:

        ll)
          # 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.

Kommentar schreiben