Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android: Show keyboard with WindowInsetsController. #3787

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

xorgy
Copy link
Contributor

@xorgy xorgy commented Jul 10, 2024

  • Tested on all platforms changed
  • Added an entry to the changelog module if knowledge of this change could be valuable to users
  • Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior

Should resolve #1823 but it's not real IME.

@xorgy xorgy marked this pull request as ready for review July 10, 2024 20:12
@xorgy xorgy requested a review from MarijnS95 as a code owner July 10, 2024 20:12
@xorgy
Copy link
Contributor Author

xorgy commented Jul 10, 2024

I'm a little unsatisfied that this is activated with set_ime_allowed, but doesn't actually cause any Ime:: to happen... but AFAIK that is the behavior on Wayland when you have a simple soft keyboard as well.

Copy link
Member

@kchibisov kchibisov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it just shown or some input works?

src/changelog/unreleased.md Outdated Show resolved Hide resolved
src/platform_impl/android/mod.rs Outdated Show resolved Hide resolved
@kchibisov
Copy link
Member

I'm a little unsatisfied that this is activated with set_ime_allowed, but doesn't actually cause any Ime events to happen... but AFAIK that is the behavior on Wayland when you have a simple soft keyboard as well.

Well, enabling IME doesn't mean that you'll have IME input, because you still enable it for us layout, because you don't actually know whether it'll happen or not. It's more of a hint that you're doing text input. Disabling it on the other side is used when you want just hotkeys to work, like in games, but you still toggle it back when you type in game chat or something, so it seems logical, I'd say, maybe just name is not ideal.

@xorgy
Copy link
Contributor Author

xorgy commented Jul 11, 2024

Is it just shown or some input works?

It produces key events, and you can type with it (including with some alternate layouts). It only works for keyboards that produce these and are not IME-only (e.g. US, US Dvorak, MX-es Gboard, etc.).

Here's what it looks like in a Xilem/Masonry demo app:

screen-20240711-092914.2.mp4

@xorgy
Copy link
Contributor Author

xorgy commented Jul 11, 2024

@mwcampbell is going to get an accesskit_winit branch ready for winit master (with the user events change) and I'll bring the branch up to date with that and test it again (with Masonry/Xilem). I'll have your comments addressed by then. :+ )

@xorgy xorgy requested a review from kchibisov July 11, 2024 13:57
@xorgy xorgy changed the title [Android] Show keyboard with WindowInsetsController. Android: Show keyboard with WindowInsetsController. Jul 11, 2024
@xorgy xorgy force-pushed the basic-keyboard-show-hide branch 2 times, most recently from 01cf7c5 to 593e41a Compare July 11, 2024 14:18
@kchibisov
Copy link
Member

branch ready for winit master

I'll backport this to 0.30.x branch, because it's an addition and doesn't break anything, so if you can test with only 0.30.x in your setup it's fine, though you can test with glutin's android example and see the typing in the console or something or whether it pops when calling.

src/platform_impl/android/mod.rs Outdated Show resolved Hide resolved
@xorgy xorgy requested a review from kchibisov July 11, 2024 19:53
@podusowski
Copy link
Contributor

podusowski commented Jul 15, 2024

Does hiding the keyboard work as expected for you? I have ported it to 0.29 to test it with my winit/egui stack and it crashes with:

07-14 14:27:00.856 15745 15802 E widnet : winit::platform_impl::platform: Showing or hiding the soft keyboard failed: JavaException
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] JNI DETECTED ERROR IN APPLICATION: JNI GetObjectClass called with pending exception android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.ViewRootImpl.checkThread() (ViewRootImpl.java:9728)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.ViewRootImpl.requestLayout() (ViewRootImpl.java:1883)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.ViewRootImpl.notifyInsetsChanged() (ViewRootImpl.java:1864)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.ViewRootInsetsControllerHost.notifyInsetsChanged() (ViewRootInsetsControllerHost.java:55)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.InsetsController.notifyVisibilityChanged() (InsetsController.java:1307)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.InsetsSourceConsumer.setRequestedVisible(boolean) (InsetsSourceConsumer.java:387)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.InsetsSourceConsumer.hide() (InsetsSourceConsumer.java:229)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.ImeInsetsSourceConsumer.hide() (ImeInsetsSourceConsumer.java:68)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.ImeInsetsSourceConsumer.hide(boolean, int) (ImeInsetsSourceConsumer.java:74)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.InsetsController.hideDirectly(int, boolean, int, boolean) (InsetsController.java:1426)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.InsetsController.controlAnimationUnchecked(int, android.os.CancellationSignal, android.view.WindowInsetsAnimationControlListener, android.graphics.Rect, boolean, long, android.view.animation.Interpolator, int, int, boolean) (InsetsController.java:1092)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.InsetsController.applyAnimation(int, boolean, boolean, boolean) (InsetsController.java:1409)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.InsetsController.applyAnimation(int, boolean, boolean) (InsetsController.java:1390)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.InsetsController.hide(int, boolean) (InsetsController.java:958)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] at void android.view.InsetsController.hide(int) (InsetsController.java:933)
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591]
07-14 14:27:00.867 15745 15802 F al.widnet.debu: java_vm_ext.cc:591] in call to GetObjectClass

