Search code examples
androidc++multithreadingjava-native-interfacejnienv

Why does the same native thread appear to call methods from different Java threads?


I'm trying to set screen brightness for my activity on the fly. At first, before I enter the ALooper loop, I easily do this via the JNIEnv calls to CallVoidMethod and company. But after 65 iterations of the loop I start consistently getting exceptions like this:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

What makes me wonder is that I do the JNIEnv calls from the same native thread, but the call only starts failing after many attempts.

What's going on here? How can I make sure that I do the Java method calls from the correct Java thread?

Here's the reduced example code:

#include <cmath>
#include <stdexcept>
#include <jni.h>

#include <android/log.h>
#include <android_native_app_glue.h>

#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG,"color-picker", __VA_ARGS__))

class JNIEnvGetter
{
    JavaVM* javaVM = nullptr;
    JNIEnv* jniEnv = nullptr;
    bool threadAttached = false;
public:
    JNIEnvGetter(ANativeActivity* activity)
        : javaVM(activity->vm)
    {
        // Get JNIEnv from javaVM using GetEnv to test whether
        // thread is attached or not to the VM. If not, attach it
        // (and note that it will need to be detached at the end
        //  of the function).
        switch (javaVM->GetEnv((void**)&jniEnv, JNI_VERSION_1_6))
        {
        case JNI_OK:
            LOGD("No need to attach thread");
            break;
        case JNI_EDETACHED:
        {
            const auto result = javaVM->AttachCurrentThread(&jniEnv, nullptr);
            if(result == JNI_ERR)
                throw std::runtime_error("Could not attach current thread");
            LOGD("Thread attached");
            threadAttached = true;
            break;
        }
        case JNI_EVERSION:
            throw std::runtime_error("Invalid java version");
        }
    }
    JNIEnv* env() { return jniEnv; }
    ~JNIEnvGetter()
    {
        if(threadAttached)
          javaVM->DetachCurrentThread();
    }
};

void setBrightness(JNIEnv* env, ANativeActivity* activity, const float screenBrightness)
{
    LOGD("setBrightness()");
    const jclass NativeActivity = env->FindClass("android/app/NativeActivity");
    const jclass Window = env->FindClass("android/view/Window");

    const jmethodID getWindow = env->GetMethodID(NativeActivity, "getWindow",
                                 "()Landroid/view/Window;");
    const jmethodID getAttributes = env->GetMethodID(Window, "getAttributes",
                                     "()Landroid/view/WindowManager$LayoutParams;");
    const jmethodID setAttributes = env->GetMethodID(Window, "setAttributes",
                                     "(Landroid/view/WindowManager$LayoutParams;)V");

    const jobject window = env->CallObjectMethod(activity->clazz, getWindow);
    const jobject attrs = env->CallObjectMethod(window, getAttributes);
    const jclass LayoutParams = env->GetObjectClass(attrs);

    const jfieldID screenBrightnessID = env->GetFieldID(LayoutParams, "screenBrightness", "F");
    env->SetFloatField(attrs, screenBrightnessID, screenBrightness);
    env->CallVoidMethod(window, setAttributes, attrs);
    if(env->ExceptionCheck())
    {
        LOGD("Exception detected");
        env->ExceptionDescribe();
        env->ExceptionClear();
    }
    else
    {
        static int count=0;
        LOGD("Brightness set successfully %d times", ++count);
    }

    env->DeleteLocalRef(attrs);
    env->DeleteLocalRef(window);
}

void android_main(struct android_app* state)
{
    JNIEnvGetter jeg(state->activity);
    const auto env=jeg.env();
    setBrightness(env, state->activity, 1); // works fine

    for(float x=0;;x+=0.001)
    {
        int events;
        struct android_poll_source* source;
        while (ALooper_pollAll(0, nullptr, &events, (void**)&source) >= 0)
        {
            if (source)
                source->process(state, source);

            if (state->destroyRequested != 0)
                return;
        }

        setBrightness(env, state->activity, std::cos(x)); // gets exception
    }
}

