termux / termux-packages

A package build system for Termux.
https://termux.dev
Other
12.62k stars 2.91k forks source link

Is it possible to call SDK API (`AudioManager`) without using Termux:API? #20605

Open knyipab opened 2 weeks ago

knyipab commented 2 weeks ago

Is it correct that if the SDK function does not require Android context, then it would not need Termux:API? Just like TermuxAm am? But any simpler example?

Indeed, I am trying to enable a pipewire package feature to enumerate all audio devices. It requires to call AudioManager.getDevices() and AudioManager.OnCommunicationDeviceChangedListener() in Java. And any simple code available on termux-package or anywhere else? Thanks!

Feel free to move it to elsewhere if it's not appropriate here, though I am trying enhance the pipewire pacakge.

twaik commented 2 weeks ago

Actually there is some sort of way you can use. But it is quirky and weird, but works for termux-x11. If you want to use Android SDK APIs you must have JVM. You can obtain one by using app_process. See https://github.com/termux/termux-x11/blob/367a2b1be70a038a4b4b3f0eed04c5097e769dfc/termux-x11 . It is pretty simple if you used java on regular PC. You start app_process with CLASSPATH pointing to your apk and specifying class in commandline. JVM initialises itself and performs public static void main(String args[]) of your class.

twaik commented 2 weeks ago

Probably it will be a good idea to run the whole pipewire server in your JVM instance to not trigger JVM instantiating every time you should enumerate devices or do any other stuff (it is pretty much resource intensive operation). You can link JNI library by using System.load so it will be like regular Java application.

knyipab commented 2 weeks ago

Thank you! Termux:X11 seems to be a good example and starting point.

I believe that AudioManager.OnCommunicationDeviceChangedListener() will need a long standing instance to keep a callback function alive. It seems reasonable and practical to write a pipewire module that starts a standby JVM instance with app_process (using popen()?) and that JVM instance will notify the module about any changes in audio devices through JNI. The module will also notify the JVM instance to shut down as the user terminate pipewire and the module.

For the idea of "pipewire server in JVM instance", honestly, I have no experience and understanding about using C/C++ codes in a Java Android project (so you may see that I never touch C/C++ code in Termux:X11 project). I'm even new to using JNI. The aforementioned solutions seem sensible and easier to implement for me and I will study and develop it perhaps for few days before encountering issues and coming back here.

All in all, your info is so helpful. Thanks.

twaik commented 2 weeks ago

In the case if you want to use pipewire only with Android audio subsystem it will be better to keep everything in single process. If you have any questions about this I am here to help.

No need to be afraid of JNI. You can try to make main function of your class native, in this case you will need to save pointer to JavaVM somewhere in memory (where you can access it from your module), convert arguments from Java format to C format with code like this and invoke regular pipewire's main function with argc and argv you generated. Of course pipewire's main executable code should be compiled as a shared library.

knyipab commented 5 days ago

May I continue my question on IPC & JNI in xfce4-battery-plugin here. I still can't understand how IPC is not needed, cuz popen() app_process should spawn a new process and JNI call could not contact with the calling process. Tested with code below. I know that it will be fine if all native code is initiated by the JVM instance, but I believe that xfce4-battery-plugin and pipewire will be the opposite to have its C/C++ program to initiate a long-standing JVM instance.

build script

./gradlew build
chmod -w ./app/build/outputs/apk/release/app-release-unsigned.apk
g++ --shared -o "./libnative.so" "./native.cpp"
g++ -o "./main" "./main.cpp" -lnative -L"./"

run script

LD_LIBRARY_PATH=./ ./main

output

battery status set to 4
battery status = -1
battery status = -1
battery status = -1

Source code

Loader.java

public class Loader {
    static {
        System.loadLibrary("native");
    }
    public static native void setBatteryStatus(int status);

    public static void main(String[] args) throws InterruptedException {
        // adatped from scrcpy: 
        // https://github.com/Genymobile/scrcpy/blob/v2.4/server/src/main/java/com/genymobile/scrcpy/Workarounds.java
        // https://github.com/Genymobile/scrcpy/blob/v2.4/server/src/main/java/com/genymobile/scrcpy/FakeContext.java
        Context context;
        Looper.prepareMainLooper();
        try {
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
            activityThreadConstructor.setAccessible(true);
            Object activityThread = activityThreadConstructor.newInstance();

            Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
            context = (Context) getSystemContextMethod.invoke(activityThread);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
        BatteryManager batteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE);
        setBatteryStatus(batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS));
    }
}