Also, typing in egui does not work, because egui expects KeyEvent::text to be filled, which is not in the android impl. But I guess it's a different story.

Anyway, great work on pushing the keyboard input forward.

@xorgy
Copy link
Contributor Author

xorgy commented Jul 15, 2024

@podusowski seems threading related, by the message you copied here.
Could you say what version of Android are you running on; and also could you share the application or a skeleton of it? (so that I can take a look)

Cheers

@xorgy
Copy link
Contributor Author

xorgy commented Jul 15, 2024

As for the text field being filled, Masonry handles Key::Character which is why it's working for us. Maybe it makes sense to do some mapping (at least copy the contents of Key::Character when it is there).

@kchibisov
Copy link
Member

Probably can just feel the text with the logical_key? Since generally the input is done via text and not logical_key, because you have compose and such on desktop platforms which is routed via the text field.

@kchibisov
Copy link
Member

seems threading related, by the message you copied here

Do you by any chance somehow create the EventLoop of the main thread? Like inside the std::thread or something like that? It looks like Android backend doesn't have a check to ensure the thread it's running on, so if you manually speculate with threads it won't notice that.

@MarijnS95 do you happen to know whether android is required to use the main thread? And in general, I think we should have a way to ensure that it's running on the main thread. We can just use the same linux code with gettid == getpid I guess?

@podusowski
Copy link
Contributor

podusowski commented Jul 15, 2024

Do you by any chance somehow create the EventLoop of the main thread? Like inside the std::thread or something like that?

@kchibisov No, I don't think so. It's a pretty typical eframe/egui app. I'll chop out some demo once I find some spare time, because currently it's on a private repo.

Probably can just feel the text with the logical_key?

Yeah, it works nicely.

Could you say what version of Android are you running on

@xorgy It's 12. Running on Mi Note 10 Lite.

@lucasmerlin
Copy link
Contributor

Great work! Does this work with NativeActivity and GameActivity?

@lucasmerlin lucasmerlin mentioned this pull request Jul 15, 2024
7 tasks
@xorgy
Copy link
Contributor Author

xorgy commented Jul 15, 2024

Great work! Does this work with NativeActivity and GameActivity?

I've tested it with NativeActivity, but since it goes directly to the Window it should work with any activity.

@podusowski
Copy link
Contributor

I've created a demo based on winit 0.29 and egui, showing a crash here:

https://github.com/podusowski/winit/tree/pr3787-with-egui-crash

it's in demo_android. To run it, call

make -C demo_android run-on-device

I haven't got time to dig into it so no idea from me yet why this is happening.

@lucasmerlin It's GameActivity, so I guess it's a second yes. :)

@xorgy
Copy link
Contributor Author

xorgy commented Jul 17, 2024

@kchibisov

... do you happen to know whether android is required to use the main thread? And in general, I think we should have a way to ensure that it's running on the main thread. We can just use the same linux code with gettid == getpid I guess?

You figure the same check should be added? Just panic when you start the event loop off the main thread? I think this is still an issue if set_ime_allowed is called off the main thread.

@kchibisov
Copy link
Member

You figure the same check should be added? Just panic when you start the event loop off the main thread? I think this is still an issue if set_ime_allowed is called off the main thread.

I'm not familiar with how android does things, like it probably won't hurt, but if it always spawns some thread for you which is not the same as pid it could break a lot of things, so I generally won't try to change anything, until I know that it's safe.

@kchibisov
Copy link
Member

@podusowski just in case, you don't have anything duplicated from Android side of things, like jni, etc?

I can't really help with android stuff myself, just checking common mistakes.

@xorgy
Copy link
Contributor Author

xorgy commented Jul 17, 2024

I'm not familiar with how android does things, like it probably won't hurt, but if it always spawns some thread for you which is not the same as pid it could break a lot of things, so I generally won't try to change anything, until I know that it's safe.

Yeah the thing I wonder is if the thread that the Activity is spawned on is tid == pid to begin with; otherwise we'd have to capture the thread ID (where the rust android_main is loaded from the solib and executed) at event loop start.

@podusowski
Copy link
Contributor

