Modifications to JUCE Android implementation

I’ve been working on some major changes to how JUCE interacts with Android. Mainly driven by the desire to get React Native working with JUCE on Android, but also to allow much easier customisation and the use of multiple JUCE component views within different activities.

Projucer will also leave you custom java files alone! (this has been an annoyance of mine for some time…)

You can get it here https://github.com/adamski/JUCE/tree/android-bridge (rebased against the develop branch)

NOTE: You will need to recompile Projucer and re-export your AndroidStudio project for this to work!

The current version is compatible with React Native 0.31.

I’ve moved all the JNI stuff that interacts with JUCE to the JuceBridge class, and left JuceAppActivity to be as minimal as it needs to be. I’ve also left it in the com.juce package, so you can leave it there if you don’t need to modify it, but if you do you can just copy it over to your package folder e.g. com.mycompany.myapp and make your changes - e.g. inherit from AppCompatActivity, implement the BackButtonHandler etc.
JuceViewHolder has been separated out, and JuceBridge holds a map of these, which are linked together by the component name. It will use the default MainWindow class, but if you want to create multiple JUCE component views across different activities or Android views you can do e.g:

#if JUCE_ANDROID
        /* Set up our windows that will be attached to Android views */
        myViewContainer = new DocumentWindow("My View", Colours::black, DocumentWindow::allButtons);
        myViewContainer->setUsingNativeTitleBar(true); // this removes the JUCE title bar on Android
        myViewContainer->setContentOwned(new MyViewComponent(), true);
        myViewContainer->setFullScreen(true);
        myViewContainer->setVisible(true);
        openGLContext.attachTo (*myViewContainer);  // Note this has to be detached before it can be attached to another component
                                                    // e.g. on switching views or activities
        // Set up more DocumentWindows... 
#else
        // Instantiate MainWindow
        mainWindow = new MainWindow (getApplicationName());
#endif
...
private:
    ScopedPointer<MainWindow> mainWindow;
    ScopedPointer<DocumentWindow> myViewContainer;
    OpenGLContext openGLContext;

Then in your custom activity or view:

    JuceBridge juceBridge = JuceBridge.getInstance();
    juceBridge.setActivityContext(this);  // If this is a new Activity
    JuceViewHolder view = new JuceViewHolder(this);
    JuceBridge.ComponentPeerView peerView = juceBridge.getPeerViewForComponent("My View");
    view.addView(peerView);

I’ve tested it with the HelloWorld and JuceDemo apps, please test it out and let me know any issues in this thread. I’m sending a PR to the JUCE guys too.

Thanks - saw your PR. This all looks interesting, but we probably won’t have chance to look at it properly until at least next week!

Thats all good - it could still do with a little tidying up, I just wanted to put it out there and get some feedback on different use cases.

Just updated the branch with some changes, see edited first post. Changes the Java API.

I also need to integrate a juce static library within a ReactNative environment but so far I’ve run into this problem.
I haven’t checked out your (very promising) solution yet but any chance that this becomes less of an hassle in an upcoming version of Juce? :slight_smile:

@adamski sorry if my question sounds naive but I haven’t touched Android for a while.
My static library is only using the Audio part of juce and therefore does not use any graphical elements from Juce (which I would really like to avoid being bundled in the library).
My idea would be to load this library, let React initialize as usual and then bridge my library as a Native module so that React can trigger sounds in my audio engine.

Is this idea compatible with your initiative?

Yes it should be. The main thing I changed was to decouple the JUCE static library from the Activity, allowing you to create your Android app as usual and then use the parts of JUCE that you wish. Not using and graphical elements from JUCE will make things simpler as you don’t need to worry about the OpenGL context etc.

I know @robclouth has been working on a project like this. Not sure if he’s got onto Android just yet though…

Ok I will try your solution in the upcoming days and let you know of my results. Thank you so much!

Hey!
Yeah I’m using React Native for the GUI of my app. I managed to ditch all the Juce GUI modules finally, but it means you can’t use AudioProcessors. Not really sure why graphics should be a dependency on those but that’s the way it is.
Then make your Juce part as normal, and extend from JUCEApplicationBase. Basically the android app starts mostly as normal like in any Juce app on android if you export for Android Studio. It calls private native void launchApp (String appFile, String appDataDir); in the OnCreate() method. Again, exactly like any Juce-based android app.
I essentially just mashed together a barebones RN and Juce app, and this is how it came out. Works really nicely actually!
Also I made a bridge class that receives and sends serialised messages to and from RN via JNI. There’s probably a better way of doing this, but it works fast enough for a real-time music app.

JUCE_JNI_CALLBACK(com_ovalsound_Oval_OvalBridge, sendMessage, void,
                  (JNIEnv * env, jobject ovalBridge, jint messageId, jstring address,
                   jstring argString)) {
    jboolean isCopy;
    const char* nativeAddressString = env->GetStringUTFChars(address, &isCopy);
    const char* nativeArgString = env->GetStringUTFChars(argString, &isCopy);
    String addressJuce = String(nativeAddressString);
    String argStringJuce = String(nativeArgString);
    env->ReleaseStringUTFChars(address, nativeAddressString);
    env->ReleaseStringUTFChars(argString, nativeArgString);

    // call on message thread
    EngineInterface::Message* aeMessage = new EngineInterface::Message(
        addressJuce, argStringJuce,
        [messageId, env, addressJuce](const String& result) {
            env->CallVoidMethod(ovalBridgeRef, OvalBridge.audioEngineMessageCallback, messageId,
                                javaString(addressJuce).get(), javaString(result).get(),
                                javaString("").get());
        },
        [messageId, env, addressJuce](const String& error) {
            env->CallVoidMethod(ovalBridgeRef, OvalBridge.audioEngineMessageCallback, messageId,
                                javaString(addressJuce).get(), javaString("").get(),
                                javaString(error).get());
        });
    aeMessage->post();
}

Then back to Java (and RN)

void EngineInterface::sendMessage(const String& address, const var& message) {
#if defined(JUCE_ANDROID)
        ovalBridgeRef.callVoidMethod(OvalBridge.audioEngineMessageCallback, -1,
                            javaString(address).get(), javaString(JSON::toString(message, true)).get(),
                            javaString("").get());
#elif defined(JUCE_IOS)
        callback(address, message);
#endif
    }

ovalBridgeRef is set by calling

JUCE_JNI_CALLBACK(com_ovalsound_Oval_OvalBridge, initialise, void, (JNIEnv * env, jobject ovalBridge)) {
    setEnv(env);
    ovalBridgeRef = GlobalRef(ovalBridge);
}

from Java

1 Like

I would say that, unless you need JUCE for GUI stuff (I wanted to reuse some custom GUI code across platforms) then this seems like a much cleaner solution.
I may at some point drop JUCE for GUI altogether, and use OpenGL directly to draw my custom graphics.