Relevant output from adb logcat:

04-20 15:34:45.778 12468 12487 D color-picker: Thread attached
04-20 15:34:45.778 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.779 12468 12487 D color-picker: Brightness set successfully 1 times
04-20 15:34:45.779 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.779 12468 12487 D color-picker: Brightness set successfully 2 times
<...>
04-20 15:34:45.834 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.835 12468 12487 D color-picker: Brightness set successfully 64 times
04-20 15:34:45.835 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.837 12468 12487 D color-picker: Brightness set successfully 65 times
04-20 15:34:45.837 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.837  3176  5876 V WindowManager: Relayout Window{37a74d5 u0 zozzozzz.color_picker/android.app.NativeActivity}: viewVisibility=0 req=720x1232 WM.LayoutParams{(0,0)(fillxfill) sim=#110 ty=1 fl=#81810100 pfl=0x20000 wanim=0x10302fc sbrt=0.9980162 vsysui=0x600 needsMenuKey=2 colorMode=0 naviIconColor=0}
04-20 15:34:45.838 12468 12487 D color-picker: Exception detected
04-20 15:34:45.838 12468 12487 W System.err: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
04-20 15:34:45.838 12468 12487 W System.err:    at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8483)
04-20 15:34:45.839 12468 12487 W System.err:    at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1428)
04-20 15:34:45.839 12468 12487 W System.err:    at android.view.View.requestLayout(View.java:23221)
04-20 15:34:45.839 12468 12487 W System.err:    at android.view.View.setLayoutParams(View.java:16318)
04-20 15:34:45.840 12468 12487 W System.err:    at android.view.WindowManagerGlobal.updateViewLayout(WindowManagerGlobal.java:402)
04-20 15:34:45.840 12468 12487 W System.err:    at android.view.WindowManagerImpl.updateViewLayout(WindowManagerImpl.java:106)
04-20 15:34:45.840 12468 12487 W System.err:    at android.app.Activity.onWindowAttributesChanged(Activity.java:3201)
04-20 15:34:45.840 12468 12487 W System.err:    at android.view.Window.dispatchWindowAttributesChanged(Window.java:1138)
04-20 15:34:45.841 12468 12487 W System.err:    at com.android.internal.policy.PhoneWindow.dispatchWindowAttributesChanged(PhoneWindow.java:3207)
04-20 15:34:45.841 12468 12487 W System.err:    at android.view.Window.setAttributes(Window.java:1191)
04-20 15:34:45.841  2680  2680 I SurfaceFlinger: id=12125 createSurf (720x1280),1 flag=404, zozzozzz.color_picker/android.app.NativeActivity#0
04-20 15:34:45.841 12468 12487 W System.err:    at com.android.internal.policy.PhoneWindow.setAttributes(PhoneWindow.java:4197)
04-20 15:34:45.841 12468 12487 D color-picker: setBrightness()
04-20 15:34:45.842 12468 12487 D color-picker: Exception detected
04-20 15:34:45.842 12468 12487 W System.err: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Solution

  • Thanks to the comment by Michael I learned that android_main indeed doesn't execute in the main thread, and it's expected that the call I was doing to set brightness shouldn't generally work from there. Thanks to this answer, I was able to make the JNIEnv calls run in the main (UI) thread, which made it work.

    What took the most time to find out was how to get the main thread looper. The trick I came up with is to run ALooper_forThread() in a static initializer of a global pointer. This way this function call is guaranteed to be executed in the same thread that dlopens the library, which happens to be the main thread.

    The final code that works for me looks like this:

    #include <cmath>
    #include <stdexcept>
    #include <unistd.h>
    #include <jni.h>
    
    #include <android/log.h>
    #include <android_native_app_glue.h>
    
    #define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG,"color-picker", __VA_ARGS__))
    
    class JNIEnvGetter
    {
        JavaVM* javaVM = nullptr;
        JNIEnv* jniEnv = nullptr;
        bool threadAttached = false;
    public:
        JNIEnvGetter(ANativeActivity* activity)
            : javaVM(activity->vm)
        {
            // Get JNIEnv from javaVM using GetEnv to test whether
            // thread is attached or not to the VM. If not, attach it
            // (and note that it will need to be detached at the end
            //  of the function).
            switch (javaVM->GetEnv((void**)&jniEnv, JNI_VERSION_1_6))
            {
            case JNI_OK:
                LOGD("No need to attach thread");
                break;
            case JNI_EDETACHED:
            {
                const auto result = javaVM->AttachCurrentThread(&jniEnv, nullptr);
                if(result == JNI_ERR)
                    throw std::runtime_error("Could not attach current thread");
                LOGD("Thread attached");
                threadAttached = true;
                break;
            }
            case JNI_EVERSION:
                throw std::runtime_error("Invalid java version");
            }
        }
        JNIEnv* env() { return jniEnv; }
        ~JNIEnvGetter()
        {
            if(threadAttached)
              javaVM->DetachCurrentThread();
        }
    };
    
    void setBrightness(ANativeActivity* activity, const float screenBrightness)
    {
        LOGD("setBrightness()");
        JNIEnvGetter jeg(activity);
        const auto env=jeg.env();
    
        const jclass NativeActivity = env->FindClass("android/app/NativeActivity");
        const jclass Window = env->FindClass("android/view/Window");
    
        const jmethodID getWindow = env->GetMethodID(NativeActivity, "getWindow",
                                     "()Landroid/view/Window;");
        const jmethodID getAttributes = env->GetMethodID(Window, "getAttributes",
                                         "()Landroid/view/WindowManager$LayoutParams;");
        const jmethodID setAttributes = env->GetMethodID(Window, "setAttributes",
                                         "(Landroid/view/WindowManager$LayoutParams;)V");
    
        const jobject window = env->CallObjectMethod(activity->clazz, getWindow);
        const jobject attrs = env->CallObjectMethod(window, getAttributes);
        const jclass LayoutParams = env->GetObjectClass(attrs);
    
        const jfieldID screenBrightnessID = env->GetFieldID(LayoutParams, "screenBrightness", "F");
        env->SetFloatField(attrs, screenBrightnessID, screenBrightness);
        env->CallVoidMethod(window, setAttributes, attrs);
        if(env->ExceptionCheck())
        {
            LOGD("Exception detected");
            env->ExceptionDescribe();
            env->ExceptionClear();
        }
        else
        {
            static int count=0;
            LOGD("Brightness set successfully %d times", ++count);
        }
    
        env->DeleteLocalRef(attrs);
        env->DeleteLocalRef(window);
    }
    
    int setBrightnessPipe[2];
    void requestSetBrightness(const float brightness)
    {
        write(setBrightnessPipe[1], &brightness, sizeof brightness);
    }
    
    int setBrightnessCallback(const int fd, const int events, void*const data)
    {
        float brightness;
        // FIXME: not ideally robust check
        if(read(fd, &brightness, sizeof brightness)!=sizeof brightness)
            return 1;
        const auto activity=static_cast<ANativeActivity*>(data);
        setBrightness(activity, brightness);
        return 1; // continue listening for events
    }
    
    // a funny way to use static initialization to execute something in main thread
    const auto mainThreadLooper=ALooper_forThread();
    
    void android_main(struct android_app* state)
    {
        ALooper_acquire(mainThreadLooper);
        pipe(setBrightnessPipe);
        ALooper_addFd(mainThreadLooper, setBrightnessPipe[0], 0, ALOOPER_EVENT_INPUT,
                      setBrightnessCallback, state->activity);
    
        for(float x=0;;x+=0.001)
        {
            int events;
            struct android_poll_source* source;
            while (ALooper_pollAll(0, nullptr, &events, (void**)&source) >= 0)
            {
                if (source)
                    source->process(state, source);
    
                if (state->destroyRequested != 0)
                    return;
            }
    
            requestSetBrightness((1+std::cos(x))/2);
        }
    }