I have checked the process/thread calls the function and they are the same in both:

07-20 21:55:50.830 19863 19945 I widnet  : winit::platform_impl::platform: show_hide_keyboard(true) pid: ThreadId(2) tid: 19863 -> Ok(())
07-20 21:55:52.048 19863 19945 I widnet  : winit::platform_impl::platform: show_hide_keyboard(false) pid: ThreadId(2) tid: 19863 -> Err(JavaException)

No idea how it relates to "thread that created a view hierarchy", maybe the whole thing runs on a different, at least in GameActivity.

Anyway, I did a test it with runOnUiThread and I do not see an exception anymore:

    public void showKeyboard() {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Window window = getWindow();
                WindowInsetsController wic = window.getInsetsController();
                wic.show(WindowInsets.Type.ime());
            }
        });
    }

I don't even know how to start moving this to jni though.

@xorgy
Copy link
Contributor Author

xorgy commented Jul 20, 2024

@podusowski

I don't even know how to start moving this to jni though.

I think it would involve injecting Dalvik bytecode, or need to be built into the Activity, though there may be another way.

It is fascinating that showing it succeeds on your end, but hiding it fails. I'll have a think on how something similar to what you're suggesting could be done with plain JNI.

@kchibisov
Copy link
Member

I wonder how C++ stuff solves it, like in QT, since they likely need to workaround it and have a way to run things on the UI thread. I'm not really surprised though that something like that was needed, since macOS/ios/windows do require the exact same thing...

@MarijnS95
Copy link
Member

@MarijnS95 do you happen to know whether android is required to use the main thread? And in general, I think we should have a way to ensure that it's running on the main thread. We can just use the same linux code with gettid == getpid I guess?

For UI stuff it's been a long time ago, but it seems runOnUiThread() was already proposed which exists "exactly" for this purpose?

The thread is spawned off the "main thread for that Activity" by android-activity, because we "have to" return quickly from that onCreate callback. In contrast, inside the users' Rust code, the Looper (i.e. your winit loop) is ran.

At some point I've been wondering if it makes more sense to expose these lower level details to apps and winit specifically, instead of hiding this thread creation inside android-activity. That probably goes paired with untangling the current onCreate() != main() mess.

@podusowski
Copy link
Contributor

@MarijnS95 You mean like exposing looper attached to the main thread (assuming I understand this stuff correctly) or something in AndroidApp or move the creation of this thread outside of android-activity completely?

@podusowski
Copy link
Contributor

I've made some experiments and came up with something like this:

In android-activity/game-activity-csrc/game-activity/native_app_glue/android_native_app_glue.c, just before the app thread is created, I saved a pointer to the looper attached to the main thread:

android_app->main_thread_looper = ALooper_forThread();

Then, I used this looper to run a Rust callback on the main thread like this:

fn show_hide_keyboard_fallible(app: AndroidApp, show: bool) -> Result<(), jni::errors::Error> {
    let looper = unsafe { ForeignLooper::from_ptr(app.main_thread_looper()) };

    let (mut tx, mut rx) = pipe_channel::channel::<String>();
    looper.add_fd_with_callback(
        unsafe { BorrowedFd::borrow_raw(rx.as_raw_fd()) },
        ndk::looper::FdEvent::INPUT,
        |fd, _| {
            let s = rx.recv().unwrap();

            log::info!(
                "got keyboard show/hide event in pid: {:?} tid: {}: {s}",
                std::thread::current().id(),
                std::process::id()
            );

            // After Android R, it is no longer possible to show the soft keyboard
            // with `showSoftInput` alone.
            // Here we use `WindowInsetsController`, which is the other way.
            let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr() as _).unwrap() };
            let activity = unsafe { JObject::from_raw(app.activity_as_ptr() as _) };
            let mut env = vm.attach_current_thread().unwrap();
            let window = env
                .call_method(&activity, "getWindow", "()Landroid/view/Window;", &[])
                .unwrap()
                .l()
                .unwrap();
            let wic = env
                .call_method(
                    window,
                    "getInsetsController",
                    "()Landroid/view/WindowInsetsController;",
                    &[],
                )
                .unwrap()
                .l()
                .unwrap();
            let window_insets_types = env.find_class("android/view/WindowInsets$Type").unwrap();
            let ime_type = env
                .call_static_method(&window_insets_types, "ime", "()I", &[])
                .unwrap()
                .i()
                .unwrap();
            env.call_method(&wic, &s, "(I)V", &[ime_type.into()])
                .unwrap()
                .v();

            false
        },
    );

    std::thread::sleep(Duration::from_secs(1));

    log::info!(
        "sending keyboard show/hide event from pid: {:?} tid: {}",
        std::thread::current().id(),
        std::process::id()
    );

    if show {
        tx.send("show".to_string());
    } else {
        tx.send("hide".to_string());
    }

    std::thread::sleep(Duration::from_secs(5));

    Ok(())
}

