Konubinix' opinionated web of thoughts

Access Hardware Volume Keys in Kivy

Fleeting

when 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.

Notes linking here