native.cpp

#include <iostream>
#include <jni.h>
#include <unistd.h>

static int battery_status = -1;

extern "C" {
    JNIEXPORT void JNICALL Java_com_termux_xfce4_battery_Loader_setBatteryStatus(JNIEnv* env, jobject thisObject, jint status);
};

JNIEXPORT void JNICALL Java_com_termux_xfce4_battery_Loader_setBatteryStatus(JNIEnv* env, jobject thisObject, jint status) {
    battery_status = status;
    std::cout << "battery status set to " << battery_status << std::endl;
}

void loop_monitor() {
    FILE* jvm_battery = popen("LD_LIBRARY_PATH= LD_PRELOAD= /system/bin/app_process -Djava.class.path=./app/build/outputs/apk/release/app-release-unsigned.apk -Djava.library.path=./ -Xnoimage-dex2oat / com.termux.xfce4.battery.Loader", 
        "r");
    char buffer[128];
    while (fgets(buffer, sizeof(buffer), jvm_battery) != NULL) {
        std::cout << buffer;
    }
    while (true) {
        std::cout << "battery status = " << battery_status << std::endl;
        sleep(1);
    }
}

main.cpp

void loop_monitor();

int main(int argc, char *argv[]) {
    loop_monitor();
}
twaik commented 5 days ago

Disclaimer: it is not long, it is detailed. And I do not think you are stupid. I do not know what is your background and don't want any misunderstandings.

Instantiating context.

My advice is to avoid instantiating context this way since on some heavily modified ROMs (like on some LG, Xiaomi and Huawei firmwares) it may fail. Vendors like to put some code in ActivityThread constructor and fake context creating always fails.

https://github.com/termux/termux-x11/blob/360b6042911ad704cde1e7f40d2986695e654b32/app/src/main/java/com/termux/x11/CmdEntryPoint.java#L185-L211

Using JNI and interprocess communication.

In the case if your target is only to send current battery percentage you can use java without JNI. You can send some socket (one part of socketpair) to your child process (closing parent part of socket in child's part of process after fork() and before exec, closing child part after fork in parent process after fork()), removing close-on-exec flag from child's socket, passing number of the socket as environment variable or commandline argument to app_process (which is inherited by application), adopting socket with android.os.ParcelFileDescriptor::adoptFd, wrapping it with InputStream and OutputStream. These streams can be used for requests from battery plugin and responses from application. Or whatever you need.

There are many ways of communication through sockets between Java and C. I'll explain the pretty much simple one. C side is easy. You create struct like

struct message {
    int32_t magic;
    int32_t type;
    int32_t value;
}

It does not matter how much field this struct will have and what kind of messages you will have there. The only thing you should care of is the size of this struct and the size of every single member (use sizeof).

The Java side is a bit more complicated. You can write bytes to OutputStream. I say "bytes" and not "integers" because OutputStream can use only bytes. Also you should be aware that (AFAIK, not sure) C is little endian and Java is big endian so you should convert integer to 4 bytes and flip bytes like

    public static void write(int value, OutputStream outputStream) throws IOException {
        outputStream.write(value & 0xFF);
        outputStream.write((value >> 8) & 0xFF);
        outputStream.write((value >> 16) & 0xFF);
        outputStream.write((value >> 24) & 0xFF);
    }

Make sure int in C and int/Integer in Java have the same size, I am not so sure about this (sizeof in C and int.BYTES or Integer.BYTES in Java).

In java you can write the C's struct message like

write (stream, /*MAGIC*/ 0xDEADBEEF); 
write (stream, /*TYPE*/ 0x1);
write (stream, /*VALUE*/ 0x16);

In C you can read it with

struct message buffer = {0};
read(fd, &buffer, sizeof(buffer));

And all fields will be filled. You can use magic field to check if the data you read is fine. In the case if magic is not right you can skip this data package, drain the pipe/socket to make sure there are no other parts of data packages (because we do not know where we messed up), display reading socket warning (like messed up data package, skipping it) and wait for other data packages (requests? responses?).

twaik commented 5 days ago

Also socket can be used to track if parent process is alive and when he dies (you can use System.exit(0) when you get IOException during reading/writing).

knyipab commented 5 days ago

Thanks and the explanation is very helpful, cuz my IPC experience is limited to websocket with JSON which is so unnatural to C, named pipe between shell and python asyncio (which is a horrible experience). Your information is useful and I will take it as an exercise.