2

I have an android app in which I've set up an OpenGL context on the Java side, and am sending drawing from the NDK/C++ side. This all seems to work well.

I want the C++ side to be able to pop up a dialog. I implemented a java MakeADialog function which is getting fired just fine from the C side via a env->CallVoidMethod(javaClass, javaMethod); My Java-side receiving function looks like this:

public static void MakeADialog() {  
     Log.w("title", "MakeADialog fired!");
}

This is in a separate class (not an Activity or Runnable). The above works fine and I can see my MakeADialg log message. However, when I try to create an actual dialog box, then I get lots of crashes. I can tell I'm not grocking what 'thread' I'm running on when I call from the C side into the Java side. It appears I'm getting into trouble when I try to create a new thread/dialog.

I've tried lots of the suggestions on here about creating a Runnable, Thread, etc - but they always seem to give me the dreaded 'Can't create handler inside thread that has not called Looper.prepare()' or that the parent is null for the view. Most of these methods revolve around storing the Activity and Context pointers as static and retrieving them via get functions when I'm in the MakeADialog callback.

AlertDialog alertDialog = new AlertDialog.Builder(MyApp.GetMyContext()).create(); where the GetMyContext() function simply returns the this pointer of the main activity creation thread that I stored during app startup.

Has anyone popped up a dialog launched from their NDK side or can point me to some relevant docs that will help me understand how to create a new dialog from an NDK callback?

Thanks in advance!

1 Answers1

1

Maybe we can use the example from gist to create a modal dialog. I suspected that You called it from a worker thread so "they always seem to give me the dreaded 'Can't create handler inside thread that has not called Looper.prepare()' or that the parent is null for the view." (see also Can't create handler inside thread that has not called Looper.prepare()).

The key code based on the official example Native Activity and the gist code:

  1. On the Java side,
package ss.fang.brickgo;

import android.app.AlertDialog;
import android.app.Dialog;
import android.app.NativeActivity;
import android.content.DialogInterface;
import android.content.pm.ApplicationInfo;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;

import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

public class GameActivity extends NativeActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    /**
     * This function will be called from C++ by name and signature (Ljava/lang/String;Z)I)
     *
     * @param message the message text to show
     * @param model   if true, it will block the current thread, otherwise, it acts like a modeless dialog.
     * @return return id of the button that was clicked for a model dialog, otherwise, 0.
     * @see #showAlertCallback
     * @see <a href="https://stackoverflow.com/questions/11730001/create-a-message-dialog-in-android-via-ndk-callback/60611870#60611870">
     * Create a message dialog in Android via NDK callback</a>
     * @see <a href="https://stackoverflow.com/questions/6120567/android-how-to-get-a-modal-dialog-or-similar-modal-behavior">
     * Android: How to get a modal dialog or similar modal behavior?</a>
     */
    public int showAlert(final String message, boolean model) {
        //https://stackoverflow.com/questions/11411022/how-to-check-if-current-thread-is-not-main-thread
        if (Looper.myLooper() == Looper.getMainLooper() && model) {
            // Current Thread is UI Thread. Looper.getMainLooper().isCurrentThread()
            //android.os.NetworkOnMainThreadException
            throw new RuntimeException("Can't create a model dialog inside Main thread");
        }
        ApplicationInfo applicationInfo = getApplicationInfo();
        final CharSequence appName = getPackageManager().getApplicationLabel(applicationInfo);
        // Use a semaphore to create a modal dialog. Also, it's holden by the dialog's listener.
        final Semaphore semaphore = model ? new Semaphore(0, true) : null;
        // The button that was clicked (ex. BUTTON_POSITIVE) or the position of the item clicked
        final AtomicInteger buttonId = new AtomicInteger();
        this.runOnUiThread(new Runnable() {
            public void run() {
                AlertDialog.Builder builder = new AlertDialog.Builder(GameActivity.this, AlertDialog.THEME_HOLO_DARK);
                builder.setTitle(appName);
                builder.setMessage(message);
                DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        buttonId.set(id);
                        if (null != semaphore)
                            semaphore.release();
                        else
                            showAlertCallback(id);
                        if (DialogInterface.BUTTON_POSITIVE == id) {
                            GameActivity.this.finish();
                        }
                    }
                };
                builder.setNegativeButton(android.R.string.cancel, listener);
                builder.setPositiveButton(android.R.string.ok, listener);
                builder.setCancelable(false);
                AlertDialog dialog = builder.create();
                dialog.show();
            }
        });
        if (null != semaphore)
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                Log.v("GameActivity", "ignored", e);
            }
        return buttonId.get();
    }

    /**
     * The callback for showAlert when it acts like a modeless dialog
     *
     * @param id the button that was clicked
     */
    public native void showAlertCallback(int id);

    /**
     * @see <a href="https://stackoverflow.com/questions/13822842/dialogfragment-with-clear-background-not-dimmed">
     * DialogFragment with clear background (not dimmed)</a>
     */
    protected void showDialog() {
        Dialog dialog = new Dialog(this);
        dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
        dialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        // layout to display
        dialog.setContentView(R.layout.dialog_layout);

        // set color transpartent
        dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

        dialog.show();
    }
}
  1. On the Native (C++11) side,