Now, obviously this has these unwraps and sleeps (not to drop everything before the callback runs) which must go away, and I had to patch android_native_app_glue.c, but at least I can confirm that it works.

@Zoxc
Copy link

Zoxc commented Aug 8, 2024

Why does this not use android-activity's methods to show/hide the keyboard? It will then start to deliver text events, even if it's from the rather limited GameTextInput API.

@xorgy
Copy link
Contributor Author

xorgy commented Aug 13, 2024

@Zoxc

Why does this not use android-activity's methods to show/hide the keyboard? It will then start to deliver text events, even if it's from the rather limited GameTextInput API.

On NativeActivity at least, it's a no-op because of a platform change in Android 11. That's why #2993 petered out: it wasn't able to open the keyboard.

If there's a way to make that clearer than I make it in the comment then I'd love to hear it. 😅

https://github.com/rust-windowing/winit/pull/3787/files#diff-9169a22d6397a250be741006cd857b8a575f804c74cc07e3a4fb8f3341606d9bR758-R760

@Zoxc
Copy link

Zoxc commented Aug 13, 2024

Still that seems like something that should be fixed with android-activitys NativeActivity handling?

@xorgy
Copy link
Contributor Author

xorgy commented Aug 14, 2024

Still that seems like something that should be fixed with android-activitys NativeActivity handling?

Should be fixed with [...] as in, this should be in android-activity? or should be fixed with [...] as in it's already fixed there somehow? Because we were using that function before and it doesn't work; but it doesn't work because Android changed and broke NativeActivity.

I'm glad to do the equivalent in android-activity (and that lines up with my plans, ultimately) but that was not the signal I got before I opened this PR is all.

@podusowski
Copy link
Contributor

Still that seems like something that should be fixed with android-activitys NativeActivity handling?

I think either way, some changes are needed in android-activity crate.

In:

#3787 (comment)

show_hide_keyboard_fallible is garbage, but there seem to be no other way of doing something on the main thread than attaching to its looper. At least this is my understanding, based on a nearly non-existing experience in writing stuff for Android.

@xorgy
Copy link
Contributor Author

xorgy commented Aug 14, 2024

show_hide_keyboard_fallible is garbage, but there seem to be no other way of doing something on the main thread than attaching to its looper. At least this is my understanding, based on a nearly non-existing experience in writing stuff for Android.

Well, that's true in some sense, but it's also not that hard to initiate the call from the main thread. The way we do this in Masonry is to have things like this activated by a queue that is processed in the main thread.

I am working on a new prebuilt activity (kinda like the proposed RustActivity) that sets up IME and AccessKit, but is otherwise similar to NativeActivity, and that will be part of our long term solution.

@MarijnS95
Copy link
Member

MarijnS95 commented Aug 14, 2024

Should be fixed with [...] as in, this should be in android-activity

Yes. If there's any hack required to get this working that's specific to NativeActivity and/or doesn't translate to GameActivity, this should be abstracted away inside android-activity. Perhaps also to keep some JNI workarounds out of winit, at least for now.

That should also more cleanly allow you to attach a callback to the "main UI thread" Looper (which could use a pipe to handle a variety of events).


I am working on a new prebuilt activity (kinda like the proposed RustActivity) that sets up IME and AccessKit, but is otherwise similar to NativeActivity, and that will be part of our long term solution.

This is going to be contributed to android-activity and reuse existing primitives and abstractions right? And for embedding (perhaps prebuilt) Java .class/.jar files into existing build-tools like xbuild/cargo-apk via the proposed println!("xbuild:java-lib=path/to/My.class") in build.rs for those tools to merge into a single classes.dex via d8 (as proposed and intended with https://github.com/MarijnS95/android-support and other references to it)?

@xorgy
Copy link
Contributor Author

xorgy commented Aug 14, 2024

This is going to be contributed to android-activity and reuse existing primitives and abstractions right?

My intention is to contribute it to android-activity, and for it to reuse as much as possible there. The other thing is I want it to work before there's any interesting build infrastructure added, so while I'm excited for your work on that, I'm starting with the basics (as I described above).

@MarijnS95
Copy link
Member

The other thing is I want it to work before there's any interesting build infrastructure added, so while I'm excited for your work on that, I'm starting with the basics (as I described above).

Ah I missed that but am glad to hear it; I've got some ideas and things in-progress and I think we can just meet in the middle in the end 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

SoftInput (Software Keyboard) support
6 participants