diff --git a/meson.build b/meson.build index 8a076775..e14bd805 100644 --- a/meson.build +++ b/meson.build @@ -45,6 +45,16 @@ portal_openuri = gnome.gdbus_codegen('portal-openuri', 'src/api-impl-jni/content/org.freedesktop.portal.OpenURI.xml', interface_prefix: 'org.freedesktop.portal') +extra_deps = [] +extra_jni_srcs = [] + +layer_shell_dep = dependency('gtk4-layer-shell-0', required : false) +if layer_shell_dep.found() + extra_deps = [ layer_shell_dep ] + extra_jni_srcs = [ 'src/api-impl-jni/app/android_app_ATLKeyboardDialog.c' ] + add_project_arguments('-DATL_HAS_OSK', language: 'c') +endif + # libandroid libandroid_so = shared_library('android', [ 'src/libandroid/asset_manager.c', @@ -70,6 +80,11 @@ libandroid_so = shared_library('android', [ '-Wl,-z,lazy', # outdated Nvidia driver version 340 lacks EGL symbols ]) +wayland_protos_dep = dependency('wayland-protocols', version: '>=1.12') + +# wayland protocols for osk +subdir('protocol') + libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/api-impl-jni/android_app_NativeActivity.c', 'src/api-impl-jni/android_content_res_AssetManager.c', @@ -85,6 +100,7 @@ libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/api-impl-jni/app/android_app_Dialog.c', 'src/api-impl-jni/app/android_app_NotificationManager.c', 'src/api-impl-jni/app/android_app_WallpaperManager.c', + 'src/api-impl-jni/android_inputmethodservice_InputMethodService.c', 'src/api-impl-jni/AssetInputStream.c', 'src/api-impl-jni/audio/android_media_AudioTrack.c', 'src/api-impl-jni/audio/android_media_SoundPool.c', @@ -138,15 +154,19 @@ libtranslationlayer_so = shared_library('translation_layer_main', [ 'src/api-impl-jni/widgets/android_widget_TextView.c', mpris, portal_openuri, + wl_proto_headers, + wl_proto_sources, + extra_jni_srcs, ] + marshal_files, install: true, install_dir : get_option('libdir') / 'java/dex/android_translation_layer/natives', install_rpath: '$ORIGIN/:' + get_option('prefix') / get_option('libdir') / 'art', dependencies: [ + extra_deps, dependency('gtk4', version: '>=4.14'), dependency('gl'), dependency('egl'), dependency('wayland-client'), dependency('jni'), dependency('libportal'), dependency('sqlite3'), dependency('libavcodec', version: '>=59'), dependency('libdrm'), dependency('gudev-1.0'), dependency('libswscale'), dependency('webkitgtk-6.0'), - libandroidfw_dep + libandroidfw_dep, wayland_protos_dep ], link_with: [ libandroid_so ], link_args: [ diff --git a/protocol/input-method-unstable-v2.xml b/protocol/input-method-unstable-v2.xml new file mode 100644 index 00000000..62be9d94 --- /dev/null +++ b/protocol/input-method-unstable-v2.xml @@ -0,0 +1,490 @@ + + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2011 Intel Corporation + Copyright © 2012-2013 Collabora, Ltd. + Copyright © 2012, 2013 Intel Corporation + Copyright © 2015, 2016 Jan Arne Petersen + Copyright © 2017, 2018 Red Hat, Inc. + Copyright © 2018 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows applications to act as input methods for compositors. + + An input method context is used to manage the state of the input method. + + Text strings are UTF-8 encoded, their indices and lengths are in bytes. + + This document adheres to the RFC 2119 when using words like "must", + "should", "may", etc. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + An input method object allows for clients to compose text. + + The objects connects the client to a text input in an application, and + lets the client to serve as an input method for a seat. + + The zwp_input_method_v2 object can occupy two distinct states: active and + inactive. In the active state, the object is associated to and + communicates with a text input. In the inactive state, there is no + associated text input, and the only communication is with the compositor. + Initially, the input method is in the inactive state. + + Requests issued in the inactive state must be accepted by the compositor. + Because of the serial mechanism, and the state reset on activate event, + they will not have any effect on the state of the next text input. + + There must be no more than one input method object per seat. + + + + + Notification that a text input focused on this seat requested the input + method to be activated. + + This event serves the purpose of providing the compositor with an + active input method. + + This event resets all state associated with previous enable, disable, + surrounding_text, text_change_cause, and content_type events, as well + as the state associated with set_preedit_string, commit_string, and + delete_surrounding_text requests. In addition, it marks the + zwp_input_method_v2 object as active, and makes any existing + zwp_input_popup_surface_v2 objects visible. + + The surrounding_text, and content_type events must follow before the + next done event if the text input supports the respective + functionality. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Notification that no focused text input currently needs an active + input method on this seat. + + This event marks the zwp_input_method_v2 object as inactive. The + compositor must make all existing zwp_input_popup_surface_v2 objects + invisible until the next activate event. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Updates the surrounding plain text around the cursor, excluding the + preedit text. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. + + The argument text is a buffer containing the preedit string, and must + include the cursor position, and the complete selection. It should + contain additional characters before and after these. There is a + maximum length of wayland messages, so text can not be longer than 4000 + bytes. + + cursor is the byte offset of the cursor within the text buffer. + + anchor is the byte offset of the selection anchor within the text + buffer. If there is no selected text, anchor must be the same as + cursor. + + If this event does not arrive before the first done event, the input + method may assume that the text input does not support this + functionality and ignore following surrounding_text events. + + Values set with this event are double-buffered. They will get applied + and set to initial values on the next zwp_input_method_v2.done + event. + + The initial state for affected fields is empty, meaning that the text + input does not support sending surrounding text. If the empty values + get applied, subsequent attempts to change them may have no effect. + + + + + + + + + Tells the input method why the text surrounding the cursor changed. + + Whenever the client detects an external change in text, cursor, or + anchor position, it must issue this request to the compositor. This + request is intended to give the input method a chance to update the + preedit text in an appropriate way, e.g. by removing it when the user + starts typing with a keyboard. + + cause describes the source of the change. + + The value set with this event is double-buffered. It will get applied + and set to its initial value on the next zwp_input_method_v2.done + event. + + The initial value of cause is input_method. + + + + + + + Indicates the content type and hint for the current + zwp_input_method_v2 instance. + + Values set with this event are double-buffered. They will get applied + on the next zwp_input_method_v2.done event. + + The initial value for hint is none, and the initial value for purpose + is normal. + + + + + + + + Atomically applies state changes recently sent to the client. + + The done event establishes and updates the state of the client, and + must be issued after any changes to apply them. + + Text input state (content purpose, content hint, surrounding text, and + change cause) is conceptually double-buffered within an input method + context. + + Events modify the pending state, as opposed to the current state in use + by the input method. A done event atomically applies all pending state, + replacing the current state. After done, the new pending state is as + documented for each related request. + + Events must be applied in the order of arrival. + + Neither current nor pending state are modified unless noted otherwise. + + + + + + Send the commit string text for insertion to the application. + + Inserts a string at current cursor position (see commit event + sequence). The string to commit could be either just a single character + after a key press or the result of some composing. + + The argument text is a buffer containing the string to insert. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.commit request. + + The initial value of text is an empty string. + + + + + + + Send the pre-edit string text to the application text input. + + Place a new composing text (pre-edit) at the current cursor position. + Any previously set composing text must be removed. Any previously + existing selected text must be removed. The cursor is moved to a new + position within the preedit string. + + The argument text is a buffer containing the preedit string. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + The arguments cursor_begin and cursor_end are counted in bytes relative + to the beginning of the submitted string buffer. Cursor should be + hidden by the text input when both are equal to -1. + + cursor_begin indicates the beginning of the cursor. cursor_end + indicates the end of the cursor. It may be equal or different than + cursor_begin. + + Values set with this event are double-buffered. They must be applied on + the next zwp_input_method_v2.commit event. + + The initial value of text is an empty string. The initial value of + cursor_begin, and cursor_end are both 0. + + + + + + + + + Remove the surrounding text. + + before_length and after_length are the number of bytes before and after + the current cursor index (excluding the preedit text) to delete. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. In effect before_length is counted from the + beginning of preedit text, and after_length from its end (see commit + event sequence). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_input_method_v2.commit request. + + The initial values of both before_length and after_length are 0. + + + + + + + + Apply state changes from commit_string, set_preedit_string and + delete_surrounding_text requests. + + The state relating to these events is double-buffered, and each one + modifies the pending state. This request replaces the current state + with the pending state. + + The connected text input is expected to proceed by evaluating the + changes in the following order: + + 1. Replace existing preedit string with the cursor. + 2. Delete requested surrounding text. + 3. Insert commit string with the cursor at its end. + 4. Calculate surrounding text to send. + 5. Insert new preedit text in cursor position. + 6. Place cursor inside preedit text. + + The serial number reflects the last state of the zwp_input_method_v2 + object known to the client. The value of the serial argument must be + equal to the number of done events already issued by that object. When + the compositor receives a commit request with a serial different than + the number of past done events, it must proceed as normal, except it + should not change the current state of the zwp_input_method_v2 object. + + + + + + + Creates a new zwp_input_popup_surface_v2 object wrapping a given + surface. + + The surface gets assigned the "input_popup" role. If the surface + already has an assigned role, the compositor must issue a protocol + error. + + + + + + + + Allow an input method to receive hardware keyboard input and process + key events to generate text events (with pre-edit) over the wire. This + allows input methods which compose multiple key events for inputting + text like it is done for CJK languages. + + The compositor should send all keyboard events on the seat to the grab + holder via the returned wl_keyboard object. Nevertheless, the + compositor may decide not to forward any particular event. The + compositor must not further process any event after it has been + forwarded to the grab holder. + + Releasing the resulting wl_keyboard object releases the grab. + + + + + + + The input method ceased to be available. + + The compositor must issue this event as the only event on the object if + there was another input_method object associated with the same seat at + the time of its creation. + + The compositor must issue this request when the object is no longer + useable, e.g. due to seat removal. + + The input method context becomes inert and should be destroyed after + deactivation is handled. Any further requests and events except for the + destroy request must be ignored. + + + + + + Destroys the zwp_text_input_v2 object and any associated child + objects, i.e. zwp_input_popup_surface_v2 and + zwp_input_method_keyboard_grab_v2. + + + + + + + This interface marks a surface as a popup for interacting with an input + method. + + The compositor should place it near the active text input area. It must + be visible if and only if the input method is in the active state. + + The client must not destroy the underlying wl_surface while the + zwp_input_popup_surface_v2 object exists. + + + + + Notify about the position of the area of the text input expressed as a + rectangle in surface local coordinates. + + This is a hint to the input method telling it the relative position of + the text being entered. + + + + + + + + + + + + + + The zwp_input_method_keyboard_grab_v2 interface represents an exclusive + grab of the wl_keyboard interface associated with the seat. + + + + + This event provides a file descriptor to the client which can be + memory-mapped to provide a keyboard mapping description. + + + + + + + + + A key was pressed or released. + The time argument is a timestamp with millisecond granularity, with an + undefined base. + + + + + + + + + + Notifies clients that the modifier and/or group state has changed, and + it should update its local state. + + + + + + + + + + + + + + + Informs the client about the keyboard's repeat rate and delay. + + This event is sent as soon as the zwp_input_method_keyboard_grab_v2 + object has been created, and is guaranteed to be received by the + client before any key press event. + + Negative values for either rate or delay are illegal. A rate of zero + will disable any repeating (regardless of the value of delay). + + This event can be sent later on as well with a new value if necessary, + so clients should continue listening for the event past the creation + of zwp_input_method_keyboard_grab_v2. + + + + + + + + + The input method manager allows the client to become the input method on + a chosen seat. + + No more than one input method must be associated with any seat at any + given time. + + + + + Request a new input zwp_input_method_v2 object associated with a given + seat. + + + + + + + + Destroys the zwp_input_method_manager_v2 object. + + The zwp_input_method_v2 objects originating from it remain valid. + + + + diff --git a/protocol/meson.build b/protocol/meson.build new file mode 100644 index 00000000..089645b3 --- /dev/null +++ b/protocol/meson.build @@ -0,0 +1,39 @@ +fs = import('fs') + +proto_inc = include_directories('.') + +wl_protocol_dir = wayland_protos_dep.get_variable(pkgconfig: 'pkgdatadir') + +wayland_scanner = find_program('wayland-scanner') + +wl_protos = [ + ['input-method-unstable-v2.xml'], + ['virtual-keyboard-unstable-v1.xml'], +] + +wl_proto_sources = [] +wl_proto_headers = [] + +foreach p : wl_protos + xml = join_paths(p) + + base = fs.name(xml) + proto = fs.stem(base) + + wl_proto_headers += custom_target('@0@ client header'.format(proto), + input: xml, + output: '@0@-client-protocol.h'.format(proto), + command: [wayland_scanner, + 'client-header', + '@INPUT@', + '@OUTPUT@'] + ) + wl_proto_sources += custom_target('@0@ source'.format(proto), + input: xml, + output: '@0@-protocol.c'.format(proto), + command: [wayland_scanner, + 'private-code', + '@INPUT@', + '@OUTPUT@'] + ) +endforeach diff --git a/protocol/virtual-keyboard-unstable-v1.xml b/protocol/virtual-keyboard-unstable-v1.xml new file mode 100644 index 00000000..df4d01ce --- /dev/null +++ b/protocol/virtual-keyboard-unstable-v1.xml @@ -0,0 +1,113 @@ + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2013 Intel Corporation + Copyright © 2012-2013 Collabora, Ltd. + Copyright © 2018 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The virtual keyboard provides an application with requests which emulate + the behaviour of a physical keyboard. + + This interface can be used by clients on its own to provide raw input + events, or it can accompany the input method protocol. + + + + + Provide a file descriptor to the compositor which can be + memory-mapped to provide a keyboard mapping description. + + Format carries a value from the keymap_format enumeration. + + + + + + + + + + + + + A key was pressed or released. + The time argument is a timestamp with millisecond granularity, with an + undefined base. All requests regarding a single object must share the + same clock. + + Keymap must be set before issuing this request. + + State carries a value from the key_state enumeration. + + + + + + + + + Notifies the compositor that the modifier and/or group state has + changed, and it should update state. + + The client should use wl_keyboard.modifiers event to synchronize its + internal state with seat state. + + Keymap must be set before issuing this request. + + + + + + + + + + + + + + + A virtual keyboard manager allows an application to provide keyboard + input events as if they came from a physical keyboard. + + + + + + + + + Creates a new virtual keyboard associated to a seat. + + If the compositor enables a keyboard to perform arbitrary actions, it + should present an error when an untrusted client requests a new + keyboard. + + + + + + diff --git a/src/api-impl-jni/android_inputmethodservice_InputMethodService.c b/src/api-impl-jni/android_inputmethodservice_InputMethodService.c new file mode 100644 index 00000000..6365d3be --- /dev/null +++ b/src/api-impl-jni/android_inputmethodservice_InputMethodService.c @@ -0,0 +1,301 @@ +#include + +#include +#include +#include +#include + +#include +#include + +#include "input-method-unstable-v2-client-protocol.h" +#include "virtual-keyboard-unstable-v1-client-protocol.h" + +#include "defines.h" +#include "util.h" +#include "generated_headers/android_inputmethodservice_InputMethodService_ATLInputConnection.h" + +#define INFO(x...) android_log_printf(ANDROID_LOG_INFO, "ATLKeyboardIMS", x) +#define DEBUG(fmt, ...) android_log_printf(ANDROID_LOG_DEBUG, "ATLKeyboardIMS", "%s:%d: " fmt, __func__, __LINE__, ##__VA_ARGS__) + +struct { + struct wl_display *display; + struct wl_registry *registry; + struct wl_seat *seat; + struct zwp_input_method_manager_v2 *input_method_manager; + struct zwp_input_method_v2 *input_method; + struct zwp_virtual_keyboard_manager_v1 *virtual_keyboard_manager; + struct zwp_virtual_keyboard_v1 *virtual_keyboard; + + char compositing[4096]; + char surrounding[4096]; + guint text_len; + guint cursor; + guint serial; +} osk = {0}; + +/* android_app_ATLKeyboardDialog.c */ +#ifdef ATL_HAS_OSK +extern void atlosk_set_visible(gboolean new_visible); +#else +static void atlosk_set_visible(gboolean new_visible) +{ + DEBUG("=%d\n", new_visible); +} +#endif + +/* + * input-method-unstable-v2 + */ + +static void +handle_activate(void *data, + struct zwp_input_method_v2 *zwp_input_method_v2) +{ + atlosk_set_visible(TRUE); +} + +static void +handle_deactivate(void *data, + struct zwp_input_method_v2 *zwp_input_method_v2) +{ + atlosk_set_visible(FALSE); +} + +static void +handle_surrounding_text (void *data, + struct zwp_input_method_v2 *zwp_input_method_v2, + const char *text, + uint32_t cursor, + uint32_t anchor) +{ + DEBUG("(cursor=%d, '%s')\n", cursor, text); + osk.cursor = cursor; + osk.text_len = strnlen(text, sizeof(osk.surrounding)); + strncpy(osk.surrounding, text, sizeof(osk.surrounding)); +} + +static void +handle_text_change_cause (void *data, + struct zwp_input_method_v2 *zwp_input_method_v2, + uint32_t cause) +{ + //DEBUG("\n"); +} + +static void +handle_content_type (void *data, + struct zwp_input_method_v2 *zwp_input_method_v2, + uint32_t hint, + uint32_t purpose) +{ + //DEBUG("\n"); +} + +static void +handle_done (void *data, + struct zwp_input_method_v2 *zwp_input_method_v2) +{ + osk.serial++; +} + +static void +handle_unavailable (void *data, + struct zwp_input_method_v2 *zwp_input_method_v2) +{ + INFO("Input method unavailable"); +} + +static const struct zwp_input_method_v2_listener input_method_listener = { + .activate = handle_activate, + .deactivate = handle_deactivate, + .surrounding_text = handle_surrounding_text, + .text_change_cause = handle_text_change_cause, + .content_type = handle_content_type, + .done = handle_done, + .unavailable = handle_unavailable, +}; + +static void +registry_handle_global (void *data, + struct wl_registry *registry, + uint32_t name, + const char *interface, + uint32_t version) +{ + if (!strcmp (interface, zwp_input_method_manager_v2_interface.name)) { + osk.input_method_manager = wl_registry_bind(registry, name, &zwp_input_method_manager_v2_interface, 1); + osk.input_method = zwp_input_method_manager_v2_get_input_method(osk.input_method_manager, osk.seat); + zwp_input_method_v2_add_listener (osk.input_method, &input_method_listener, &osk); + } else if (!strcmp (interface, zwp_virtual_keyboard_manager_v1_interface.name)) { + osk.virtual_keyboard_manager = wl_registry_bind(registry, name, &zwp_virtual_keyboard_manager_v1_interface, 1); + osk.virtual_keyboard = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(osk.virtual_keyboard_manager, osk.seat); + } +} + +static void +registry_handle_global_remove (void *data, + struct wl_registry *registry, + uint32_t name) +{ + INFO("Global %d removed but not handled", name); +} + +static const struct wl_registry_listener registry_listener = { + registry_handle_global, + registry_handle_global_remove, +}; + +JNIEXPORT jlong JNICALL Java_android_inputmethodservice_InputMethodService_00024ATLInputConnection_nativeInit + (JNIEnv *env, jobject this) +{ + GdkDisplay *gdk_display; + GdkSeat *gdk_seat; + extern GtkWindow *window; /* Main activity window. */ + + INFO("Native init!"); + + gdk_display = gtk_root_get_display(GTK_ROOT(window)); + if (gdk_display == NULL) { + g_critical ("ATLKeyboardIMS: Failed to get display: %m\n"); + return 0; + } + + gdk_seat = gdk_display_get_default_seat(gdk_display); + if (gdk_seat == NULL) { + g_critical ("ATLKeyboardIMS: Failed to get seat: %m\n"); + return 0; + } + + osk.display = gdk_wayland_display_get_wl_display (gdk_display); + osk.seat = gdk_wayland_seat_get_wl_seat(gdk_seat); + osk.registry = wl_display_get_registry (osk.display); + wl_registry_add_listener (osk.registry, ®istry_listener, &osk); + + gtk_widget_set_visible(GTK_WIDGET(window), false); + + return 1; +} + +JNIEXPORT jboolean JNICALL Java_android_inputmethodservice_InputMethodService_00024ATLInputConnection_nativeSetCompositingText + (JNIEnv *env, jobject this, jlong ptr, jstring text, jint newCursorPosition) +{ + const char *data = (*env)->GetStringUTFChars(env, text, NULL); + + INFO("nativeSetCompositingText('%s', cur=%d)\n", data, newCursorPosition); + + if (osk.input_method) { + size_t text_len = strlen(data); + int cursor; + + if (newCursorPosition > 0) + cursor = text_len - newCursorPosition + 1; + else + cursor = -1 * newCursorPosition; + + zwp_input_method_v2_set_preedit_string (osk.input_method, data, cursor, cursor); + strncpy(osk.compositing, data, sizeof(osk.compositing)); + zwp_input_method_v2_commit (osk.input_method, osk.serial); + } + + return true; +} + +JNIEXPORT jboolean JNICALL Java_android_inputmethodservice_InputMethodService_00024ATLInputConnection_nativeSetCompositingRegion + (JNIEnv *env, jobject this, jlong ptr, jint start, jint end) +{ + INFO("nativeSetCompositingRegion(start=%d, end=%d)\n", start, end); + + if (osk.input_method) { + int beforeLength, afterLength, cursor = osk.cursor; + char tmp[4096] = {0}; + + if (start > end) { + int tmp = end; + end = start; + start = tmp; + } + + beforeLength = (end-start); + afterLength = 0; + + strncpy(tmp, &osk.surrounding[cursor - beforeLength], beforeLength); + cursor = strlen(tmp); + + zwp_input_method_v2_delete_surrounding_text (osk.input_method, beforeLength, afterLength); + zwp_input_method_v2_set_preedit_string (osk.input_method, tmp, cursor, cursor); + zwp_input_method_v2_commit (osk.input_method, osk.serial); + } + + return true; +} + +JNIEXPORT jboolean JNICALL Java_android_inputmethodservice_InputMethodService_00024ATLInputConnection_nativeFinishComposingText + (JNIEnv *env, jobject this, jlong ptr) +{ + INFO("nativeFinishCompositingText()\n"); + + if (osk.input_method) { + zwp_input_method_v2_commit_string (osk.input_method, osk.compositing); + zwp_input_method_v2_commit (osk.input_method, osk.serial); + osk.compositing[0] = '\0'; + } + + return true; +} + +JNIEXPORT jboolean JNICALL Java_android_inputmethodservice_InputMethodService_00024ATLInputConnection_nativeCommitText + (JNIEnv *env, jobject this, jlong ptr, jstring text, jint newCursorPosition) +{ + const char *data = (*env)->GetStringUTFChars(env, text, NULL); + + INFO("nativeCommitText('%s', cur=%d)\n", data, newCursorPosition); + + if (osk.input_method) { + zwp_input_method_v2_commit_string (osk.input_method, data); + zwp_input_method_v2_commit (osk.input_method, osk.serial); + osk.compositing[0] = '\0'; + } + + return true; +} + +JNIEXPORT jboolean JNICALL Java_android_inputmethodservice_InputMethodService_00024ATLInputConnection_nativeDeleteSurroundingText + (JNIEnv *env, jobject this, jlong ptr, jint beforeLength, jint afterLength) +{ + INFO("nativeDeleteSurroundingText(before=%d, after=%d)\n", beforeLength, afterLength); + + if (osk.input_method) { + zwp_input_method_v2_delete_surrounding_text (osk.input_method, beforeLength, afterLength); + osk.cursor -= beforeLength; + zwp_input_method_v2_commit (osk.input_method, osk.serial); + } + + return true; +} + +JNIEXPORT jboolean JNICALL Java_android_inputmethodservice_InputMethodService_00024ATLInputConnection_nativeSetSelection + (JNIEnv *env, jobject this, jlong ptr, jint start, jint end) +{ + INFO("nativeSetSelection(start=%d, end=%d)\n", start, end); + + if (osk.input_method) { + } + + return true; +} + +JNIEXPORT jboolean JNICALL Java_android_inputmethodservice_InputMethodService_00024ATLInputConnection_nativeSendKeyEvent + (JNIEnv *env, jobject this, jlong ptr, jlong time, jlong key, jlong state) +{ + INFO("nativeSendKeyEvent(time=%ld, key=%ld, state=%ld)\n", ptr, time, key, state); + + if (key == 67 /* KEYCODE_DEL */ && state == 1 /* ACTION_UP */) { + if (osk.input_method) { + zwp_input_method_v2_delete_surrounding_text (osk.input_method, 1, 0); + zwp_input_method_v2_commit (osk.input_method, osk.serial); + } + } + + return true; +} diff --git a/src/api-impl-jni/app/android_app_ATLKeyboardDialog.c b/src/api-impl-jni/app/android_app_ATLKeyboardDialog.c new file mode 100644 index 00000000..219cce1c --- /dev/null +++ b/src/api-impl-jni/app/android_app_ATLKeyboardDialog.c @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include + +#include + +#include "../defines.h" +#include "../util.h" +#include "../generated_headers/android_app_ATLKeyboardDialog.h" + +#define DEBUG(fmt, ...) android_log_printf(ANDROID_LOG_INFO, "ATLKeyboardDialog", "%s:%d: " fmt, __func__, __LINE__, ##__VA_ARGS__) + +static GDBusNodeInfo *introspection_data = NULL; +static GDBusConnection *dbus_connection = NULL; +static gboolean visible = TRUE; +static GtkWidget *osk_window = NULL; + +static void emit_property_changed(GDBusConnection *connection) +{ + GVariantBuilder builder; + + g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY); + g_variant_builder_add(&builder, "{sv}", "Visible", g_variant_new_boolean(visible)); + + g_dbus_connection_emit_signal(connection, + NULL, + "/sm/puri/OSK0", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new("(sa{sv}as)", "sm.puri.OSK0", &builder, NULL), NULL); + + g_variant_builder_clear(&builder); +} + +/* Used in IMS. */ +void atlosk_set_visible(gboolean new_visible) +{ + visible = new_visible; + + if (osk_window) { + gtk_widget_set_visible(osk_window, visible); + if (dbus_connection) + emit_property_changed(dbus_connection); + } +} + +static void handle_method_call(GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + if (g_strcmp0(method_name, "SetVisible") == 0) { + gboolean new_visible; + + g_variant_get(parameters, "(b)", &new_visible); + atlosk_set_visible(new_visible); + g_dbus_method_invocation_return_value(invocation, NULL); + } +} + +static GVariant *handle_get_property(GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) +{ + if (g_strcmp0(property_name, "Visible") == 0) { + return g_variant_new_boolean(visible); + } + + return NULL; +} + +static const GDBusInterfaceVTable interface_vtable = { + handle_method_call, + handle_get_property, + NULL, +}; + +static void on_name_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) +{ + DEBUG("Acquired D-Bus name: %s\n", name); + dbus_connection = connection; +} + +static int connect_osk_dbus_iface(GtkWidget *dialog) +{ + GDBusConnection *connection; + GError *error = NULL; + guint registration_id; + guint owner_id; + + owner_id = g_bus_own_name(G_BUS_TYPE_SESSION, + "sm.puri.OSK0", + G_BUS_NAME_OWNER_FLAGS_REPLACE, + NULL, + on_name_acquired, + NULL, NULL, NULL); + + if (owner_id == 0) { + g_printerr("OSK: Error: Could not acquire D-Bus name\n"); + return 1; + } + + /* https://world.pages.gitlab.gnome.org/Phosh/phosh/phosh-dbus-sm.puri.OSK0.html */ + const gchar introspection_xml[] = + "" + " " + " " + " " + " " + " " + " " + ""; + + introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, &error); + if (!introspection_data) { + g_printerr("OSK: Failed to parse introspection XML: %s\n", error->message); + g_error_free(error); + return 1; + } + + connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); + if (!connection) { + g_printerr("OSK: Failed to connect to D-Bus: %s\n", error->message); + g_error_free(error); + return 1; + } + + registration_id = g_dbus_connection_register_object(connection, + "/sm/puri/OSK0", + introspection_data->interfaces[0], + &interface_vtable, + NULL, NULL, &error); + if (!registration_id) { + g_printerr("OSK: Failed to register object: %s\n", error->message); + g_error_free(error); + return 1; + } + + osk_window = dialog; + + return 0; +} + +static gboolean on_close_request(GtkWidget *dialog, jobject jobj) +{ + JNIEnv *env = get_jni_env(); + jmethodID dismiss = _METHOD(_CLASS(jobj), "dismiss", "()V"); + (*env)->CallVoidMethod(env, jobj, dismiss); + return FALSE; +} + +JNIEXPORT jlong JNICALL Java_android_app_ATLKeyboardDialog_nativeInit(JNIEnv *env, jobject this) +{ + GtkWidget *dialog = gtk_window_new(); + GtkWindow *window = GTK_WINDOW(dialog); + + gtk_layer_init_for_window(window); + gtk_layer_auto_exclusive_zone_enable(window); + gtk_layer_set_namespace(window, "osk"); + gtk_layer_set_exclusive_zone(window, 200); + + static const gboolean anchors[] = {TRUE, TRUE, FALSE, TRUE}; + for (int i = 0; i < GTK_LAYER_SHELL_EDGE_ENTRY_NUMBER; i++) + gtk_layer_set_anchor(window, i, anchors[i]); + + connect_osk_dbus_iface(dialog); + + gtk_window_set_child(GTK_WINDOW(dialog), gtk_box_new(GTK_ORIENTATION_VERTICAL, 1)); + g_signal_connect_swapped(dialog, "response", G_CALLBACK(gtk_window_destroy), dialog); + g_signal_connect(GTK_WINDOW(dialog), "close-request", G_CALLBACK(on_close_request), _REF(this)); + + return _INTPTR(g_object_ref(dialog)); +} diff --git a/src/api-impl-jni/generated_headers/android_app_ATLKeyboardDialog.h b/src/api-impl-jni/generated_headers/android_app_ATLKeyboardDialog.h new file mode 100644 index 00000000..6021844d --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_app_ATLKeyboardDialog.h @@ -0,0 +1,21 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class android_app_ATLKeyboardDialog */ + +#ifndef _Included_android_app_ATLKeyboardDialog +#define _Included_android_app_ATLKeyboardDialog +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: android_app_ATLKeyboardDialog + * Method: nativeInit + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_android_app_ATLKeyboardDialog_nativeInit + (JNIEnv *, jobject); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/api-impl-jni/generated_headers/android_view_MotionEvent.h b/src/api-impl-jni/generated_headers/android_view_MotionEvent.h index 6229759b..1e0e0d5a 100644 --- a/src/api-impl-jni/generated_headers/android_view_MotionEvent.h +++ b/src/api-impl-jni/generated_headers/android_view_MotionEvent.h @@ -453,14 +453,6 @@ JNIEXPORT void JNICALL Java_android_view_MotionEvent_nativeGetPointerProperties JNIEXPORT void JNICALL Java_android_view_MotionEvent_nativeScale (JNIEnv *, jclass, jint, jfloat); -/* - * Class: android_view_MotionEvent - * Method: nativeTransform - * Signature: (ILandroid/graphics/Matrix;)V - */ -JNIEXPORT void JNICALL Java_android_view_MotionEvent_nativeTransform - (JNIEnv *, jclass, jint, jobject); - #ifdef __cplusplus } #endif diff --git a/src/api-impl/android/app/ATLKeyboardDialog.java b/src/api-impl/android/app/ATLKeyboardDialog.java new file mode 100644 index 00000000..7a7adfc5 --- /dev/null +++ b/src/api-impl/android/app/ATLKeyboardDialog.java @@ -0,0 +1,12 @@ +package android.app; + +import android.content.Context; + +public class ATLKeyboardDialog extends Dialog { + @Override + protected native long nativeInit(); + + public ATLKeyboardDialog(Context context) { + super(context); + } +} diff --git a/src/api-impl/android/app/Dialog.java b/src/api-impl/android/app/Dialog.java index c1ff8bfa..410d6269 100644 --- a/src/api-impl/android/app/Dialog.java +++ b/src/api-impl/android/app/Dialog.java @@ -15,7 +15,7 @@ import android.view.Window; public class Dialog implements Window.Callback, DialogInterface { protected long nativePtr; - private native long nativeInit(); + protected native long nativeInit(); private native void nativeSetTitle(long ptr, String title); private native void nativeSetContentView(long ptr, long widget); private native void nativeShow(long ptr); diff --git a/src/api-impl/android/inputmethodservice/ATLKeyboardViewer.java b/src/api-impl/android/inputmethodservice/ATLKeyboardViewer.java index 26e86ede..a6cc43f9 100644 --- a/src/api-impl/android/inputmethodservice/ATLKeyboardViewer.java +++ b/src/api-impl/android/inputmethodservice/ATLKeyboardViewer.java @@ -29,6 +29,11 @@ public class ATLKeyboardViewer extends Activity { System.exit(1); } - ims.launch_keyboard(); + boolean is_layershell = true; + + if (extras.containsKey("layershell") && extras.getString("layershell").equals("off")) + is_layershell = false; + + ims.launch_keyboard(is_layershell); } } diff --git a/src/api-impl/android/inputmethodservice/InputMethodService.java b/src/api-impl/android/inputmethodservice/InputMethodService.java index 30bd92df..57f92c3a 100644 --- a/src/api-impl/android/inputmethodservice/InputMethodService.java +++ b/src/api-impl/android/inputmethodservice/InputMethodService.java @@ -1,6 +1,8 @@ package android.inputmethodservice; import android.app.Dialog; +import android.app.Dialog; +import android.app.ATLKeyboardDialog; import android.app.Service; import android.content.Context; import android.content.Intent; @@ -30,38 +32,58 @@ public class InputMethodService extends AbstractInputMethodService { private Dialog kb_dialog; class ATLInputConnection extends BaseInputConnection { + protected long nativePtr; + + private native long nativeInit(); + private native boolean nativeSetCompositingText(long ptr, String text, int newCursorPosition); + private native boolean nativeSetCompositingRegion(long ptr, int start, int end); + private native boolean nativeFinishComposingText(long ptr); + private native boolean nativeCommitText(long ptr, String text, int newCursorPosition); + private native boolean nativeDeleteSurroundingText(long ptr, int beforeLength, int afterLength); + private native boolean nativeSetSelection(long ptr, int start, int end); + private native boolean nativeSendKeyEvent(long ptr, long time, long key, long state); + + ATLInputConnection() { super(null, false); + nativePtr = nativeInit(); } @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { - System.out.println("softkeyboard preview: setComposingText(\""+text+"\", "+newCursorPosition+")"); - return true; + return nativeSetCompositingText(nativePtr, text.toString(), newCursorPosition); + } + + @Override + public boolean setComposingRegion(int start, int end) { + return nativeSetCompositingRegion(nativePtr, start, end); } @Override public boolean finishComposingText() { - System.out.println("softkeyboard preview: finishComposingText()"); - return true; + return nativeFinishComposingText(nativePtr); } @Override public boolean commitText(CharSequence text, int newCursorPosition) { - System.out.println("softkeyboard preview: commitText(\""+text+"\", "+newCursorPosition+")"); - return true; + return nativeCommitText(nativePtr, text.toString(), newCursorPosition); } @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { - System.out.println("softkeyboard preview: deleteSurroundingText("+beforeLength+", "+afterLength+")"); - return true; + System.out.println("ATLKeyboardIMS: deleteSurroundingText("+ beforeLength + ", " + afterLength +")"); + return nativeDeleteSurroundingText(nativePtr, beforeLength, afterLength); + } + + @Override + public boolean setSelection(int start, int end) { + return nativeSetSelection(nativePtr, start, end); } @Override public boolean sendKeyEvent(KeyEvent event) { System.out.println("softkeyboard preview: sendKeyEvent("+event+")"); - return true; + return nativeSendKeyEvent(nativePtr, event.getEventTime(), event.getKeyCode(), event.getAction()); } /* these functions are noop on AOSP by default, so we just add a print for debugging purposes and still return false */ @@ -84,8 +106,11 @@ public class InputMethodService extends AbstractInputMethodService { super(new Context()); } - public void launch_keyboard() { - kb_dialog = new Dialog(this); + public void launch_keyboard(boolean is_layershell) { + if (is_layershell) + kb_dialog = new ATLKeyboardDialog(this); + else + kb_dialog = new Dialog(this); View decorview = kb_dialog.getWindow().getDecorView(); decorview.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); @@ -138,7 +163,7 @@ public class InputMethodService extends AbstractInputMethodService { } public InputBinding getCurrentInputBinding() { - return new InputBinding(new ATLInputConnection(), null, 0, 0); + return new InputBinding(input_connection, null, 0, 0); } public IBinder onBind(Intent intent) { diff --git a/src/api-impl/android/view/MotionEvent.java b/src/api-impl/android/view/MotionEvent.java index 640f360b..1de85ddd 100644 --- a/src/api-impl/android/view/MotionEvent.java +++ b/src/api-impl/android/view/MotionEvent.java @@ -1363,7 +1363,8 @@ public final class MotionEvent extends InputEvent { int pointerIndex, PointerProperties outPointerProperties); private static native void nativeScale(int nativePtr, float scale); - private static native void nativeTransform(int nativePtr, Matrix matrix); + private static /* native */ void nativeTransform(int nativePtr, Matrix matrix) { + } private static final int X_OFFSET = 0; private static final int Y_OFFSET = 1; diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index c9ff62d8..6c7054f2 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -37,6 +37,7 @@ srcs = [ 'android/app/ApplicationErrorReport.java', 'android/app/ApplicationExitInfo.java', 'android/app/Dialog.java', + 'android/app/ATLKeyboardDialog.java', 'android/app/DownloadManager.java', 'android/app/Fragment.java', 'android/app/FragmentManager.java',