Access Hardware Volume Keys in Kivy
Fleetingwhen the application in is focus
need to implement Acticity::dispatchKeyEvent
This needs to be in the main PythonActivity (python-for-android/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java::public class PythonActivity extends SDLActivity {) but nothing was done
I can change the smali code to add some custom behavior, like logging test, by adding it in poc/smali/org/kivy/android/PythonActivity.smali
This prints test and resturn true (to preempt the event).
.method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z
.locals 2
# Log the string "test"
const-string v0, "MyApp"
const-string v1, "test"
invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
const/4 v0, 0x1
return v0
.end method
apktool b poc && apksigner sign --ks your-keystore.jks --ks-key-alias your-alias --ks-pass pass:000000 ./poc/dist/poc.apk
I: Using Apktool 2.10.0 with 4 thread(s).
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Building apk file...
I: Copying unknown files/dir...
I: Built apk into: poc/dist/poc.apk
And I can see the log and no more volume control.
Now, I want to call a callback set by jnius.
in poc/smali/com/example/KeyEventCallback.smali, put
.class public interface abstract Leu/konubinix/konixpoc/KeyEventCallback;
.super Ljava/lang/Object;
# Define the method signature for the callback
.method public abstract onKeyEvent(Landroid/view/KeyEvent;)Z
.end method
In poc/smali/org/kivy/android/PythonActivity.smali
# add the callback attribute
.field public keyEventCallback:Leu/konubinix/konixpoc/KeyEventCallback;
...
.method protected onCreate(Landroid/os/Bundle;)V
...
# init keyEventCallback to null
const/4 v0, 0x0
iput-object v0, p0, Lorg/kivy/android/PythonActivity;->keyEventCallback:Leu/konubinix/konixpoc/KeyEventCallback;
Beware the .locals, it defines v0, v1 etc
In the same file, implement dispatchKeyEvent that calls the callback if it is set
.method public dispatchKeyEvent(Landroid/view/KeyEvent;)Z
.locals 3
const-string v1, "poc"
const-string v2, "Calling call back"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
# Retrieve the keyEventCallback attribute
iget-object v0, p0, Lorg/kivy/android/PythonActivity;->keyEventCallback:Leu/konubinix/konixpoc/KeyEventCallback;
# Check if keyEventCallback is set (not null)
if-eqz v0, :super_call
# Call the callback with the KeyEvent parameter
invoke-interface {v0, p1}, Leu/konubinix/konixpoc/KeyEventCallback;->onKeyEvent(Landroid/view/KeyEvent;)Z
# Get the result of the callback
move-result v0
# Return the result of the callback
return v0
:super_call
const-string v1, "poc"
const-string v2, "No callback"
invoke-static {v1, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
# Fallback to the superclass dispatchKeyEvent if no callback is set
invoke-super {p0, p1}, Lorg/kivy/android/PythonActivity;->dispatchKeyEvent(Landroid/view/KeyEvent;)Z
move-result v0
return v0
.end method
In the python code, implement the callback and register it in the activity
from jnius import autoclass, PythonJavaClass, java_method
# Access the activity
PythonActivity = autoclass('org.kivy.android.PythonActivity')
KeyEvent = autoclass('android.view.KeyEvent')
activity = PythonActivity.mActivity
# Create a Python implementation of KeyEventCallback
class MyKeyEventCallback(PythonJavaClass):
__javainterfaces__ = ['eu/konubinix/konixpoc/KeyEventCallback']
__javacontext__ = 'app'
@java_method('(Landroid/view/KeyEvent;)Z')
def onKeyEvent(self, key_event):
if key_event.getKeyCode() == KeyEvent.KEYCODE_VOLUME_DOWN:
action = key_event.getAction()
if action == KeyEvent.ACTION_DOWN:
print("volume down pressed")
elif action == KeyEvent.ACTION_UP:
print("volume down released")
else:
raise NotImplementedError()
return True # or False depending on your logic
# Set the callback
callback = MyKeyEventCallback()
activity.keyEventCallback = callback
when the application is not
You need to implement a VolumeProvider and initialize a MediaSession, like it is done in MALP (https://gitlab.com/gateship-one/malp/-/blob/master/app/src/main/java/org/gateshipone/malp/application/background/NotificationManager.java?ref_type=heads#L155)
In python, this would look like
from jnius import java_method
from jnius import PythonJavaClass, JavaClass, MetaJavaClass
MediaSession = autoclass("android.media.session.MediaSession")
class MyVolumeProvider(PythonJavaClass, metaclass=MetaJavaClass):
__javaclass__ = 'android/media/VolumeProvider'
__javainterfaces__ = []
def onAdjustVolume(self, direction):
print(direction)
from android import mActivity
mediaSession = MediaSession(mActivity, "konubinix")
volumeprovider = MyVolumeProvider()
print("created volume provider")
mediaSession.setPlaybackToRemote(volumeprovider)
But this won’t work, because android.media.VolumeProvider is an abstract class (not an interface) and can implement only interfaces, not abstract classes.
Therefore, it would need to provide a java implementation on android.media.VolumeProvider that would in turn call the python callbacks.