/** @param onClickListener MUST be NULL because the model dialog is not implemented. */
typedef void *(OnClickListener)(int id);

jint showAlert(struct android_app *state, const char *message, bool model = false);

/** Process the next input event. */
static int32_t engine_handle_input(struct android_app *app, AInputEvent *event) {
    auto *engine = (struct engine *) app->userData;
    auto type = AInputEvent_getType(event);
    if (AINPUT_EVENT_TYPE_MOTION == type) {
        engine->animating = 1;
        engine->state.x = AMotionEvent_getX(event, 0);
        engine->state.y = AMotionEvent_getY(event, 0);
        return 1;
    } else if (AINPUT_EVENT_TYPE_KEY == type) {
        auto action = AKeyEvent_getAction(event);
        if (AKEY_EVENT_ACTION_DOWN == action && AKEYCODE_BACK == AKeyEvent_getKeyCode(event)) {
            //https://stackoverflow.com/questions/15913080/crash-when-closing-soft-keyboard-while-using-native-activity
            // skip predispatch (all it does is send to the IME)
            //if (!AInputQueue_preDispatchEvent(app->inputQueue, event))
            //int32_t handled = 0;
            //if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event);
            //AInputQueue_finishEvent(app->inputQueue, event, handled);
            LOGI("Before showAlert in modal behavior, it blocks until the dialog dismisses.");
            showAlert(app, "Go Back?", true);
            LOGI("After showAlert in modal behavior, the dialog should have been dismissed.");
            return 1;
        }
    }
    return 0; //not handled
}

/** @return the id of the button clicked if model is true, or 0 */
jint showAlert(struct android_app *state, const char *message, bool model /* = false */) {
    JNIEnv *jni = NULL;
    state->activity->vm->AttachCurrentThread(&jni, NULL);

    jclass clazz = jni->GetObjectClass(state->activity->clazz);

    // Get the ID of the method we want to call
    // This must match the name and signature from the Java side Signature has to match java
    // implementation (second string hints a java string parameter)
    jmethodID methodID = jni->GetMethodID(clazz, "showAlert", "(Ljava/lang/String;Z)I");

    // Strings passed to the function need to be converted to a java string object
    jstring jmessage = jni->NewStringUTF(message);

    jint result = jni->CallIntMethod(state->activity->clazz, methodID, jmessage, model);

    // Remember to clean up passed values
    jni->DeleteLocalRef(jmessage);

    state->activity->vm->DetachCurrentThread();

    return result;
}

extern "C"
JNIEXPORT void JNICALL
Java_ss_fang_brickgo_GameActivity_showAlertCallback(JNIEnv *env, jobject thiz, jint id) {
    LOGI("showAlertCallback %d", id);
}
samm
  • 620
  • 10
  • 22