From cfbc8a5dbcd362b69b37b4e6832ae4a31834364c Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 31 Dec 2022 18:04:18 +0800 Subject: Bring up the Android operating system and its window system * .dir-locals.el (c-mode): Add ANDROID_EXPORT noise macro. * .gitignore: Add new files to ignore. * Makefile.in: Adjust for Android. * admin/merge-gnulib: Add new warning. * configure.ac: Detect Android. Run cross-configuration for Android when appropriate. * etc/DEBUG: Document how to debug Emacs on Android. * java/AndroidManifest.xml: * java/Makefile.in: * java/README: * java/debug.sh: * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): * java/org/gnu/emacs/EmacsApplication.java (EmacsApplication): * java/org/gnu/emacs/EmacsCopyArea.java (EmacsCopyArea): * java/org/gnu/emacs/EmacsDrawLine.java (EmacsDrawLine): * java/org/gnu/emacs/EmacsDrawPoint.java (EmacsDrawPoint): * java/org/gnu/emacs/EmacsDrawRectangle.java (EmacsDrawRectangle): * java/org/gnu/emacs/EmacsDrawable.java (EmacsDrawable): * java/org/gnu/emacs/EmacsFillPolygon.java (EmacsFillPolygon): * java/org/gnu/emacs/EmacsFillRectangle.java (EmacsFillRectangle): * java/org/gnu/emacs/EmacsFontDriver.java (EmacsFontDriver): * java/org/gnu/emacs/EmacsGC.java (EmacsGC): * java/org/gnu/emacs/EmacsHandleObject.java (EmacsHandleObject): * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsPaintQueue.java (EmacsPaintQueue): * java/org/gnu/emacs/EmacsPaintReq.java (EmacsPaintReq): * java/org/gnu/emacs/EmacsPixmap.java (EmacsPixmap): * java/org/gnu/emacs/EmacsSdk7FontDriver.java (EmacsSdk7FontDriver): * java/org/gnu/emacs/EmacsService.java (class Holder) (EmacsService): * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): * java/org/gnu/emacs/EmacsThread.java (EmacsThread): * java/org/gnu/emacs/EmacsView.java (EmacsView): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow): New files and classes. * lib-src/Makefile.in (srcdir): * lib/Makefile.in (VPATH): (HAVE_NATIVE_COMP): (libgnu_a_SOURCES): (DEPFLAGS): Configure correctly for cross-compiling. * lib/faccessat.c: * lib/fpending.c (__fpending): * lib/open.c: * lib/unistd.c (_GL_UNISTD_INLINE): Temporary adjustments to gnulib. * lisp/frame.el (display-graphic-p): (display-screens): (display-pixel-height): (display-pixel-width): (display-mm-height): (display-mm-width): (display-backing-store): (display-save-under): (display-planes): (display-color-cells): (display-visual-class): Adjust for new window system `android'. * lisp/image/wallpaper.el (x-open-connection): Add declaration. * lisp/loadup.el (featurep): Load up files for Android. * lisp/net/eww.el (eww-form-submit, eww-form-file) (eww-form-checkbox, eww-form-select): Adjust faces for android. * lisp/term/android-win.el: New file. * src/Makefile.in: Add new targets emacs.so and android-emacs, then adjust for cross compilation. * src/alloc.c (cleanup_vector): Clean up Android font entities as well. (garbage_collect): Mark androidterm. * src/android-emacs.c (main): * src/android.c (ANDROID_THROW, enum android_fd_table_entry_flags) (struct android_emacs_service, struct android_emacs_pixmap) (struct android_graphics_point, struct android_event_container) (struct android_event_queue, android_run_select_thread) (android_handle_sigusr1, android_init_events, android_pending) (android_next_event, android_write_event, android_select) (android_run_debug_thread, android_user_full_name) (android_get_asset_name, android_fstat, android_fstatat) (android_file_access_p, android_hack_asset_fd, android_open) (android_close, JNICALL, android_init_emacs_service) (android_init_emacs_pixmap, android_init_graphics_point) (MAX_HANDLE, struct android_handle_entry, android_alloc_id) (android_destroy_handle, android_resolve_handle) (android_resolve_handle2, android_change_window_attributes) (android_create_window, android_set_window_background) (android_destroy_window, android_init_android_rect_class) (android_init_emacs_gc_class, android_create_gc, android_free_gc) (android_change_gc, android_set_clip_rectangles) (android_reparent_window, android_lookup_method) (android_clear_window, android_map_window, android_unmap_window) (android_resize_window, android_move_window, android_swap_buffers) (android_get_gc_values, android_set_foreground) (android_fill_rectangle, android_create_pixmap_from_bitmap_data) (android_set_clip_mask, android_set_fill_style, android_copy_area) (android_free_pixmap, android_set_background, android_fill_polygon) (android_draw_rectangle, android_draw_point, android_draw_line) (android_create_pixmap, android_set_ts_origin, android_clear_area): * src/android.h (ANDROID_EXPORT): * src/androidfns.c (android_display_info_for_name) (check_android_display_info, check_x_display_info, gamma_correct) (android_defined_color, android_decode_color) (android_implicitly_set_name, android_explicitly_set_name) (android_set_tool_bar_lines, android_change_tool_bar_height) (android_set_tab_bar_lines, android_change_tab_bar_height) (android_set_scroll_bar_default_height) (android_set_scroll_bar_default_width, android_icon_verify) (android_icon, android_make_gc, android_free_gcs) (unwind_create_frame, do_unwind_create_frame) (android_default_font_parameter, android_create_frame_window) (Fx_create_frame, Fxw_color_defined_p, Fxw_color_values) (Fxw_display_color_p, Fx_display_grayscale_p) (Fx_display_pixel_width, Fx_display_pixel_height) (Fx_display_planes, Fx_display_color_cells, Fx_display_screens) (Fx_display_mm_width, Fx_display_mm_height) (Fx_display_backing_store, Fx_display_visual_class) (Fx_display_monitor_attributes_list, Fx_frame_geometry) (Fx_frame_list_z_order, Fx_frame_restack) (Fx_mouse_absolute_pixel_position) (Fx_set_mouse_absolute_pixel_position, Fandroid_get_connection) (Fx_display_list, Fx_show_tip, Fx_hide_tip) (android_set_background_color, android_set_border_color) (android_set_cursor_color, android_set_cursor_type) (android_set_foreground_color) (android_set_child_frame_border_width) (android_set_internal_border_width, android_set_menu_bar_lines) (android_set_mouse_color, android_set_title, android_set_alpha) (android_frame_parm_handlers, syms_of_androidfns): * src/androidfont.c (struct android_emacs_font_driver) (struct android_emacs_font_spec, struct android_emacs_font_metrics) (struct android_emacs_font_object, struct android_integer) (struct androidfont_info, struct androidfont_entity) (android_init_font_driver, android_init_font_spec) (android_init_font_metrics, android_init_integer) (android_init_font_object, androidfont_get_cache) (androidfont_from_lisp, androidfont_from_java, androidfont_list) (androidfont_match, androidfont_draw, androidfont_open_font) (androidfont_close_font, androidfont_has_char) (androidfont_encode_char, androidfont_text_extents) (androidfont_list_family, androidfont_driver) (syms_of_androidfont_for_pdumper, syms_of_androidfont) (init_androidfont, android_finalize_font_entity): * src/androidgui.h (_ANDROID_GUI_H_, struct android_rectangle) (struct android_point, enum android_gc_function) (enum android_gc_value_mask, enum android_fill_style) (enum android_window_value_mask) (struct android_set_window_attributes, struct android_gc_values) (struct android_gc, enum android_swap_action, enum android_shape) (enum android_coord_mode, struct android_swap_info) (NativeRectangle, struct android_any_event) (struct android_key_event, struct android_configure_event) (union android_event): * src/androidterm.c (android_window_to_frame, android_clear_frame) (android_ring_bell, android_toggle_invisible_pointer) (android_update_begin, android_update_end, show_back_buffer) (android_flush_dirty_back_buffer_on, handle_one_android_event) (android_read_socket, android_frame_up_to_date) (android_buffer_flipping_unblocked_hook) (android_query_frame_background_color, android_parse_color) (android_alloc_nearest_color, android_query_colors) (android_mouse_position, android_get_focus_frame) (android_focus_frame, android_frame_rehighlight) (android_frame_raise_lower, android_make_frame_visible) (android_make_frame_invisible) (android_make_frame_visible_invisible, android_fullscreen_hook) (android_iconify_frame, android_set_window_size_1) (android_set_window_size, android_set_offset, android_set_alpha) (android_new_font, android_bitmap_icon, android_free_pixmap_hook) (android_free_frame_resources, android_delete_frame) (android_delete_terminal, android_scroll_run) (android_after_update_window_line, android_flip_and_flush) (android_clear_rectangle, android_reset_clip_rectangles) (android_clip_to_row, android_draw_fringe_bitmap) (android_set_cursor_gc, android_set_mouse_face_gc) (android_set_mode_line_face_gc, android_set_glyph_string_gc) (android_set_glyph_string_clipping) (android_set_glyph_string_clipping_exactly) (android_compute_glyph_string_overhangs) (android_clear_glyph_string_rect) (android_draw_glyph_string_background, android_fill_triangle) (android_make_point, android_inside_rect_p, android_clear_point) (android_draw_relief_rect, android_draw_box_rect) (HIGHLIGHT_COLOR_DARK_BOOST_LIMIT, android_setup_relief_color) (android_setup_relief_colors, android_draw_glyph_string_box) (android_draw_glyph_string_bg_rect, android_draw_image_relief) (android_draw_image_foreground, android_draw_image_foreground_1) (android_draw_image_glyph_string) (android_draw_stretch_glyph_string, android_draw_underwave) (android_draw_glyph_string_foreground) (android_draw_composite_glyph_string_foreground) (android_draw_glyphless_glyph_string_foreground) (android_draw_glyph_string, android_define_frame_cursor) (android_clear_frame_area, android_clear_under_internal_border) (android_draw_hollow_cursor, android_draw_bar_cursor) (android_draw_window_cursor, android_draw_vertical_window_border) (android_draw_window_divider, android_redisplay_interface) (frame_set_mouse_pixel_position, get_keysym_name) (android_create_terminal, android_term_init, syms_of_androidterm) (mark_androidterm): * src/androidterm.h (_ANDROID_TERM_H_, struct android_display_info) (struct android_output, FRAME_ANDROID_OUTPUT, XSCROLL_BAR): New files. * src/dired.c (file_attributes): Do not use openat on Android. * src/dispextern.h (No_Cursor): Define appropriately on Android. (struct glyph_string, struct face): Make gc field of type struct android_gc on Android. * src/dispnew.c (clear_current_matrices, clear_desired_matrices) (adjust_frame_glyphs_for_window_redisplay, free_glyphs) (update_frame, scrolling, char_ins_del_cost, update_frame_line) (init_display_interactive): Disable text terminal support completely on Android. Fix non-toolkit menus for non-X systems. * src/editfns.c (Fuser_full_name): Call android_user_full_name. * src/emacs.c (android_emacs_init): Make main this on Android. Prohibit argv sorting from exceeding end of argv. * src/epaths.in: Add path definitions for Android. * src/fileio.c (file_access_p): Call android_file_access_p. (file_name_directory): Avoid using openat on Android. (Fcopy_file): Adjust to call sys_fstat instead. (file_directory_p): (Finsert_file_contents): (write_region): Likewise. * src/filelock.c: * src/fns.c (Flocale_info): Pacify warning on Android. * src/font.c (font_make_entity_android): New function. * src/font.h: * src/frame.c (Fframep): (Fwindow_system): Handle new window system `android'. Update doc strings. (Fmake_terminal_frame): Disable on Android. (gui_display_get_resource): Disable get_string_resource_hook on Android. (syms_of_frame): New defsym `android'. * src/frame.h (GCALIGNED_STRUCT): Add new output data for Android. (ENUM_BF): Expand enumerator size. (FRAME_ANDROID_P, FRAME_WINDOW_P, MOUSE_HL_INFO): Add definitions for Android. * src/image.c (GET_PIXEL): (image_create_bitmap_from_file): (image_create_x_image_and_pixmap_1): (image_get_x_image): (slurp_file): (lookup_rgb_color): (image_to_emacs_colors): (image_from_emacs_colors): (image_pixmap_draw_cross): (image_disable_image): (MaskForeground): (gif_load): Add stubs for Android. * src/lisp.h: * src/lread.c (safe_to_load_version, maybe_swap_for_eln1, openp): * src/pdumper.c (pdumper_load): Call sys_fstat instead of fstat. * src/process.c (wait_reading_process_output): Use android_select instead of pselect. * src/scroll.c: Disable on Android. * src/sysdep.c (widen_foreground_group, reset_sys_modes) (init_signals, emacs_fstatat, sys_fstat): New function. (emacs_open, emacs_open_noquit, emacs_close): Implement differently on Android. (close_output_streams): Disable what is not required on Android. * src/term.c (OUTPUT1_IF, encode_terminal_code, string_cost) (string_cost_one_line, per_line_cost, calculate_costs) (struct fkey_table, tty_append_glyph, produce_glyphs) (tty_capable_p, Fsuspend_tty, Fresume_tty, device, init_tty) (maybe_fatal, syms_of_term): Disable text terminal support on Android. * src/termhooks.h (enum output_method): Add android output method. (GCALIGNED_STRUCT, TERMINAL_FONT_CACHE): Define for Android. * src/terminal.c (Fterminal_live_p): Implement for Android. * src/verbose.mk.in (AM_V_GLOBALS): Add JAVAC and DX. * src/xdisp.c (redisplay_internal): Disable text terminals on Android. (display_menu_bar): (display_tty_menu_item): (draw_row_with_mouse_face): (expose_frame): Make the non toolkit menu bar work on Android. * src/xfaces.c (GCGraphicsExposures): (x_create_gc): (x_free_gc): (Fx_load_color_file): Define for Android. * xcompile/Makefile.in (top_srcdir): (top_builddir): * xcompile/README: * xcompile/langinfo.h (nl_langinfo): New files. --- java/org/gnu/emacs/EmacsNative.java | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 java/org/gnu/emacs/EmacsNative.java (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java new file mode 100644 index 00000000000..6550e6fa2a1 --- /dev/null +++ b/java/org/gnu/emacs/EmacsNative.java @@ -0,0 +1,72 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import java.lang.System; + +import android.content.res.AssetManager; + +public class EmacsNative +{ + /* Set certain parameters before initializing Emacs. This proves + that libemacs.so is being loaded from Java code. + + assetManager must be the asset manager associated with the + context that is loading Emacs. It is saved and remains for the + remainder the lifetime of the Emacs process. + + filesDir must be the package's data storage location for the + current Android user. + + libDir must be the package's data storage location for native + libraries. It is used as PATH. + + emacsService must be the emacsService singleton. */ + public static native void setEmacsParams (AssetManager assetManager, + String filesDir, + String libDir, + EmacsService emacsService); + + /* Initialize Emacs with the argument array ARGV. Each argument + must contain a NULL terminated string, or else the behavior is + undefined. */ + public static native void initEmacs (String argv[]); + + /* Abort and generate a native core dump. */ + public static native void emacsAbort (); + + /* Send an ANDROID_CONFIGURE_NOTIFY event. */ + public static native void sendConfigureNotify (short window, long time, + int x, int y, int width, + int height); + + /* Send an ANDROID_KEY_PRESS event. */ + public static native void sendKeyPress (short window, long time, int state, + int keyCode); + + /* Send an ANDROID_KEY_RELEASE event. */ + public static native void sendKeyRelease (short window, long time, int state, + int keyRelease); + + static + { + System.loadLibrary ("emacs"); + }; +}; -- cgit v1.3 From a32963e11f9f8e5d22b0d754d34a867f3b178ed2 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 2 Jan 2023 21:38:19 +0800 Subject: Update Android port * Makefile.in (java): Depend on info. (MAKEFILE_NAME): (config.status): Remove unneeded changes. * configure.ac (BUILD_DETAILS, ANDROID_STUBIFY): Don't require a C++ compiler on Android. * java/AndroidManifest.xml: : Set launchMode appropriately. : New activity. * java/Makefile.in (CROSS_BINS): Add EmacsClient. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity) (onCreate): Use the window attachment manager. * java/org/gnu/emacs/EmacsCopyArea.java (EmacsCopyArea) (paintTo): Implement clip masks correctly. * java/org/gnu/emacs/EmacsDrawRectangle.java (getRect, paintTo): Fix damage tracking rectangles. * java/org/gnu/emacs/EmacsFontDriver.java (FontSpec, toString): New function. (FontMetrics, EmacsFontDriver): Fix signature of textExtents. * java/org/gnu/emacs/EmacsMultitaskActivity.java (EmacsMultitaskActivity): New file. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New functions sendFocusIn, sendFocusOut, sendWindowAction. * java/org/gnu/emacs/EmacsPaintQueue.java (run): Fix clipping handling. * java/org/gnu/emacs/EmacsPixmap.java (EmacsPixmap): Add constructor for mutable pixmaps. * java/org/gnu/emacs/EmacsSdk23FontDriver.java (EmacsSdk23FontDriver): New file. * java/org/gnu/emacs/EmacsSdk7FontDriver.java (EmacsSdk7FontDriver, Sdk7Typeface, Sdk7FontEntity, Sdk7FontObject) (checkMatch, hasChar, encodeChar): Implement text display and fix font metrics semantics. * java/org/gnu/emacs/EmacsService.java (EmacsService): Remove availableChildren. (getLibraryDirectory, onCreate): Pass pixel density to Emacs. (clearArea): Fix arguments. Switch to using the window attachment manager. * java/org/gnu/emacs/EmacsSurfaceView.java (surfaceChanged) (surfaceCreated): Flip buffers on surface attachment. * java/org/gnu/emacs/EmacsView.java (EmacsView, swapBuffers): New argument FORCE. Always swap if it is true. (onKeyMultiple, onFocusChanged): New functions. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, destroyHandle) (run): Switch to using the window attachment manager. * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager): New file. * lisp/cus-edit.el (custom-button, custom-button-mouse) (custom-button-pressed): * lisp/faces.el (tool-bar): Define faces correctly on Android. * src/android.c (struct android_emacs_pixmap): Add mutable constructor. (struct android_emacs_drawable): New structure. (android_write_event): Check if event queue hasn't yet been initialized. (android_select): Set errno to EINTR if pselect fails. (android_close): Remove unused debugging code. (android_get_home_directory): New function. (Java_org_gnu_emacs_EmacsNative_setEmacsParams): Set pixel density and compute game path. (android_init_emacs_drawable): New function. (Java_org_gnu_emacs_EmacsNative_sendKeyPress): New argument `unicode_char'. Pass it in events. (Java_org_gnu_emacs_EmacsNative_sendKeyRelease): Likewise. (Java_org_gnu_emacs_EmacsNative_sendFocusIn) (Java_org_gnu_emacs_EmacsNative_sendFocusOut) (Java_org_gnu_emacs_EmacsNative_sendWindowAction): New functions. (android_resolve_handle): Export function. (android_change_gc): Clear clip rects under the right circumstances. Set right clip mask field. (android_create_pixmap_from_bitmap_data): Use correct alpha channels. (android_create_pixmap): Create mutable pixmap and avoid redundant color array allocation. (android_create_bitmap_from_data, android_create_image) (android_destroy_image, android_put_pixel, android_get_pixel) (android_get_image, android_put_image, faccessat): New functions. * src/android.h: Update prototypes. * src/androidfns.c (android_default_font_parameter): Prefer monospace to Droid Sans Mono. * src/androidfont.c (struct android_emacs_font_driver): New method `draw'. (struct android_emacs_font_spec): New field `dpi'. (struct androidfont_info): Add font metrics cache. (android_init_font_driver, android_init_font_spec): Adjust accordingly. (androidfont_from_lisp, androidfont_from_java): Handle new fields. (androidfont_draw): Implement function. (androidfont_open_font): Set pixel size correctly. (androidfont_close_font): Free metrics cache. (androidfont_cache_text_extents) (androidfont_check_cached_extents): New functions. (androidfont_text_extents): Cache glyph metrics somewhere for future use. (androidfont_list_family): Implement function. * src/androidgui.h (enum android_event_type): New focus and window action events. (enum android_modifier_mask): New masks. (struct android_key_event): New field `unicode_char'. (ANDROID_IS_MODIFIER_KEY): Newmacro. (struct android_focus_event, struct android_window_action_event): New structs. (union android_event): Add new fields. (enum android_image_format, struct android_image): New enums and structs. * src/androidterm.c (android_android_to_emacs_modifiers) (android_emacs_to_android_modifiers, android_lower_frame) (android_raise_frame, android_new_focus_frame) (android_focus_changed, android_detect_focus_change): New functions. (handle_one_android_event): Implement focus and key event handling. (android_frame_rehighlight): New function. (android_frame_raise_lower): Implement accordingly. (android_make_frame_invisible): Clear highlight_frame if required. (android_free_frame_resources): Clear x_focus_event_frame if required. (android_draw_fringe_bitmap, android_draw_image_foreground) (android_draw_image_foreground_1) (android_draw_image_glyph_string): Remove unnecessary code. (android_create_terminal, android_term_init): Set the baud rate to something sensible. * src/androidterm.h (struct android_bitmap_record): Make structure the same as on X. (struct android_display_info): New focus tracking fields. (struct android_output): Likewise. * src/dispextern.h (struct image): Add ximg and mask_img on Android. * src/emacs.c (android_emacs_init): Fix argc sorting iteration. * src/fileio.c (user_homedir): (get_homedir): Implement correctly on Android. * src/font.h (PT_PER_INCH): Define correctly on Android. * src/fringe.c (X, swap_nibble, init_fringe_bitmap): Swap fringe bitmaps correctly on Android. * src/image.c (GET_PIXEL, image_create_bitmap_from_data) (image_create_bitmap_from_file, free_bitmap_record) (image_unget_x_image_or_dc, struct image_type) (prepare_image_for_display, image_clear_image_1) (image_size_in_bytes, x_check_image_size) (x_create_x_image_and_pixmap, x_destroy_x_image) (image_check_image_size, image_create_x_image_and_pixmap_1) (image_destroy_x_image, gui_put_x_image, image_put_x_image) (image_get_x_image, image_unget_x_image) (Create_Pixmap_From_Bitmap_Data, image_pixmap_draw_cross) (MaskForeground, image_types, syms_of_image): Implement all of the above on Android in terms of an API very similar to X. * src/keyboard.c (FUNCTION_KEY_OFFSET, lispy_function_keys): Define on Android to something sensible. * src/lread.c (build_load_history): Fix problem. --- Makefile.in | 17 +- configure.ac | 20 +- java/AndroidManifest.xml | 9 +- java/Makefile.in | 3 +- java/org/gnu/emacs/EmacsActivity.java | 153 +++-- java/org/gnu/emacs/EmacsCopyArea.java | 135 ++++- java/org/gnu/emacs/EmacsDrawRectangle.java | 8 +- java/org/gnu/emacs/EmacsFontDriver.java | 26 +- java/org/gnu/emacs/EmacsMultitaskActivity.java | 25 + java/org/gnu/emacs/EmacsNative.java | 18 +- java/org/gnu/emacs/EmacsPaintQueue.java | 24 +- java/org/gnu/emacs/EmacsPixmap.java | 29 + java/org/gnu/emacs/EmacsSdk23FontDriver.java | 43 ++ java/org/gnu/emacs/EmacsSdk7FontDriver.java | 188 +++++- java/org/gnu/emacs/EmacsService.java | 83 +-- java/org/gnu/emacs/EmacsSurfaceView.java | 37 +- java/org/gnu/emacs/EmacsView.java | 45 +- java/org/gnu/emacs/EmacsWindow.java | 143 +++-- .../gnu/emacs/EmacsWindowAttachmentManager.java | 166 ++++++ lisp/cus-edit.el | 6 +- lisp/faces.el | 2 +- src/android.c | 627 +++++++++++++++++++-- src/android.h | 15 + src/androidfns.c | 5 +- src/androidfont.c | 270 ++++++--- src/androidgui.h | 81 +++ src/androidterm.c | 544 ++++++++++-------- src/androidterm.h | 30 +- src/dispextern.h | 11 +- src/emacs.c | 5 +- src/fileio.c | 9 + src/font.h | 6 + src/fringe.c | 23 +- src/image.c | 249 +++++--- src/keyboard.c | 53 +- src/lread.c | 20 +- 36 files changed, 2435 insertions(+), 693 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsMultitaskActivity.java create mode 100644 java/org/gnu/emacs/EmacsSdk23FontDriver.java create mode 100644 java/org/gnu/emacs/EmacsWindowAttachmentManager.java (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/Makefile.in b/Makefile.in index d8c5ba555d3..6ca1b1dc382 100644 --- a/Makefile.in +++ b/Makefile.in @@ -536,7 +536,7 @@ lisp: src lib lib-src lisp nt: Makefile $(MAKE) -C $@ all -java: lisp +java: lisp info $(MAKE) -C $@ all xcompile: src @@ -571,20 +571,10 @@ blessmail: Makefile src # then attempts to build that file. This forces 'Makefile', 'lib/Makefile', # etc. to be built without running into similar recursion problems. MAKEFILE_NAME = Makefile -ifeq ($(ANDROID),) $(MAKEFILE_NAME): config.status $(srcdir)/configure \ $(srcdir)/lib/gnulib.mk.in \ $(srcdir)/Makefile.in $(SUBDIR_MAKEFILES_IN) $(CONFIG_STATUS_FILES_IN) MAKE='$(MAKE)' ./config.status -else -# Note that calling config.status is insufficient on Android due to -# the recursive calls to configure. - -$(MAKEFILE_NAME): $(srcdir)/configure \ - $(srcdir)/lib/gnulib.mk.in \ - $(srcdir)/Makefile.in $(SUBDIR_MAKEFILES_IN) $(CONFIG_STATUS_FILES_IN) - $(CFG) $(srcdir)/configure $(CONFIGURE_FLAGS); -endif # Don't erase these files if make is interrupted while refreshing them. .PRECIOUS: Makefile config.status @@ -592,17 +582,12 @@ endif # Note that calling config.status --recheck is insufficient on Android # due to the recursive calls to configure. -ifneq ($(ANDROID),) -config.status: ${srcdir}/configure - $(CFG) $(srcdir)/configure $(CONFIGURE_FLAGS) -else config.status: ${srcdir}/configure if [ -x ./config.status ]; then \ $(CFG) ./config.status --recheck; \ else \ $(CFG) $(srcdir)/configure $(CONFIGURE_FLAGS); \ fi -endif $(srcdir)/configure: $(srcdir)/configure.ac $(srcdir)/m4/*.m4 cd $(srcdir) && ./autogen.sh autoconf diff --git a/configure.ac b/configure.ac index da8d753afca..70dbfa10b35 100644 --- a/configure.ac +++ b/configure.ac @@ -31,9 +31,8 @@ if test "$XCONFIGURE" = "android"; then # Android! AC_MSG_NOTICE([called to recursively configure Emacs \ for Android.]) - # Set CC to ANDROID_CC, and CXX to ANDROID_CXX. + # Set CC to ANDROID_CC. CC=$ANDROID_CC - CXX=$ANDROID_CXX fi dnl Set emacs_config_options to the options of 'configure', quoted for the shell, @@ -147,7 +146,7 @@ if test "$XCONFIGURE" = "android"; then ;; *) AC_MSG_ERROR([The cross compiler does not compile for Android. Please verify that you specified the correct compiler in the ANDROID_CC -and ANDROID_CXX variables when you ran configure.]) +variable when you ran configure.]) ;; esac AC_MSG_RESULT([$host_alias]) @@ -759,8 +758,7 @@ tools such as aapt, dx, and aidl): The cross-compiler should then be specified: - ANDROID_CC=/path/to/armv7a-linux-androideabi19-clang - ANDROID_CXX=/path/to/armv7a-linux-androideabi19-clang++]) + ANDROID_CC=/path/to/armv7a-linux-androideabi19-clang]) elif test "$with_android" = "no" || test "$with_android" = ""; then ANDROID=no else @@ -833,12 +831,11 @@ Please verify that the path to the SDK build tools you specified is correct]); dnl Now configure Emacs to generate binaries for Android. After the dnl configuration completes, move the generated Makefiles. - if test "$ANDROID_CC" = "" || test "$ANDROID_CXX" = ""; then + if test "$ANDROID_CC" = ""; then AC_MSG_ERROR([Please specify the path to the Android cross-compiler for your machine. For example: ANDROID_CC=/path/to/armv7a-linux-androideabi19-clang \\ - ANDROID_CXX=/path/to/armv7a-linux-androideabi19-clang++ \\ ./configure --with-android]) fi @@ -863,7 +860,7 @@ for your machine. For example: *) AC_MSG_ERROR([configure could not determine the type of Android \ binary Emacs is being configured for. Please port this configure script \ to your Android system, or verify that you specified the correct compiler \ -in the ANDROID_CC and ANDROID_CXX variables when you ran configure.]) +in the ANDROID_CC variable when you ran configure.]) ;; esac AC_MSG_RESULT([$android_abi]) @@ -874,9 +871,8 @@ in the ANDROID_CC and ANDROID_CXX variables when you ran configure.]) mv -f confdefs.h _confdefs.h mv -f config.log _config.log - AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" \ - ANDROID_CXX="$ANDROID_CXX" $0], [], [AC_MSG_ERROR([Failed to cross-\ -configure Emacs for android.])]) + AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" $0], [], + [AC_MSG_ERROR([Failed to cross-configure Emacs for android.])]) # Now set ANDROID to yes. ANDROID=yes @@ -2254,7 +2250,7 @@ for Android, but all API calls need to be stubbed out]) ANDROID_CFLAGS="-fPIC -fvisibility=hidden" # Link with libraries required for Android support. - ANDROID_LIBS="-landroid -llog" + ANDROID_LIBS="-landroid -llog -ljnigraphics" fi fi diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml index 75aa5bdf409..06417017ae3 100644 --- a/java/AndroidManifest.xml +++ b/java/AndroidManifest.xml @@ -31,13 +31,15 @@ android:targetSdkVersion="28"/> - + @@ -45,6 +47,9 @@ + + availableActivities; - /* The currently attached EmacsWindow, or null if none. */ private EmacsWindow window; /* The frame layout associated with the activity. */ private FrameLayout layout; + /* List of activities with focus. */ + private static List focusedActivities; + + /* The currently focused window. */ + public static EmacsWindow focusedWindow; + static { - /* Set up the list of available activities. */ - availableActivities = new ArrayList (); + focusedActivities = new ArrayList (); }; + public static void + invalidateFocus1 (EmacsWindow window) + { + if (window.view.isFocused ()) + focusedWindow = window; + + for (EmacsWindow child : window.children) + invalidateFocus1 (window); + } + + public static void + invalidateFocus () + { + EmacsWindow oldFocus; + + /* Walk through each focused activity and assign the window focus + to the bottom-most focused window within. Record the old focus + as well. */ + oldFocus = focusedWindow; + focusedWindow = null; + + for (EmacsActivity activity : focusedActivities) + { + if (activity.window != null) + invalidateFocus1 (activity.window); + } + + /* Send focus in- and out- events to the previous and current + focus. */ + + if (oldFocus != null) + EmacsNative.sendFocusOut (oldFocus.handle, + System.currentTimeMillis ()); + + if (focusedWindow != null) + EmacsNative.sendFocusIn (focusedWindow.handle, + System.currentTimeMillis ()); + } + + @Override public void - attachChild (EmacsWindow child) + detachWindow () + { + if (window == null) + Log.w (TAG, "detachWindow called, but there is no window"); + else + { + /* Clear the window's pointer to this activity and remove the + window's view. */ + window.setConsumer (null); + layout.removeView (window.view); + window = null; + + invalidateFocus (); + } + } + + @Override + public void + attachWindow (EmacsWindow child) { if (window != null) throw new IllegalStateException ("trying to attach window when one" + " already exists"); /* Record and attach the view. */ + window = child; layout.addView (window.view); + child.setConsumer (this); - /* Remove the objects from the lists of what is available. */ - EmacsService.availableChildren.remove (child); - availableActivities.remove (this); - - /* Now set child->activity. */ - child.setActivity (this); + /* Invalidate the focus. */ + invalidateFocus (); } - /* Make this activity available for future windows to attach - again. */ - + @Override public void - makeAvailable () + destroy () { - window = null; - - for (EmacsWindow iterWindow - : EmacsService.availableChildren) - { - synchronized (iterWindow) - { - if (!iterWindow.isDestroyed ()) - attachChild (iterWindow); - - return; - } - } + finish (); + } - availableActivities.add (this); + @Override + public EmacsWindow + getAttachedWindow () + { + return window; } @Override @@ -109,38 +158,38 @@ public class EmacsActivity extends Activity /* Set it as the content view. */ setContentView (layout); - /* Make the activity available before starting the - service. */ - makeAvailable (); - if (EmacsService.SERVICE == null) /* Start the Emacs service now. */ startService (new Intent (this, EmacsService.class)); + /* Add this activity to the list of available activities. */ + EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this); + super.onCreate (savedInstanceState); } @Override public void - onStop () + onDestroy () { - /* The activity is no longer visible. If there is a window - attached, detach it. */ - - if (window != null) - { - layout.removeView (window.view); + /* The activity will die shortly hereafter. If there is a window + attached, close it now. */ + Log.d (TAG, "onDestroy " + this); + EmacsWindowAttachmentManager.MANAGER.removeWindowConsumer (this); + focusedActivities.remove (this); + invalidateFocus (); + super.onDestroy (); + } - /* Notice that the window is already available too. But do - not call noticeAvailableChild; that might assign it to some - other activity, which behaves badly. */ - EmacsService.availableChildren.add (window); - window = null; - } + @Override + public void + onWindowFocusChanged (boolean isFocused) + { + if (isFocused && !focusedActivities.contains (this)) + focusedActivities.add (this); + else + focusedActivities.remove (this); - /* Finally, remove this activity from the list of available - activities. */ - availableActivities.remove (this); - super.onStop (); + invalidateFocus (); } }; diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index f34d1ecde01..0dd5b2c1fb6 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -32,19 +32,22 @@ public class EmacsCopyArea implements EmacsPaintReq private int src_x, src_y, dest_x, dest_y, width, height; private EmacsDrawable destination, source; private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu; + private static Xfermode xorAlu, srcInAlu, overAlu; static { + overAlu = new PorterDuffXfermode (Mode.SRC_OVER); xorAlu = new PorterDuffXfermode (Mode.XOR); srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); }; public - EmacsCopyArea (EmacsDrawable destination, EmacsDrawable source, + EmacsCopyArea (EmacsDrawable source, EmacsDrawable destination, int src_x, int src_y, int width, int height, int dest_x, int dest_y, EmacsGC immutableGC) { + Bitmap bitmap; + this.destination = destination; this.source = source; this.src_x = src_x; @@ -71,6 +74,16 @@ public class EmacsCopyArea implements EmacsPaintReq return destination; } + private void + insetRectBy (Rect rect, int left, int top, int right, + int bottom) + { + rect.left += left; + rect.top += top; + rect.right -= right; + rect.bottom -= bottom; + } + @Override public EmacsGC getGC () @@ -86,16 +99,45 @@ public class EmacsCopyArea implements EmacsPaintReq Bitmap bitmap; Paint maskPaint; Canvas maskCanvas; - Bitmap maskBitmap; - Rect rect, srcRect; + Bitmap srcBitmap, maskBitmap, clipBitmap; + Rect rect, maskRect, srcRect, dstRect, maskDestRect; + boolean needFill; /* TODO implement stippling. */ if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; alu = immutableGC.function; + + /* A copy must be created or drawBitmap could end up overwriting + itself. */ + srcBitmap = source.getBitmap (); + + /* If srcBitmap is out of bounds, then adjust the source rectangle + to be within bounds. Note that tiling on windows with + backgrounds is unimplemented. */ + + if (src_x < 0) + { + width += src_x; + dest_x -= src_x; + src_x = 0; + } + + if (src_y < 0) + { + height += src_y; + dest_y -= src_y; + src_y = 0; + } + + if (src_x + width > srcBitmap.getWidth ()) + width = srcBitmap.getWidth () - src_x; + + if (src_y + height > srcBitmap.getHeight ()) + height = srcBitmap.getHeight () - src_y; + rect = getRect (); - bitmap = source.getBitmap (); if (alu == EmacsGC.GC_COPY) paint.setXfermode (null); @@ -103,29 +145,76 @@ public class EmacsCopyArea implements EmacsPaintReq paint.setXfermode (xorAlu); if (immutableGC.clip_mask == null) - canvas.drawBitmap (bitmap, new Rect (src_x, src_y, - src_x + width, - src_y + height), - rect, paint); + { + bitmap = Bitmap.createBitmap (srcBitmap, + src_x, src_y, width, + height); + canvas.drawBitmap (bitmap, null, rect, paint); + } else { - maskPaint = new Paint (); - srcRect = new Rect (0, 0, rect.width (), - rect.height ()); - maskBitmap - = immutableGC.clip_mask.bitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) + /* Drawing with a clip mask involves calculating the + intersection of the clip mask with the dst rect, and + extrapolating the corresponding part of the src rect. */ + clipBitmap = immutableGC.clip_mask.bitmap; + dstRect = new Rect (dest_x, dest_y, + dest_x + width, + dest_y + height); + maskRect = new Rect (immutableGC.clip_x_origin, + immutableGC.clip_y_origin, + (immutableGC.clip_x_origin + + clipBitmap.getWidth ()), + (immutableGC.clip_y_origin + + clipBitmap.getHeight ())); + clipBitmap = immutableGC.clip_mask.bitmap; + + if (!maskRect.setIntersect (dstRect, maskRect)) + /* There is no intersection between the clip mask and the + dest rect. */ return; - maskPaint.setXfermode (srcInAlu); + /* Now figure out which part of the source corresponds to + maskRect and return it relative to srcBitmap. */ + srcRect = new Rect (src_x, src_y, src_x + width, + src_y + height); + insetRectBy (srcRect, maskRect.left - dstRect.left, + maskRect.top - dstRect.top, + maskRect.right - dstRect.right, + maskRect.bottom - dstRect.bottom); + + /* Finally, create a temporary bitmap that is the size of + maskRect. */ + + maskBitmap + = Bitmap.createBitmap (maskRect.width (), maskRect.height (), + Bitmap.Config.ARGB_8888); + + /* Draw the mask onto the maskBitmap. */ maskCanvas = new Canvas (maskBitmap); - maskCanvas.drawBitmap (bitmap, new Rect (src_x, src_y, - src_x + width, - src_y + height), - srcRect, maskPaint); - canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + maskRect.offset (-immutableGC.clip_x_origin, + -immutableGC.clip_y_origin); + maskCanvas.drawBitmap (immutableGC.clip_mask.bitmap, + maskRect, new Rect (0, 0, + maskRect.width (), + maskRect.height ()), + paint); + maskRect.offset (immutableGC.clip_x_origin, + immutableGC.clip_y_origin); + + /* Set the transfer mode to SRC_IN to preserve only the parts + of the source that overlap with the mask. */ + maskPaint = new Paint (); + maskPaint.setXfermode (srcInAlu); + + /* Draw the source. */ + maskDestRect = new Rect (0, 0, srcRect.width (), + srcRect.height ()); + maskCanvas.drawBitmap (srcBitmap, srcRect, maskDestRect, + maskPaint); + + /* Finally, draw the mask bitmap to the destination. */ + paint.setXfermode (overAlu); + canvas.drawBitmap (maskBitmap, null, maskRect, paint); } } } diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index 462bf7c85b5..e3f28227146 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -57,7 +57,10 @@ public class EmacsDrawRectangle implements EmacsPaintReq public Rect getRect () { - return new Rect (x, y, x + width, y + height); + /* Canvas.drawRect actually behaves exactly like PolyRectangle wrt + to where the lines are placed, so extend the width and height + by 1 in the damage rectangle. */ + return new Rect (x, y, x + width + 1, y + height + 1); } @Override @@ -89,9 +92,10 @@ public class EmacsDrawRectangle implements EmacsPaintReq return; alu = immutableGC.function; - rect = getRect (); + rect = new Rect (x, y, x + width, y + height); paint.setStyle (Paint.Style.STROKE); + paint.setStrokeWidth (1); if (alu == EmacsGC.GC_COPY) paint.setXfermode (null); diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java index f419e71059d..9f40aa04c44 100644 --- a/java/org/gnu/emacs/EmacsFontDriver.java +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -21,6 +21,8 @@ package org.gnu.emacs; import java.util.List; +import android.os.Build; + public abstract class EmacsFontDriver { /* Font weights. */ @@ -75,6 +77,7 @@ public abstract class EmacsFontDriver public Integer size; public Integer spacing; public Integer avgwidth; + public Integer dpi; @Override public String @@ -88,7 +91,8 @@ public abstract class EmacsFontDriver + " weight: " + weight + " slant: " + slant + " spacing: " + spacing - + " avgwidth: " + avgwidth); + + " avgwidth: " + avgwidth + + " dpi: " + dpi); } }; @@ -99,6 +103,17 @@ public abstract class EmacsFontDriver public short width; public short ascent; public short descent; + + @Override + public String + toString () + { + return ("lbearing " + lbearing + + " rbearing " + rbearing + + " width " + width + + " ascent " + ascent + + " descent " + descent); + } } public class FontEntity extends FontSpec @@ -139,12 +154,19 @@ public abstract class EmacsFontDriver public abstract FontObject openFont (FontEntity fontEntity, int pixelSize); public abstract int hasChar (FontSpec font, char charCode); public abstract void textExtents (FontObject font, int code[], - FontMetrics fontMetrics[]); + FontMetrics fontMetrics); public abstract int encodeChar (FontObject fontObject, char charCode); + public abstract int draw (FontObject fontObject, EmacsGC gc, + EmacsDrawable drawable, int[] chars, + int x, int y, int backgroundWidth, + boolean withBackground); public static EmacsFontDriver createFontDriver () { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) + return new EmacsSdk23FontDriver (); + return new EmacsSdk7FontDriver (); } }; diff --git a/java/org/gnu/emacs/EmacsMultitaskActivity.java b/java/org/gnu/emacs/EmacsMultitaskActivity.java new file mode 100644 index 00000000000..dbdc99a3559 --- /dev/null +++ b/java/org/gnu/emacs/EmacsMultitaskActivity.java @@ -0,0 +1,25 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +public class EmacsMultitaskActivity extends EmacsActivity +{ + +} diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 6550e6fa2a1..c80339031a8 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -38,10 +38,15 @@ public class EmacsNative libDir must be the package's data storage location for native libraries. It is used as PATH. + pixelDensityX and pixelDensityY are the DPI values that will be + used by Emacs. + emacsService must be the emacsService singleton. */ public static native void setEmacsParams (AssetManager assetManager, String filesDir, String libDir, + float pixelDensityX, + float pixelDensityY, EmacsService emacsService); /* Initialize Emacs with the argument array ARGV. Each argument @@ -59,11 +64,20 @@ public class EmacsNative /* Send an ANDROID_KEY_PRESS event. */ public static native void sendKeyPress (short window, long time, int state, - int keyCode); + int keyCode, int unicodeChar); /* Send an ANDROID_KEY_RELEASE event. */ public static native void sendKeyRelease (short window, long time, int state, - int keyRelease); + int keyCode, int unicodeChar); + + /* Send an ANDROID_FOCUS_IN event. */ + public static native void sendFocusIn (short window, long time); + + /* Send an ANDROID_FOCUS_OUT event. */ + public static native void sendFocusOut (short window, long time); + + /* Send an ANDROID_WINDOW_ACTION event. */ + public static native void sendWindowAction (short window, int action); static { diff --git a/java/org/gnu/emacs/EmacsPaintQueue.java b/java/org/gnu/emacs/EmacsPaintQueue.java index 5af5868d3b9..f4840dbf5ae 100644 --- a/java/org/gnu/emacs/EmacsPaintQueue.java +++ b/java/org/gnu/emacs/EmacsPaintQueue.java @@ -47,7 +47,7 @@ public class EmacsPaintQueue { EmacsDrawable drawable, last; Canvas canvas; - EmacsGC gc, lastGC; + EmacsGC gc; int i; Paint paint; Rect rect, offsetRect, copyRect; @@ -60,45 +60,34 @@ public class EmacsPaintQueue for (EmacsPaintReq req : paintOperations) { drawable = req.getDrawable (); - - synchronized (req) - { - /* Ignore graphics requests for drawables that have been - destroyed. */ - if (drawable.isDestroyed ()) - continue; - } - canvas = drawable.lockCanvas (); if (canvas == null) /* No canvas is currently available. */ continue; - lastGC = gc; gc = req.getGC (); rect = req.getRect (); + drawable.damageRect (rect); + if (gc.clip_rects == null) { /* No clipping is applied. Just draw and continue. */ - canvas.save (); req.paintTo (canvas, paint, gc); - canvas.restore (); - drawable.damageRect (rect); continue; } if (gc.clip_rects != null && gc.clip_rects.length > 0) { - canvas.save (); - if (gc.clip_rects.length == 1) { /* There is only a single clip rect, which is simple enough. */ + canvas.save (); canvas.clipRect (gc.clip_rects[0]); req.paintTo (canvas, paint, gc); + canvas.restore (); } else { @@ -122,9 +111,6 @@ public class EmacsPaintQueue } } } - - drawable.damageRect (rect); - canvas.restore (); } } } diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index ccd5f1e0043..897902c5b57 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -69,6 +69,35 @@ public class EmacsPixmap extends EmacsHandleObject this.depth = depth; } + public + EmacsPixmap (short handle, int width, int height, int depth) + { + super (handle); + + if (depth != 1 && depth != 24) + throw new IllegalArgumentException ("Invalid depth specified" + + " for pixmap: " + depth); + + switch (depth) + { + case 1: + bitmap = Bitmap.createBitmap (width, height, + Bitmap.Config.ALPHA_8, + false); + break; + + case 24: + bitmap = Bitmap.createBitmap (width, height, + Bitmap.Config.ARGB_8888, + false); + break; + } + + this.width = width; + this.height = height; + this.depth = depth; + } + @Override public Canvas lockCanvas () diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java new file mode 100644 index 00000000000..34e2b1803a2 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java @@ -0,0 +1,43 @@ +/* Font backend for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.graphics.Paint; + +public class EmacsSdk23FontDriver extends EmacsSdk7FontDriver +{ + @Override + public int + hasChar (FontSpec font, char charCode) + { + Sdk7FontObject fontObject; + Paint paint; + + if (font instanceof Sdk7FontObject) + { + fontObject = (Sdk7FontObject) font; + paint = fontObject.typeface.typefacePaint; + } + else + paint = ((Sdk7FontEntity) font).typeface.typefacePaint; + + return paint.hasGlyph (String.valueOf (charCode)) ? 1 : 0; + } +}; diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index 5a8cdbfc75b..437f38e62db 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -28,16 +28,19 @@ import java.util.List; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; +import android.graphics.Canvas; import android.util.Log; +import android.os.Build; + public class EmacsSdk7FontDriver extends EmacsFontDriver { private static final String TOFU_STRING = "\uDB3F\uDFFD"; private static final String EM_STRING = "m"; private static final String TAG = "EmacsSdk7FontDriver"; - private class Sdk7Typeface + protected class Sdk7Typeface { /* The typeface and paint. */ public Typeface typeface; @@ -57,7 +60,10 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver width = UNSPECIFIED; spacing = PROPORTIONAL; + this.typeface = typeface; + typefacePaint = new Paint (); + typefacePaint.setAntiAlias (true); typefacePaint.setTypeface (typeface); /* For the calls to measureText below. */ @@ -160,7 +166,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } }; - private class Sdk7FontEntity extends FontEntity + protected class Sdk7FontEntity extends FontEntity { /* The typeface. */ public Sdk7Typeface typeface; @@ -177,19 +183,17 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver slant = typeface.slant; spacing = typeface.spacing; width = typeface.width; + dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f); this.typeface = typeface; } }; - private class Sdk7FontObject extends FontObject + protected class Sdk7FontObject extends FontObject { /* The typeface. */ public Sdk7Typeface typeface; - /* The text size. */ - public int pixelSize; - public Sdk7FontObject (Sdk7Typeface typeface, int pixelSize) { @@ -205,6 +209,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver slant = typeface.slant; spacing = typeface.spacing; width = typeface.width; + dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f); /* Compute the ascent and descent. */ typeface.typefacePaint.setTextSize (pixelSize); @@ -238,6 +243,93 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } }; + private class Sdk7DrawString implements EmacsPaintReq + { + private boolean drawBackground; + private Sdk7FontObject fontObject; + private char[] chars; + private EmacsGC immutableGC; + private EmacsDrawable drawable; + private Rect rect; + private int originX, originY; + + public + Sdk7DrawString (Sdk7FontObject fontObject, char[] chars, + EmacsGC immutableGC, EmacsDrawable drawable, + boolean drawBackground, Rect rect, + int originX, int originY) + { + this.fontObject = fontObject; + this.chars = chars; + this.immutableGC = immutableGC; + this.drawable = drawable; + this.drawBackground = drawBackground; + this.rect = rect; + this.originX = originX; + this.originY = originY; + } + + @Override + public EmacsDrawable + getDrawable () + { + return drawable; + } + + @Override + public EmacsGC + getGC () + { + return immutableGC; + } + + @Override + public void + paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) + { + int scratch; + + paint.setStyle (Paint.Style.FILL); + + if (drawBackground) + { + paint.setColor (immutableGC.background | 0xff000000); + canvas.drawRect (rect, paint); + } + + paint.setTextSize (fontObject.pixelSize); + paint.setColor (immutableGC.foreground | 0xff000000); + paint.setTypeface (fontObject.typeface.typeface); + paint.setAntiAlias (true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + /* Disable hinting as that leads to displayed text not + matching the computed metrics. */ + paint.setHinting (Paint.HINTING_OFF); + + canvas.drawText (chars, 0, chars.length, originX, originY, paint); + paint.setAntiAlias (false); + } + + @Override + public Rect + getRect () + { + Rect rect; + + rect = new Rect (); + + fontObject.typeface.typefacePaint.setTextSize (fontObject.pixelSize); + fontObject.typeface.typefacePaint.getTextBounds (chars, 0, chars.length, + rect); + + /* Add the background rect to the damage as well. */ + rect.union (this.rect); + + return rect; + } + }; + private String[] fontFamilyList; private Sdk7Typeface[] typefaceList; private Sdk7Typeface fallbackTypeface; @@ -252,7 +344,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver systemFontsDirectory = new File ("/system/fonts"); fontFamilyList = systemFontsDirectory.list (); - typefaceList = new Sdk7Typeface[fontFamilyList.length]; + typefaceList = new Sdk7Typeface[fontFamilyList.length + 3]; /* It would be nice to avoid opening each and every font upon startup. But that doesn't seem to be possible on @@ -267,8 +359,18 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver typeface); } + /* Initialize the default monospace and serif typefaces. */ fallbackTypeface = new Sdk7Typeface ("monospace", Typeface.MONOSPACE); + typefaceList[fontFamilyList.length] = fallbackTypeface; + + fallbackTypeface = new Sdk7Typeface ("Monospace", + Typeface.MONOSPACE); + typefaceList[fontFamilyList.length + 1] = fallbackTypeface; + + fallbackTypeface = new Sdk7Typeface ("Sans Serif", + Typeface.DEFAULT); + typefaceList[fontFamilyList.length + 2] = fallbackTypeface; } private boolean @@ -278,11 +380,6 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver && !fontSpec.family.equals (typeface.familyName)) return false; - - if (fontSpec.adstyle != null - && !fontSpec.adstyle.isEmpty ()) - /* return false; */; - if (fontSpec.slant != null && !fontSpec.weight.equals (typeface.weight)) return false; @@ -393,7 +490,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver paint.getTextBounds (TOFU_STRING, 0, TOFU_STRING.length (), rect1); paint.getTextBounds ("" + charCode, 0, 1, rect2); - return rect1.equals (rect2) ? 1 : 0; + return rect1.equals (rect2) ? 0 : 1; } private void @@ -434,21 +531,47 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver @Override public void - textExtents (FontObject font, int code[], FontMetrics fontMetrics[]) + textExtents (FontObject font, int code[], FontMetrics fontMetrics) { int i; Paint paintCache; Rect boundsCache; Sdk7FontObject fontObject; + char[] text; + float width; fontObject = (Sdk7FontObject) font; paintCache = fontObject.typeface.typefacePaint; paintCache.setTextSize (fontObject.pixelSize); boundsCache = new Rect (); - for (i = 0; i < code.length; ++i) - textExtents1 ((Sdk7FontObject) font, code[i], fontMetrics[i], + if (code.length == 0) + { + fontMetrics.lbearing = 0; + fontMetrics.rbearing = 0; + fontMetrics.ascent = 0; + fontMetrics.descent = 0; + fontMetrics.width = 0; + } + else if (code.length == 1) + textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics, paintCache, boundsCache); + else + { + text = new char[code.length]; + + for (i = 0; i < code.length; ++i) + text[i] = (char) code[i]; + + paintCache.getTextBounds (text, 0, 1, boundsCache); + width = paintCache.measureText (text, 0, code.length); + + fontMetrics.lbearing = (short) boundsCache.left; + fontMetrics.rbearing = (short) boundsCache.right; + fontMetrics.ascent = (short) -boundsCache.top; + fontMetrics.descent = (short) boundsCache.bottom; + fontMetrics.width = (short) Math.round (width); + } } @Override @@ -457,4 +580,37 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver { return charCode; } + + @Override + public int + draw (FontObject fontObject, EmacsGC gc, EmacsDrawable drawable, + int[] chars, int x, int y, int backgroundWidth, + boolean withBackground) + { + Rect backgroundRect; + Sdk7FontObject sdk7FontObject; + Sdk7DrawString op; + char[] charsArray; + int i; + + sdk7FontObject = (Sdk7FontObject) fontObject; + charsArray = new char[chars.length]; + + for (i = 0; i < chars.length; ++i) + charsArray[i] = (char) chars[i]; + + backgroundRect = new Rect (); + backgroundRect.top = y - sdk7FontObject.ascent; + backgroundRect.left = x; + backgroundRect.right = x + backgroundWidth; + backgroundRect.bottom = y + sdk7FontObject.descent; + + op = new Sdk7DrawString (sdk7FontObject, charsArray, + gc.immutableGC (), drawable, + withBackground, + backgroundRect, x, y); + + EmacsService.SERVICE.appendPaintOperation (op); + return 1; + } }; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 311226e6f7e..41a45b0bd85 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -28,6 +28,8 @@ import android.graphics.Canvas; import android.graphics.Bitmap; import android.graphics.Point; +import android.view.View; + import android.annotation.TargetApi; import android.app.Service; import android.content.Context; @@ -37,7 +39,9 @@ import android.os.Build; import android.os.Looper; import android.os.IBinder; import android.os.Handler; + import android.util.Log; +import android.util.DisplayMetrics; class Holder { @@ -57,14 +61,8 @@ public class EmacsService extends Service private Handler handler; private EmacsPaintQueue paintQueue; - /* List of all EmacsWindows that are available to attach to an - activity. */ - public static List availableChildren; - - static - { - availableChildren = new ArrayList (); - }; + /* Display metrics used by font backends. */ + public DisplayMetrics metrics; @Override public int @@ -88,7 +86,7 @@ public class EmacsService extends Service Context context; context = getApplicationContext (); - apiLevel = android.os.Build.VERSION.SDK_INT; + apiLevel = Build.VERSION.SDK_INT; if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) return context.getApplicationInfo().nativeLibraryDir; @@ -105,11 +103,16 @@ public class EmacsService extends Service AssetManager manager; Context app_context; String filesDir, libDir; + double pixelDensityX; + double pixelDensityY; SERVICE = this; handler = new Handler (Looper.getMainLooper ()); manager = getAssets (); app_context = getApplicationContext (); + metrics = getResources ().getDisplayMetrics (); + pixelDensityX = metrics.xdpi; + pixelDensityY = metrics.ydpi; try { @@ -122,6 +125,8 @@ public class EmacsService extends Service + " and libDir = " + libDir); EmacsNative.setEmacsParams (manager, filesDir, libDir, + (float) pixelDensityX, + (float) pixelDensityY, this); /* Start the thread that runs Emacs. */ @@ -147,7 +152,8 @@ public class EmacsService extends Service } EmacsView - getEmacsView (final EmacsWindow window) + getEmacsView (final EmacsWindow window, final int visibility, + final boolean isFocusedByDefault) { Runnable runnable; final Holder view; @@ -161,6 +167,8 @@ public class EmacsService extends Service synchronized (this) { view.thing = new EmacsView (window); + view.thing.setVisibility (visibility); + view.thing.setFocusedByDefault (isFocusedByDefault); notify (); } } @@ -183,48 +191,6 @@ public class EmacsService extends Service return view.thing; } - /* Notice that a child of the root window named WINDOW is now - available for attachment to a specific activity. */ - - public void - noticeAvailableChild (final EmacsWindow window) - { - Log.d (TAG, "A new child is available: " + window); - - handler.post (new Runnable () { - public void - run () - { - for (EmacsActivity activity - : EmacsActivity.availableActivities) - { - /* TODO: check if the activity matches. */ - activity.attachChild (window); - break; - } - - /* Nope, wait for an activity to become available. */ - availableChildren.add (window); - } - }); - } - - /* Notice that a child of the root window named WINDOW has been - destroyed. */ - - public void - noticeChildDestroyed (final EmacsWindow child) - { - handler.post (new Runnable () { - @Override - public void - run () - { - availableChildren.remove (child); - } - }); - } - /* X drawing operations. These are quite primitive operations. The drawing queue is kept on the Emacs thread, but is periodically flushed to the application thread, upon buffers swaps and once it @@ -311,11 +277,6 @@ public class EmacsService extends Service ensurePaintQueue (); - if (gc.clip_rects != null && gc.clip_rects.length >= 1) - android.util.Log.d ("drawRectangle", - gc.clip_rects[0].toString () - + " " + gc.toString ()); - req = new EmacsDrawRectangle (drawable, x, y, width, height, gc.immutableGC ()); @@ -381,4 +342,12 @@ public class EmacsService extends Service { window.clearArea (x, y, width, height); } + + public void + appendPaintOperation (EmacsPaintReq op) + { + ensurePaintQueue (); + paintQueue.appendPaintOperation (op); + checkFlush (); + } }; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 194f6ad37a3..b8b828e4820 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -22,6 +22,8 @@ package org.gnu.emacs; import android.view.SurfaceView; import android.view.SurfaceHolder; +import android.os.Build; + import android.graphics.Canvas; import android.graphics.Rect; @@ -40,7 +42,9 @@ public class EmacsSurfaceView extends SurfaceView surfaceChanged (SurfaceHolder holder, int format, int width, int height) { - + /* Force a buffer swap now to get the contents of the Emacs + view on screen. */ + view.swapBuffers (true); } @Override @@ -51,7 +55,7 @@ public class EmacsSurfaceView extends SurfaceView /* Force a buffer swap now to get the contents of the Emacs view on screen. */ - view.swapBuffers (); + view.swapBuffers (true); } @Override @@ -72,12 +76,37 @@ public class EmacsSurfaceView extends SurfaceView public Canvas lockCanvas (Rect damage) { - return getHolder ().lockCanvas (damage); + SurfaceHolder holder; + + holder = getHolder (); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + damage.setEmpty (); + return holder.lockHardwareCanvas (); + } + + return holder.lockCanvas (damage); + } + + /* This method is only used during debugging when it seems damage + isn't working correctly. */ + + public Canvas + lockCanvas () + { + SurfaceHolder holder; + + holder = getHolder (); + return holder.lockCanvas (); } public void unlockCanvasAndPost (Canvas canvas) { - getHolder ().unlockCanvasAndPost (canvas); + SurfaceHolder holder; + + holder = getHolder (); + holder.unlockCanvasAndPost (canvas); } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 237946d6366..7b48eaf0aa6 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -19,17 +19,20 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import android.content.res.ColorStateList; + import android.view.View; import android.view.KeyEvent; import android.view.ViewGroup; + import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Region; import android.graphics.Paint; -import android.util.Log; import android.os.Build; +import android.util.Log; /* This is an Android view which has a back and front buffer. When swapBuffers is called, the back buffer is swapped to the front @@ -70,10 +73,11 @@ public class EmacsView extends ViewGroup this.damageRegion = new Region (); this.paint = new Paint (); + setFocusable (true); + setFocusableInTouchMode (true); + /* Create the surface view. */ this.surfaceView = new EmacsSurfaceView (this); - - setFocusable (FOCUSABLE); addView (this.surfaceView); } @@ -162,7 +166,7 @@ public class EmacsView extends ViewGroup } public void - swapBuffers () + swapBuffers (boolean force) { Bitmap back; Canvas canvas; @@ -185,14 +189,25 @@ public class EmacsView extends ViewGroup if (canvas == null) return; - /* Copy from the back buffer to the canvas. */ - canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + /* Copy from the back buffer to the canvas. If damageRect was + made empty, then draw the entire back buffer. */ + + if (damageRect.isEmpty ()) + canvas.drawBitmap (bitmap, 0f, 0f, paint); + else + canvas.drawBitmap (bitmap, damageRect, damageRect, paint); /* Unlock the canvas and clear the damage. */ surfaceView.unlockCanvasAndPost (canvas); damageRegion.setEmpty (); } + public void + swapBuffers () + { + swapBuffers (false); + } + @Override public boolean onKeyDown (int keyCode, KeyEvent event) @@ -201,6 +216,14 @@ public class EmacsView extends ViewGroup return true; } + @Override + public boolean + onKeyMultiple (int keyCode, int repeatCount, KeyEvent event) + { + window.onKeyDown (keyCode, event); + return true; + } + @Override public boolean onKeyUp (int keyCode, KeyEvent event) @@ -208,4 +231,14 @@ public class EmacsView extends ViewGroup window.onKeyUp (keyCode, event); return true; } + + @Override + public void + onFocusChanged (boolean gainFocus, int direction, + Rect previouslyFocusedRect) + { + window.onFocusChanged (gainFocus); + super.onFocusChanged (gainFocus, direction, + previouslyFocusedRect); + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 28db04a261d..26e788a20a8 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -32,6 +32,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.KeyEvent; +import android.content.Intent; + /* This defines a window, which is a handle. Windows represent a rectangular subset of the screen with their own contents. @@ -57,10 +59,10 @@ public class EmacsWindow extends EmacsHandleObject /* List of all children in stacking order. This must be kept consistent! */ - private ArrayList children; + public ArrayList children; - /* The EmacsActivity currently attached, if it exists. */ - private EmacsActivity attached; + /* The window consumer currently attached, if it exists. */ + private EmacsWindowAttachmentManager.WindowConsumer attached; /* The window background scratch GC. foreground is always the window background. */ @@ -74,35 +76,44 @@ public class EmacsWindow extends EmacsHandleObject rect = new Rect (x, y, x + width, y + height); - /* Create the view from the context's UI thread. */ - view = EmacsService.SERVICE.getEmacsView (this); + /* Create the view from the context's UI thread. The window is + unmapped, so the view is GONE. */ + view = EmacsService.SERVICE.getEmacsView (this, View.GONE, + parent == null); this.parent = parent; - children = new ArrayList (); - /* The window is unmapped by default. */ - view.setVisibility (View.GONE); + /* Create the list of children. */ + children = new ArrayList (); - /* If parent is the root window, notice that there are new - children available for interested activites to pick up. */ - if (parent == null) - EmacsService.SERVICE.noticeAvailableChild (this); - else + if (parent != null) { - /* Otherwise, directly add this window as a child of that - window's view. */ - synchronized (parent) + parent.children.add (this); + parent.view.post (new Runnable () { + @Override + public void + run () + { + parent.view.addView (view); + } + }); + } + else + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () { - parent.children.add (this); - parent.view.post (new Runnable () { - @Override - public void - run () - { - parent.view.addView (view); - } - }); + EmacsWindowAttachmentManager manager; + + manager = EmacsWindowAttachmentManager.MANAGER; + + /* If parent is the root window, notice that there are new + children available for interested activites to pick + up. */ + + manager.registerWindow (EmacsWindow.this); } - } + }); scratchGC = new EmacsGC ((short) 0); } @@ -129,28 +140,35 @@ public class EmacsWindow extends EmacsHandleObject public void destroyHandle () throws IllegalStateException { - synchronized (this) - { - if (!children.isEmpty ()) - throw new IllegalStateException ("Trying to destroy window with " - + "children!"); - } + if (parent != null) + parent.children.remove (this); + + EmacsActivity.invalidateFocus (); - /* Notice that the child has been destroyed. */ - EmacsService.SERVICE.noticeChildDestroyed (this); + if (!children.isEmpty ()) + throw new IllegalStateException ("Trying to destroy window with " + + "children!"); /* Remove the view from its parent and make it invisible. */ view.post (new Runnable () { public void run () { + View parent; + EmacsWindowAttachmentManager manager; + + if (EmacsActivity.focusedWindow == EmacsWindow.this) + EmacsActivity.focusedWindow = null; + + manager = EmacsWindowAttachmentManager.MANAGER; view.setVisibility (View.GONE); - if (view.getParent () != null) - ((ViewGroup) view.getParent ()).removeView (view); + parent = (View) view.getParent (); - if (attached != null) - attached.makeAvailable (); + if (parent != null && attached == null) + ((ViewGroup) parent).removeView (view); + + manager.detachWindow (EmacsWindow.this); } }); @@ -158,12 +176,15 @@ public class EmacsWindow extends EmacsHandleObject } public void - setActivity (EmacsActivity activity) + setConsumer (EmacsWindowAttachmentManager.WindowConsumer consumer) { - synchronized (this) - { - activity = activity; - } + attached = consumer; + } + + public EmacsWindowAttachmentManager.WindowConsumer + getAttachedConsumer () + { + return attached; } public void @@ -233,7 +254,10 @@ public class EmacsWindow extends EmacsHandleObject public void run () { + view.setVisibility (View.VISIBLE); + /* Eventually this should check no-focus-on-map. */ + view.requestFocus (); } }); } @@ -319,18 +343,47 @@ public class EmacsWindow extends EmacsHandleObject public void onKeyDown (int keyCode, KeyEvent event) { + int state; + + state = event.getModifiers (); + state &= ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); + EmacsNative.sendKeyPress (this.handle, event.getEventTime (), event.getModifiers (), - keyCode); + keyCode, + /* Ignore meta-state understood by Emacs + for now, or Ctrl+C will not be + recognized as an ASCII key press + event. */ + event.getUnicodeChar (state)); } public void onKeyUp (int keyCode, KeyEvent event) { + int state; + + state = event.getModifiers (); + state &= ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); + EmacsNative.sendKeyRelease (this.handle, event.getEventTime (), event.getModifiers (), - keyCode); + keyCode, + event.getUnicodeChar (state)); + } + + public void + onFocusChanged (boolean gainFocus) + { + EmacsActivity.invalidateFocus (); + } + + public void + onActivityDetached () + { + /* Destroy the associated frame when the activity is detached. */ + EmacsNative.sendWindowAction (this.handle, 0); } }; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java new file mode 100644 index 00000000000..34be2ab8789 --- /dev/null +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -0,0 +1,166 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import java.util.LinkedList; +import java.util.List; + +import android.content.Intent; +import android.util.Log; + +/* Code to paper over the differences in lifecycles between + "activities" and windows. There are four interfaces to an instance + of this class: + + registerWindowConsumer (WindowConsumer) + registerWindow (EmacsWindow) + removeWindowConsumer (WindowConsumer) + removeWindow (EmacsWindow) + + A WindowConsumer is expected to allow an EmacsWindow to be attached + to it, and be created or destroyed. + + Every time a window is created, registerWindow checks the list of + window consumers. If a consumer exists and does not currently have + a window of its own attached, it gets the new window. Otherwise, + the window attachment manager starts a new consumer. + + Every time a consumer is registered, registerWindowConsumer checks + the list of available windows. If a window exists and is not + currently attached to a consumer, then the consumer gets it. + + Finally, every time a window is removed, the consumer is + destroyed. */ + +public class EmacsWindowAttachmentManager +{ + public static EmacsWindowAttachmentManager MANAGER; + private final static String TAG = "EmacsWindowAttachmentManager"; + + static + { + MANAGER = new EmacsWindowAttachmentManager (); + }; + + public interface WindowConsumer + { + public void attachWindow (EmacsWindow window); + public EmacsWindow getAttachedWindow (); + public void detachWindow (); + public void destroy (); + }; + + private List consumers; + private List windows; + + public + EmacsWindowAttachmentManager () + { + consumers = new LinkedList (); + windows = new LinkedList (); + } + + public void + registerWindowConsumer (WindowConsumer consumer) + { + Log.d (TAG, "registerWindowConsumer " + consumer); + + consumers.add (consumer); + + for (EmacsWindow window : windows) + { + if (window.getAttachedConsumer () == null) + { + Log.d (TAG, "registerWindowConsumer: attaching " + window); + consumer.attachWindow (window); + return; + } + } + + Log.d (TAG, "registerWindowConsumer: sendWindowAction 0, 0"); + EmacsNative.sendWindowAction ((short) 0, 0); + } + + public void + registerWindow (EmacsWindow window) + { + Intent intent; + + Log.d (TAG, "registerWindow " + window); + windows.add (window); + + for (WindowConsumer consumer : consumers) + { + if (consumer.getAttachedWindow () == null) + { + Log.d (TAG, "registerWindow: attaching " + consumer); + consumer.attachWindow (window); + return; + } + } + + intent = new Intent (EmacsService.SERVICE, + EmacsMultitaskActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + EmacsService.SERVICE.startActivity (intent); + Log.d (TAG, "registerWindow: startActivity"); + } + + public void + removeWindowConsumer (WindowConsumer consumer) + { + EmacsWindow window; + + Log.d (TAG, "removeWindowConsumer " + consumer); + + window = consumer.getAttachedWindow (); + + if (window != null) + { + Log.d (TAG, "removeWindowConsumer: detaching " + window); + + consumer.detachWindow (); + window.onActivityDetached (); + } + + Log.d (TAG, "removeWindowConsumer: removing " + consumer); + consumers.remove (consumer); + } + + public void + detachWindow (EmacsWindow window) + { + WindowConsumer consumer; + + Log.d (TAG, "detachWindow " + window); + + if (window.getAttachedConsumer () != null) + { + consumer = window.getAttachedConsumer (); + + Log.d (TAG, "detachWindow: removing" + consumer); + + consumers.remove (consumer); + consumer.destroy (); + } + } +}; diff --git a/lisp/cus-edit.el b/lisp/cus-edit.el index 65eb066a554..6e71bfa6db6 100644 --- a/lisp/cus-edit.el +++ b/lisp/cus-edit.el @@ -2209,7 +2209,7 @@ and `face'." ;;; The `custom' Widget. (defface custom-button - '((((type x w32 ns haiku pgtk) (class color)) ; Like default mode line + '((((type x w32 ns haiku pgtk android) (class color)) ; Like default mode line :box (:line-width 2 :style released-button) :background "lightgrey" :foreground "black")) "Face for custom buffer buttons if `custom-raised-buttons' is non-nil." @@ -2217,7 +2217,7 @@ and `face'." :group 'custom-faces) (defface custom-button-mouse - '((((type x w32 ns haiku pgtk) (class color)) + '((((type x w32 ns haiku pgtk android) (class color)) :box (:line-width 2 :style released-button) :background "grey90" :foreground "black") (t @@ -2242,7 +2242,7 @@ and `face'." (if custom-raised-buttons 'custom-button-mouse 'highlight)) (defface custom-button-pressed - '((((type x w32 ns haiku pgtk) (class color)) + '((((type x w32 ns haiku pgtk android) (class color)) :box (:line-width 2 :style pressed-button) :background "lightgrey" :foreground "black") (t :inverse-video t)) diff --git a/lisp/faces.el b/lisp/faces.el index 0dd89be4738..2779bfc295f 100644 --- a/lisp/faces.el +++ b/lisp/faces.el @@ -2911,7 +2911,7 @@ Note: Other faces cannot inherit from the cursor face." (((type haiku)) :foreground "B_MENU_ITEM_TEXT_COLOR" :background "B_MENU_BACKGROUND_COLOR") - (((type x w32 ns pgtk) (class color)) + (((type x w32 ns pgtk android) (class color)) :background "grey75") (((type x) (class mono)) :background "grey")) diff --git a/src/android.c b/src/android.c index dd841cf383a..db12e244275 100644 --- a/src/android.c +++ b/src/android.c @@ -24,6 +24,7 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include #include #include @@ -46,6 +47,7 @@ bool android_init_gui; #include #include +#include #include #include @@ -86,6 +88,7 @@ struct android_emacs_pixmap { jclass class; jmethodID constructor; + jmethodID constructor_mutable; }; struct android_graphics_point @@ -94,6 +97,12 @@ struct android_graphics_point jmethodID constructor; }; +struct android_emacs_drawable +{ + jclass class; + jmethodID get_bitmap; +}; + /* The asset manager being used. */ static AAssetManager *asset_manager; @@ -106,6 +115,12 @@ char *android_site_load_path; /* The path used to store native libraries. */ char *android_lib_dir; +/* The path used to store game files. */ +char *android_game_path; + +/* The display's pixel densities. */ +double android_pixel_density_x, android_pixel_density_y; + /* The Android application data directory. */ static char *android_files_dir; @@ -149,6 +164,9 @@ static struct android_emacs_pixmap pixmap_class; /* Various methods associated with the Point class. */ static struct android_graphics_point point_class; +/* Various methods associated with the EmacsDrawable class. */ +static struct android_emacs_drawable drawable_class; + /* Event handling functions. Events are stored on a (circular) queue @@ -383,6 +401,10 @@ android_write_event (union android_event *event) if (!container) return; + /* If the event queue hasn't been initialized yet, return false. */ + if (!event_queue.events.next) + return; + pthread_mutex_lock (&event_queue.mutex); /* The event queue is full, wait for events to be read. */ @@ -451,6 +473,10 @@ android_select (int nfds, fd_set *readfds, fd_set *writefds, /* Unlock the event queue mutex. */ pthread_mutex_unlock (&event_queue.mutex); + /* This is to shut up process.c when pselect gets EINTR. */ + if (nfds_return < 0) + errno = EINTR; + return nfds_return; } @@ -793,16 +819,20 @@ android_close (int fd) { if (fd < ANDROID_MAX_ASSET_FD && (android_table[fd].flags & ANDROID_FD_TABLE_ENTRY_IS_VALID)) - { - __android_log_print (ANDROID_LOG_INFO, __func__, - "closing android file descriptor %d", - fd); - android_table[fd].flags = 0; - } + android_table[fd].flags = 0; return close (fd); } +/* Return the current user's ``home'' directory, which is actually the + app data directory on Android. */ + +const char * +android_get_home_directory (void) +{ + return android_files_dir; +} + /* JNI functions called by Java. */ @@ -814,6 +844,8 @@ JNIEXPORT void JNICALL NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject local_asset_manager, jobject files_dir, jobject libs_dir, + jfloat pixel_density_x, + jfloat pixel_density_y, jobject emacs_service_object) { int pipefd[2]; @@ -829,6 +861,9 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, return; } + android_pixel_density_x = pixel_density_x; + android_pixel_density_y = pixel_density_y; + __android_log_print (ANDROID_LOG_INFO, __func__, "Initializing "PACKAGE_STRING"...\nPlease report bugs to " PACKAGE_BUGREPORT". Thanks.\n"); @@ -891,15 +926,23 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, if (!android_site_load_path) emacs_abort (); + android_game_path = malloc (PATH_MAX + 1); + + if (!android_game_path) + emacs_abort (); + snprintf (android_site_load_path, PATH_MAX, "%s/site-lisp", android_files_dir); + snprintf (android_game_path, PATH_MAX, "%s/scores", android_files_dir); + __android_log_print (ANDROID_LOG_INFO, __func__, "Site-lisp directory: %s\n" "Files directory: %s\n" - "Native code directory: %s", + "Native code directory: %s\n" + "Game score path: %s\n", android_site_load_path, android_files_dir, - android_lib_dir); + android_lib_dir, android_game_path); /* Make a reference to the Emacs service. */ emacs_service = (*env)->NewGlobalRef (env, emacs_service_object); @@ -997,6 +1040,7 @@ android_init_emacs_pixmap (void) assert (pixmap_class.c_name); FIND_METHOD (constructor, "", "(S[IIII)V"); + FIND_METHOD (constructor_mutable, "", "(SIII)V"); #undef FIND_METHOD } @@ -1031,6 +1075,36 @@ android_init_graphics_point (void) #undef FIND_METHOD } +static void +android_init_emacs_drawable (void) +{ + jclass old; + + drawable_class.class + = (*android_java_env)->FindClass (android_java_env, + "org/gnu/emacs/EmacsDrawable"); + eassert (drawable_class.class); + + old = drawable_class.class; + drawable_class.class + = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, + (jobject) old); + ANDROID_DELETE_LOCAL_REF (old); + + if (!drawable_class.class) + emacs_abort (); + +#define FIND_METHOD(c_name, name, signature) \ + drawable_class.c_name \ + = (*android_java_env)->GetMethodID (android_java_env, \ + drawable_class.class, \ + name, signature); \ + assert (drawable_class.c_name); + + FIND_METHOD (get_bitmap, "getBitmap", "()Landroid/graphics/Bitmap;"); +#undef FIND_METHOD +} + extern JNIEXPORT void JNICALL NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) { @@ -1063,6 +1137,15 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) android_init_emacs_service (); android_init_emacs_pixmap (); android_init_graphics_point (); + android_init_emacs_drawable (); + + /* Set HOME to the app data directory. */ + setenv ("HOME", android_files_dir, 1); + + /* Set the cwd to that directory as well. */ + if (chdir (android_files_dir)) + __android_log_print (ANDROID_LOG_WARN, __func__, + "chdir: %s", strerror (errno)); /* Initialize the Android GUI. */ android_init_gui = true; @@ -1099,7 +1182,8 @@ NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, extern JNIEXPORT void JNICALL NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, jshort window, jlong time, - jint state, jint keycode) + jint state, jint keycode, + jint unicode_char) { union android_event event; @@ -1108,6 +1192,7 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, event.xkey.time = time; event.xkey.state = state; event.xkey.keycode = keycode; + event.xkey.unicode_char = unicode_char; android_write_event (&event); } @@ -1115,7 +1200,8 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, extern JNIEXPORT void JNICALL NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, jshort window, jlong time, - jint state, jint keycode) + jint state, jint keycode, + jint unicode_char) { union android_event event; @@ -1124,6 +1210,46 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, event.xkey.time = time; event.xkey.state = state; event.xkey.keycode = keycode; + event.xkey.unicode_char = unicode_char; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, + jshort window, jlong time) +{ + union android_event event; + + event.xkey.type = ANDROID_FOCUS_IN; + event.xkey.window = window; + event.xkey.time = time; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, + jshort window, jlong time) +{ + union android_event event; + + event.xkey.type = ANDROID_FOCUS_OUT; + event.xkey.window = window; + event.xkey.time = time; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, + jshort window, jint action) +{ + union android_event event; + + event.xaction.type = ANDROID_WINDOW_ACTION; + event.xaction.window = window; + event.xaction.action = action; android_write_event (&event); } @@ -1142,13 +1268,6 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, #define MAX_HANDLE 65535 -enum android_handle_type - { - ANDROID_HANDLE_WINDOW, - ANDROID_HANDLE_GCONTEXT, - ANDROID_HANDLE_PIXMAP, - }; - struct android_handle_entry { /* The type. */ @@ -1245,7 +1364,7 @@ android_destroy_handle (android_handle handle) android_handles[handle].handle = NULL; } -static jobject +jobject android_resolve_handle (android_handle handle, enum android_handle_type type) { @@ -1625,14 +1744,15 @@ android_change_gc (struct android_gc *gc, ANDROID_HANDLE_PIXMAP); (*android_java_env)->SetObjectField (android_java_env, gcontext, - emacs_gc_stipple, + emacs_gc_clip_mask, what); /* Clearing GCClipMask also clears the clip rectangles. */ - (*android_java_env)->SetObjectField (android_java_env, - gcontext, - emacs_gc_clip_rects, - NULL); + if (!what) + (*android_java_env)->SetObjectField (android_java_env, + gcontext, + emacs_gc_clip_rects, + NULL); } if (mask & ANDROID_GC_STIPPLE) @@ -2008,10 +2128,23 @@ android_create_pixmap_from_bitmap_data (char *data, unsigned int width, { for (x = 0; x < width; ++x) { - if (data[y / 8] & (1 << (x % 8))) - region[x] = foreground; + if (depth == 24) + { + /* The alpha channels must be set, or otherwise, the + pixmap will be created entirely transparent. */ + + if (data[y * (width + 7) / 8 + x / 8] & (1 << (x % 8))) + region[x] = foreground | 0xff000000; + else + region[x] = background | 0xff000000; + } else - region[x] = background; + { + if (data[y * (width + 7) / 8 + x / 8] & (1 << (x % 8))) + region[x] = foreground; + else + region[x] = background; + } } (*android_java_env)->SetIntArrayRegion (android_java_env, @@ -2236,36 +2369,21 @@ android_create_pixmap (unsigned int width, unsigned int height, { android_handle prev_max_handle; jobject object; - jintArray colors; android_pixmap pixmap; - /* Create the color array holding the data. */ - colors = (*android_java_env)->NewIntArray (android_java_env, - width * height); - - if (!colors) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } - /* First, allocate the pixmap handle. */ prev_max_handle = max_handle; pixmap = android_alloc_id (); if (!pixmap) - { - ANDROID_DELETE_LOCAL_REF ((jobject) colors); - error ("Out of pixmap handles!"); - } + error ("Out of pixmap handles!"); object = (*android_java_env)->NewObject (android_java_env, pixmap_class.class, - pixmap_class.constructor, - (jshort) pixmap, colors, + pixmap_class.constructor_mutable, + (jshort) pixmap, (jint) width, (jint) height, (jint) depth); - ANDROID_DELETE_LOCAL_REF ((jobject) colors); if (!object) { @@ -2313,6 +2431,387 @@ android_clear_area (android_window handle, int x, int y, (jint) width, (jint) height); } +android_pixmap +android_create_bitmap_from_data (char *bits, unsigned int width, + unsigned int height) +{ + return android_create_pixmap_from_bitmap_data (bits, 1, 0, + width, height, 1); +} + +struct android_image * +android_create_image (unsigned int depth, enum android_image_format format, + char *data, unsigned int width, unsigned int height) +{ + struct android_image *image; + + image = xmalloc (sizeof *image); + + /* Fill in the fields required by image.c. N.B. that + android_destroy_image ostensibly will free data, but image.c + mostly sets and frees data itself. */ + image->width = width; + image->height = height; + image->data = data; + image->depth = depth; + image->format = format; + + /* Now fill in the image dimensions. There are only two depths + supported by this function. */ + + if (depth == 1) + { + image->bytes_per_line = (width + 7) / 8; + image->bits_per_pixel = 1; + } + else if (depth == 24) + { + image->bytes_per_line = width * 4; + image->bits_per_pixel = 32; + } + else + emacs_abort (); + + return image; +} + +void +android_destroy_image (struct android_image *ximg) +{ + /* If XIMG->data is NULL, then it has already been freed by + image.c. */ + + if (ximg->data) + xfree (ximg->data); + xfree (ximg); +} + +void +android_put_pixel (struct android_image *ximg, int x, int y, + unsigned long pixel) +{ + char *byte, *word; + unsigned int r, g, b; + + /* Ignore out-of-bounds accesses. */ + + if (x >= ximg->width || y >= ximg->height || x < 0 || y < 0) + return; + + switch (ximg->depth) + { + case 1: + byte = ximg->data + y * ximg->bytes_per_line + x / 8; + + if (pixel) + *byte |= (1 << x % 8); + else + *byte &= ~(1 << x % 8); + break; + + case 24: + /* Unaligned accesses are problematic on Android devices. */ + word = ximg->data + y * ximg->bytes_per_line + x * 4; + + /* Swizzle the pixel into ABGR format. Android uses Skia's + ``native color type'', which is ABGR. This is despite the + format being named ``ARGB'', and more confusingly + `ANDROID_BITMAP_FORMAT_RGBA_8888' in bitmap.h. */ + r = pixel & 0x00ff0000; + g = pixel & 0x0000ff00; + b = pixel & 0x000000ff; + pixel = (r >> 16) | g | (b << 16) | 0xff000000; + + memcpy (word, &pixel, sizeof pixel); + break; + } +} + +unsigned long +android_get_pixel (struct android_image *ximg, int x, int y) +{ + char *byte, *word; + unsigned int pixel, r, g, b; + + if (x >= ximg->width || y >= ximg->height + || x < 0 || y < 0) + return 0; + + switch (ximg->depth) + { + case 1: + byte = ximg->data + y * ximg->bytes_per_line + x / 8; + return (*byte & (1 << x % 8)) ? 1 : 0; + + case 24: + word = ximg->data + y * ximg->bytes_per_line + x * 4; + memcpy (&pixel, word, sizeof pixel); + + /* Convert the pixel back to RGB. */ + b = pixel & 0x00ff0000; + g = pixel & 0x0000ff00; + r = pixel & 0x000000ff; + pixel = ((r << 16) | g | (b >> 16)) & ~0xff000000; + + return pixel; + } + + emacs_abort (); +} + +struct android_image * +android_get_image (android_drawable handle, + enum android_image_format format) +{ + jobject drawable, bitmap; + AndroidBitmapInfo bitmap_info; + size_t byte_size; + void *data; + struct android_image *image; + unsigned char *data1, *data2; + int i, x; + + /* N.B. that supporting windows requires some more work to make + EmacsDrawable.getBitmap thread safe. */ + drawable = android_resolve_handle2 (handle, ANDROID_HANDLE_WINDOW, + ANDROID_HANDLE_PIXMAP); + + /* Look up the drawable and get the bitmap corresponding to it. + Then, lock the bitmap's bits. */ + bitmap = (*android_java_env)->CallObjectMethod (android_java_env, + drawable, + drawable_class.get_bitmap); + if (!bitmap) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + memset (&bitmap_info, 0, sizeof bitmap_info); + + /* The NDK doc seems to imply this function can fail but doesn't say + what value it gives when it does! */ + AndroidBitmap_getInfo (android_java_env, bitmap, &bitmap_info); + + if (!bitmap_info.stride) + { + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + + /* Compute how big the image data will be. Fail if it would be too + big. */ + + if (bitmap_info.format != ANDROID_BITMAP_FORMAT_A_8) + { + if (INT_MULTIPLY_WRAPV ((size_t) bitmap_info.stride, + (size_t) bitmap_info.height, + &byte_size)) + { + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + } + else + /* This A8 image will be packed into A1 later on. */ + byte_size = (bitmap_info.width + 7) / 8; + + /* Lock the image data. Once again, the NDK documentation says the + call can fail, but does not say how to determine whether or not + it has failed, nor how the address is aligned. */ + data = NULL; + AndroidBitmap_lockPixels (android_java_env, bitmap, &data); + + if (!data) + { + /* Take a NULL pointer to mean that AndroidBitmap_lockPixels + failed. */ + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + + /* Copy the data into a new struct android_image. */ + image = xmalloc (sizeof *image); + image->width = bitmap_info.width; + image->height = bitmap_info.height; + image->data = malloc (byte_size); + + if (!image->data) + { + ANDROID_DELETE_LOCAL_REF (bitmap); + xfree (image); + memory_full (byte_size); + } + + /* Use the format of the bitmap to determine the image depth. */ + switch (bitmap_info.format) + { + case ANDROID_BITMAP_FORMAT_RGBA_8888: + image->depth = 24; + image->bits_per_pixel = 32; + break; + + /* A8 images are used by Emacs to represent bitmaps. They have + to be packed manually. */ + case ANDROID_BITMAP_FORMAT_A_8: + image->depth = 1; + image->bits_per_pixel = 1; + break; + + /* Other formats are currently not supported. */ + default: + emacs_abort (); + } + + image->format = format; + + if (image->depth == 24) + { + image->bytes_per_line = bitmap_info.stride; + + /* Copy the bitmap data over. */ + memcpy (image->data, data, byte_size); + } + else + { + /* Pack the A8 image data into bits manually. */ + image->bytes_per_line = (image->width + 7) / 8; + + data1 = (unsigned char *) image->data; + data2 = data; + + for (i = 0; i < image->height; ++i) + { + for (x = 0; x < image->width; ++x) + /* Some bits in data1 might be initialized at this point, + but they will all be set properly later. */ + data1[x / 8] = (data2[x] + ? (data1[x / 8] | (1 << (x % 8))) + : (data1[x / 8] & ~(1 << (x % 8)))); + + data1 += image->bytes_per_line; + data2 += bitmap_info.stride; + } + } + + /* Unlock the bitmap pixels. */ + AndroidBitmap_unlockPixels (android_java_env, bitmap); + + /* Delete the bitmap reference. */ + ANDROID_DELETE_LOCAL_REF (bitmap); + return image; +} + +void +android_put_image (android_pixmap handle, struct android_image *image) +{ + jobject drawable, bitmap; + AndroidBitmapInfo bitmap_info; + void *data; + unsigned char *data_1, *data_2; + int i, x; + + drawable = android_resolve_handle (handle, ANDROID_HANDLE_PIXMAP); + + /* Look up the drawable and get the bitmap corresponding to it. + Then, lock the bitmap's bits. */ + bitmap = (*android_java_env)->CallObjectMethod (android_java_env, + drawable, + drawable_class.get_bitmap); + if (!bitmap) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + memset (&bitmap_info, 0, sizeof bitmap_info); + + /* The NDK doc seems to imply this function can fail but doesn't say + what value it gives when it does! */ + AndroidBitmap_getInfo (android_java_env, bitmap, &bitmap_info); + + if (!bitmap_info.stride) + { + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + + if (bitmap_info.width != image->width + || bitmap_info.height != image->height) + /* This is not yet supported. */ + emacs_abort (); + + /* Make sure the bitmap formats are compatible with each other. */ + + if ((image->depth == 24 + && bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) + || (image->depth == 1 + && bitmap_info.format != ANDROID_BITMAP_FORMAT_A_8)) + emacs_abort (); + + /* Lock the image data. Once again, the NDK documentation says the + call can fail, but does not say how to determine whether or not + it has failed, nor how the address is aligned. */ + data = NULL; + AndroidBitmap_lockPixels (android_java_env, bitmap, &data); + + if (!data) + { + /* Take a NULL pointer to mean that AndroidBitmap_lockPixels + failed. */ + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + + data_1 = data; + data_2 = (unsigned char *) image->data; + + /* Copy the bitmap data over scanline-by-scanline. */ + for (i = 0; i < image->height; ++i) + { + if (image->depth != 1) + memcpy (data_1, data_2, + image->width * (image->bits_per_pixel / 8)); + else + { + /* Android internally uses a 1 byte-per-pixel format for + ALPHA_8 images. Expand the image from the 1 + bit-per-pixel X format correctly. */ + + for (x = 0; x < image->width; ++x) + data_1[x] = (data_2[x / 8] & (1 << x % 8)) ? 0xff : 0; + } + + data_1 += bitmap_info.stride; + data_2 += image->bytes_per_line; + } + + /* Unlock the bitmap pixels. */ + AndroidBitmap_unlockPixels (android_java_env, bitmap); + + /* Delete the bitmap reference. */ + ANDROID_DELETE_LOCAL_REF (bitmap); +} + + + +#undef faccessat + +/* Replace the system faccessat with one which understands AT_EACCESS. + Android's faccessat simply fails upon using AT_EACCESS, so repalce + it with zero here. This isn't caught during configuration. */ + +int +faccessat (int dirfd, const char *pathname, int mode, int flags) +{ + static int (*real_faccessat) (int, const char *, int, int); + + if (!real_faccessat) + real_faccessat = dlsym (RTLD_NEXT, "faccessat"); + + return real_faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); +} + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ @@ -2332,4 +2831,44 @@ android_free_gc (struct android_gc *gc) emacs_abort (); } +struct android_image * +android_create_image (unsigned int depth, enum android_image_format format, + char *data, unsigned int width, unsigned int height) +{ + emacs_abort (); +} + +void +android_destroy_image (struct android_image *ximg) +{ + emacs_abort (); +} + +void +android_put_pixel (struct android_image *ximg, int x, int y, + unsigned long pixel) +{ + emacs_abort (); +} + +unsigned long +android_get_pixel (struct android_image *ximg, int x, int y) +{ + emacs_abort (); +} + +struct android_image * +android_get_image (android_drawable drawable, + enum android_image_format format) +{ + emacs_abort (); +} + +void +android_put_image (android_pixmap pixmap, + struct android_image *image) +{ + emacs_abort (); +} + #endif diff --git a/src/android.h b/src/android.h index 6bdcd38ed68..4d702fe2079 100644 --- a/src/android.h +++ b/src/android.h @@ -28,6 +28,8 @@ along with GNU Emacs. If not, see . */ #include #include #include + +#include "androidgui.h" #endif /* This must be used in every symbol declaration to export it to the @@ -49,6 +51,19 @@ extern int android_fstat (int, struct stat *); extern int android_fstatat (int, const char *restrict, struct stat *restrict, int); extern int android_close (int); +extern const char *android_get_home_directory (void); + +extern double android_pixel_density_x, android_pixel_density_y; + +enum android_handle_type + { + ANDROID_HANDLE_WINDOW, + ANDROID_HANDLE_GCONTEXT, + ANDROID_HANDLE_PIXMAP, + }; + +extern jobject android_resolve_handle (android_handle, + enum android_handle_type); #endif diff --git a/src/androidfns.c b/src/androidfns.c index e9e1ae3d52e..a78b35fc8ff 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -531,9 +531,10 @@ android_default_font_parameter (struct frame *f, Lisp_Object parms) if (! FONTP (font) && ! STRINGP (font)) { const char *names[] = { - /* This will find the normal font. */ - "DroidSansMono", + /* This will find the normal font. The default font size on + Android is 8. */ "monospace", + "DroidSansMono", NULL }; int i; diff --git a/src/androidfont.c b/src/androidfont.c index e312e55c54a..b2c83201b06 100644 --- a/src/androidfont.c +++ b/src/androidfont.c @@ -49,6 +49,7 @@ struct android_emacs_font_driver jmethodID has_char; jmethodID text_extents; jmethodID encode_char; + jmethodID draw; /* Static methods. */ jmethodID create_font_driver; @@ -67,6 +68,7 @@ struct android_emacs_font_spec jfieldID size; jfieldID spacing; jfieldID avgwidth; + jfieldID dpi; }; struct android_emacs_font_metrics @@ -113,6 +115,9 @@ struct androidfont_info /* The Java-side font. */ jobject object; + + /* Cached glyph metrics arranged in a two dimensional array. */ + struct font_metrics **metrics; }; struct androidfont_entity @@ -197,9 +202,11 @@ android_init_font_driver (void) FIND_METHOD (has_char, "hasChar", "(Lorg/gnu/emacs/EmacsFontDriver$Font" "Spec;C)I"); FIND_METHOD (text_extents, "textExtents", "(Lorg/gnu/emacs/EmacsFontDriver" - "$FontObject;[I[Lorg/gnu/emacs/EmacsFontDriver$FontMetrics;)V"); + "$FontObject;[ILorg/gnu/emacs/EmacsFontDriver$FontMetrics;)V"); FIND_METHOD (encode_char, "encodeChar", "(Lorg/gnu/emacs/EmacsFontDriver" "$FontObject;C)I"); + FIND_METHOD (draw, "draw", "(Lorg/gnu/emacs/EmacsFontDriver$FontObject;" + "Lorg/gnu/emacs/EmacsGC;Lorg/gnu/emacs/EmacsDrawable;[IIIIZ)I"); font_driver_class.create_font_driver = (*android_java_env)->GetStaticMethodID (android_java_env, @@ -252,6 +259,7 @@ android_init_font_spec (void) FIND_FIELD (size, "size", "Ljava/lang/Integer;"); FIND_FIELD (spacing, "spacing", "Ljava/lang/Integer;"); FIND_FIELD (avgwidth, "avgwidth", "Ljava/lang/Integer;"); + FIND_FIELD (dpi, "dpi", "Ljava/lang/Integer;"); #undef FIND_FIELD } @@ -449,6 +457,9 @@ androidfont_from_lisp (Lisp_Object font) DO_CARDINAL_FIELD (avgwidth, (FIXNUMP (AREF (font, FONT_AVGWIDTH_INDEX)) ? XFIXNUM (AREF (font, FONT_AVGWIDTH_INDEX)) : -1)); + DO_CARDINAL_FIELD (dpi, (FIXNUMP (AREF (font, FONT_DPI_INDEX)) + ? XFIXNUM (AREF (font, FONT_DPI_INDEX)) + : -1)); #undef DO_CARDINAL_FIELD @@ -507,6 +518,8 @@ androidfont_from_java (jobject spec, Lisp_Object entity) DO_CARDINAL_FIELD (size, FONT_SIZE_INDEX, false); DO_CARDINAL_FIELD (spacing, FONT_SPACING_INDEX, false); DO_CARDINAL_FIELD (avgwidth, FONT_AVGWIDTH_INDEX, false); + DO_CARDINAL_FIELD (dpi, FONT_DPI_INDEX, false); + #undef DO_CARDINAL_FIELD } @@ -613,49 +626,98 @@ static int androidfont_draw (struct glyph_string *s, int from, int to, int x, int y, bool with_background) { - return 0; + struct androidfont_info *info; + jarray chars; + int rc; + jobject gcontext, drawable; + + verify (sizeof (unsigned int) == sizeof (jint)); + info = (struct androidfont_info *) s->font; + + gcontext = android_resolve_handle (s->gc->gcontext, + ANDROID_HANDLE_GCONTEXT); + drawable = android_resolve_handle (FRAME_ANDROID_WINDOW (s->f), + ANDROID_HANDLE_WINDOW); + chars = (*android_java_env)->NewIntArray (android_java_env, + to - from); + + if (!chars) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + (*android_java_env)->SetIntArrayRegion (android_java_env, chars, + 0, to - from, + (jint *) s->char2b + from); + + info = (struct androidfont_info *) s->font; + prepare_face_for_display (s->f, s->face); + + rc = (*android_java_env)->CallIntMethod (android_java_env, + font_driver, + font_driver_class.draw, + info->object, + gcontext, drawable, + chars, (jint) x, (jint) y, + (jint) s->width, + (jboolean) with_background); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (chars); + + return rc; } static Lisp_Object -androidfont_open_font (struct frame *f, Lisp_Object font_entity, int x) +androidfont_open_font (struct frame *f, Lisp_Object font_entity, + int pixel_size) { struct androidfont_info *font_info; struct androidfont_entity *entity; struct font *font; - Lisp_Object font_object, tem; + Lisp_Object font_object; jobject old; jint value; - if (x <= 0) + if (XFIXNUM (AREF (font_entity, FONT_SIZE_INDEX)) != 0) + pixel_size = XFIXNUM (AREF (font_entity, FONT_SIZE_INDEX)); + else if (pixel_size == 0) { - /* Get pixel size from frame instead. */ - tem = get_frame_param (f, Qfontsize); - x = NILP (tem) ? 0 : XFIXNAT (tem); + /* This bit was copied from xfont.c. The values might need + adjustment. */ + + if (FRAME_FONT (f)) + pixel_size = FRAME_FONT (f)->pixel_size; + else + pixel_size = 12; } __android_log_print (ANDROID_LOG_DEBUG, __func__, "opening font entity %"pI"x:%d", - (EMACS_INT) font_entity, x); + (EMACS_INT) font_entity, pixel_size); entity = (struct androidfont_entity *) XFONT_ENTITY (font_entity); block_input (); font_object = font_make_object (VECSIZE (struct androidfont_info), - font_entity, x); + font_entity, pixel_size); ASET (font_object, FONT_TYPE_INDEX, Qandroid); font_info = (struct androidfont_info *) XFONT_OBJECT (font_object); font = &font_info->font; font->driver = &androidfont_driver; - /* Clear font_info->object early in case GC happens later on! */ + /* Clear font_info->object and font_info->metrics early in case GC + happens later on! */ font_info->object = NULL; + font_info->metrics = NULL; unblock_input (); font_info->object = (*android_java_env)->CallObjectMethod (android_java_env, font_driver, font_driver_class.open_font, - entity->object, (jint) x); + entity->object, + (jint) pixel_size); if (!font_info->object) { (*android_java_env)->ExceptionClear (android_java_env); @@ -710,9 +772,19 @@ static void androidfont_close_font (struct font *font) { struct androidfont_info *info; + int i; info = (struct androidfont_info *) font; + /* Free the font metrics cache if it exists. */ + + if (info->metrics) + { + for (i = 0; i < 256; ++i) + xfree (info->metrics[i]); + xfree (info->metrics); + } + /* If info->object is NULL, then FONT was unsuccessfully created, and there is no global reference that has to be deleted. */ @@ -762,18 +834,65 @@ androidfont_encode_char (struct font *font, int c) info->object, (jchar) c); } +static void +androidfont_cache_text_extents (struct androidfont_info *info, + unsigned int glyph, + struct font_metrics *metrics) +{ + int i; + + /* Glyphs larger than 65535 can't be cached. */ + if (glyph >= 256 * 256) + return; + + if (!info->metrics) + info->metrics = xzalloc (256 * sizeof *info->metrics); + + if (!info->metrics[glyph / 256]) + { + info->metrics[glyph / 256] + = xnmalloc (256, sizeof **info->metrics); + + /* Now, all the metrics in that array as invalid by setting + lbearing to SHRT_MAX. */ + for (i = 0; i < 256; ++i) + info->metrics[glyph / 256][i].lbearing = SHRT_MAX; + } + + /* Finally, cache the glyph. */ + info->metrics[glyph / 256][glyph % 256] = *metrics; +} + +static bool +androidfont_check_cached_extents (struct androidfont_info *info, + unsigned int glyph, + struct font_metrics *metrics) +{ + if (info->metrics && info->metrics[glyph / 256] + && info->metrics[glyph / 256][glyph % 256].lbearing != SHRT_MAX) + { + *metrics = info->metrics[glyph / 256][glyph % 256]; + return true; + } + + return false; +} + static void androidfont_text_extents (struct font *font, const unsigned int *code, int nglyphs, struct font_metrics *metrics) { struct androidfont_info *info; - jarray codepoint_array, metrics_array; + jarray codepoint_array; jobject metrics_object; - int i; short value; info = (struct androidfont_info *) font; + if (nglyphs == 1 + && androidfont_check_cached_extents (info, *code, metrics)) + return; + /* Allocate the arrays of code points and font metrics. */ codepoint_array = (*android_java_env)->NewIntArray (android_java_env, @@ -784,92 +903,103 @@ androidfont_text_extents (struct font *font, const unsigned int *code, memory_full (0); } - metrics_array - = (*android_java_env)->NewObjectArray (android_java_env, - nglyphs, - font_metrics_class.class, - NULL); - if (!metrics_array) - { - (*android_java_env)->ExceptionClear (android_java_env); - ANDROID_DELETE_LOCAL_REF (metrics_array); - memory_full (0); - } - - if (sizeof (unsigned int) == sizeof (jint)) - /* Always true on every Android device. */ - (*android_java_env)->SetIntArrayRegion (android_java_env, - codepoint_array, - 0, nglyphs, - (jint *) code); - else - emacs_abort (); - - for (i = 0; i < nglyphs; ++i) - { - metrics_object - = (*android_java_env)->AllocObject (android_java_env, - font_metrics_class.class); + verify (sizeof (unsigned int) == sizeof (jint)); - if (!metrics_object) - { - (*android_java_env)->ExceptionClear (android_java_env); - ANDROID_DELETE_LOCAL_REF (metrics_array); - ANDROID_DELETE_LOCAL_REF (codepoint_array); - memory_full (0); - } + /* Always true on every Android device. */ + (*android_java_env)->SetIntArrayRegion (android_java_env, + codepoint_array, + 0, nglyphs, + (jint *) code); - (*android_java_env)->SetObjectArrayElement (android_java_env, - metrics_array, i, - metrics_object); - ANDROID_DELETE_LOCAL_REF (metrics_object); - } + metrics_object + = (*android_java_env)->AllocObject (android_java_env, + font_metrics_class.class); (*android_java_env)->CallVoidMethod (android_java_env, font_driver, font_driver_class.text_extents, info->object, codepoint_array, - metrics_array); + metrics_object); if ((*android_java_env)->ExceptionCheck (android_java_env)) { (*android_java_env)->ExceptionClear (android_java_env); - ANDROID_DELETE_LOCAL_REF (metrics_array); + ANDROID_DELETE_LOCAL_REF (metrics_object); ANDROID_DELETE_LOCAL_REF (codepoint_array); memory_full (0); } - for (i = 0; i < nglyphs; ++i) - { - metrics_object - = (*android_java_env)->GetObjectArrayElement (android_java_env, - metrics_array, i); #define DO_CARDINAL_FIELD(field) \ value \ = (*android_java_env)->GetShortField (android_java_env, \ metrics_object, \ font_metrics_class.field); \ - metrics[i].field = value; + metrics->field = value; - DO_CARDINAL_FIELD (lbearing); - DO_CARDINAL_FIELD (rbearing); - DO_CARDINAL_FIELD (width); - DO_CARDINAL_FIELD (ascent); - DO_CARDINAL_FIELD (descent); + DO_CARDINAL_FIELD (lbearing); + DO_CARDINAL_FIELD (rbearing); + DO_CARDINAL_FIELD (width); + DO_CARDINAL_FIELD (ascent); + DO_CARDINAL_FIELD (descent); #undef DO_CARDINAL_FIELD - ANDROID_DELETE_LOCAL_REF (metrics_object); - } - - ANDROID_DELETE_LOCAL_REF (metrics_array); + ANDROID_DELETE_LOCAL_REF (metrics_object); ANDROID_DELETE_LOCAL_REF (codepoint_array); + + /* Emacs spends a lot of time in androidfont_text_extents, which + makes calling JNI too slow. Cache the metrics for this single + glyph. */ + + if (nglyphs == 1) + androidfont_cache_text_extents (info, *code, metrics); } static Lisp_Object androidfont_list_family (struct frame *f) { - return Qnil; + Lisp_Object families; + jarray family_array; + jobject string; + jsize i, length; + const char *family; + + family_array + = (*android_java_env)->CallObjectMethod (android_java_env, + font_driver, + font_driver_class.list_families); + if (!family_array) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + length = (*android_java_env)->GetArrayLength (android_java_env, + family_array); + families = Qnil; + + for (i = 0; i < length; ++i) + { + string = (*android_java_env)->GetObjectArrayElement (android_java_env, + family_array, i); + family = (*android_java_env)->GetStringUTFChars (android_java_env, + (jstring) string, NULL); + + if (!family) + { + ANDROID_DELETE_LOCAL_REF (string); + ANDROID_DELETE_LOCAL_REF (family_array); + } + + families = Fcons (build_string_from_utf8 (string), families); + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + (jstring) string, + family); + ANDROID_DELETE_LOCAL_REF (string); + } + + ANDROID_DELETE_LOCAL_REF (family_array); + return Fnreverse (families); } struct font_driver androidfont_driver = diff --git a/src/androidgui.h b/src/androidgui.h index 43ccc86e5c7..b0ea820cfdf 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -201,6 +201,9 @@ enum android_event_type ANDROID_KEY_PRESS, ANDROID_KEY_RELEASE, ANDROID_CONFIGURE_NOTIFY, + ANDROID_FOCUS_IN, + ANDROID_FOCUS_OUT, + ANDROID_WINDOW_ACTION, }; struct android_any_event @@ -209,6 +212,13 @@ struct android_any_event android_window window; }; +enum android_modifier_mask + { + ANDROID_SHIFT_MASK = 193, + ANDROID_CONTROL_MASK = 4096, + ANDROID_ALT_MASK = 2, + }; + struct android_key_event { enum android_event_type type; @@ -216,8 +226,18 @@ struct android_key_event android_time time; unsigned int state; unsigned int keycode; + unsigned int unicode_char; }; +/* These hard coded values are Android modifier keycodes derived + through experimentation. */ + +#define ANDROID_IS_MODIFIER_KEY(key) \ + ((key) == 57 || (key) == 58 || (key) == 113 || (key) == 114 \ + || (key) == 119 || (key) == 117 || (key) == 118 || (key) == 78 \ + || (key) == 94 || (key) == 59 || (key) == 60 || (key) == 95 \ + || (key) == 63) + struct android_configure_event { enum android_event_type type; @@ -227,12 +247,35 @@ struct android_configure_event int width, height; }; +struct android_focus_event +{ + enum android_event_type type; + android_window window; + android_time time; +}; + +struct android_window_action_event +{ + enum android_event_type type; + + /* The window handle. This can be ANDROID_NONE. */ + android_window window; + + /* Numerical identifier for this action. If 0 and WINDOW is set, + then it means the frame associated with that window has been + destroyed. Otherwise, it means Emacs should create a new + frame. */ + unsigned int action; +}; + union android_event { enum android_event_type type; struct android_any_event xany; struct android_key_event xkey; struct android_configure_event xconfigure; + struct android_focus_event xfocus; + struct android_window_action_event xaction; }; extern int android_pending (void); @@ -303,9 +346,47 @@ extern android_pixmap android_create_pixmap (unsigned int, unsigned int, extern void android_set_ts_origin (struct android_gc *, int, int); extern void android_clear_area (android_window, int, int, unsigned int, unsigned int); +extern android_pixmap android_create_bitmap_from_data (char *, unsigned int, + unsigned int); #endif + + +/* Image support. Keep the API as similar to XImage as possible. To + avoid leaving a huge mess of "#ifndef ANDROID_STUBIFY" in image.c, + stubs should be defined for all functions. */ + +enum android_image_format + { + ANDROID_Z_PIXMAP, + }; + +struct android_image +{ + int width, height; + enum android_image_format format; + char *data; + int depth; + int bytes_per_line; + int bits_per_pixel; +}; + +extern struct android_image *android_create_image (unsigned int, + enum android_image_format, + char *, unsigned int, + unsigned int); +extern void android_destroy_image (struct android_image *); + +extern void android_put_pixel (struct android_image *, int, int, + unsigned long); +extern unsigned long android_get_pixel (struct android_image *, int, int); +extern struct android_image *android_get_image (android_drawable, + enum android_image_format); +extern void android_put_image (android_pixmap, struct android_image *); + + + /* X emulation stuff also needed while building stubs. */ extern struct android_gc *android_create_gc (enum android_gc_value_mask, diff --git a/src/androidterm.c b/src/androidterm.c index bb3c2a29737..0279da4f58d 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -39,6 +39,8 @@ struct android_display_info *x_display_list; #ifndef ANDROID_STUBIFY +#include + enum { ANDROID_EVENT_NORMAL, @@ -141,6 +143,124 @@ android_flush_dirty_back_buffer_on (struct frame *f) show_back_buffer (f); } +/* Convert between the modifier bits Android uses and the modifier + bits Emacs uses. */ + +static int +android_android_to_emacs_modifiers (struct android_display_info *dpyinfo, + int state) +{ + return ((state & ANDROID_CONTROL_MASK) ? ctrl_modifier : 0 + | (state & ANDROID_SHIFT_MASK) ? shift_modifier : 0 + | (state & ANDROID_ALT_MASK) ? meta_modifier : 0); +} + +static int +android_emacs_to_android_modifiers (struct android_display_info *dpyinfo, + intmax_t state) +{ + return ((state & ctrl_modifier) ? ANDROID_CONTROL_MASK : 0 + | (state & shift_modifier) ? ANDROID_SHIFT_MASK : 0 + | (state & meta_modifier) ? ANDROID_ALT_MASK : 0); +} + +static void android_frame_rehighlight (struct android_display_info *); + +static void +android_lower_frame (struct frame *f) +{ + /* TODO. */ +} + +static void +android_raise_frame (struct frame *f) +{ + /* TODO. */ +} + +static void +android_new_focus_frame (struct android_display_info *dpyinfo, + struct frame *frame) +{ + struct frame *old_focus; + + old_focus = dpyinfo->focus_frame; + + if (frame != dpyinfo->focus_frame) + { + /* Set this before calling other routines, so that they see + the correct value of x_focus_frame. */ + dpyinfo->focus_frame = frame; + + if (old_focus && old_focus->auto_lower) + android_lower_frame (old_focus); + + if (dpyinfo->focus_frame && dpyinfo->focus_frame->auto_raise) + dpyinfo->pending_autoraise_frame = dpyinfo->focus_frame; + else + dpyinfo->pending_autoraise_frame = NULL; + } + + android_frame_rehighlight (dpyinfo); +} + +static void +android_focus_changed (int type, int state, + struct android_display_info *dpyinfo, + struct frame *frame, struct input_event *bufp) +{ + if (type == ANDROID_FOCUS_IN) + { + if (dpyinfo->x_focus_event_frame != frame) + { + android_new_focus_frame (dpyinfo, frame); + dpyinfo->x_focus_event_frame = frame; + bufp->kind = FOCUS_IN_EVENT; + XSETFRAME (bufp->frame_or_window, frame); + } + + frame->output_data.android->focus_state |= state; + } + else if (type == ANDROID_FOCUS_OUT) + { + frame->output_data.android->focus_state &= ~state; + + if (dpyinfo->x_focus_event_frame == frame) + { + dpyinfo->x_focus_event_frame = 0; + android_new_focus_frame (dpyinfo, 0); + + bufp->kind = FOCUS_OUT_EVENT; + XSETFRAME (bufp->frame_or_window, frame); + } + + if (frame->pointer_invisible) + android_toggle_invisible_pointer (frame, false); + } +} + +static void +android_detect_focus_change (struct android_display_info *dpyinfo, + struct frame *frame, + union android_event *event, + struct input_event *bufp) +{ + if (!frame) + return; + + switch (event->type) + { + case ANDROID_FOCUS_IN: + case ANDROID_FOCUS_OUT: + android_focus_changed (event->type, FOCUS_EXPLICIT, + dpyinfo, frame, bufp); + break; + + default: + break; + } +} + static int handle_one_android_event (struct android_display_info *dpyinfo, union android_event *event, int *finish, @@ -149,11 +269,20 @@ handle_one_android_event (struct android_display_info *dpyinfo, union android_event configureEvent; struct frame *f, *any, *mouse_frame; Mouse_HLInfo *hlinfo; + union buffered_input_event inev; + int modifiers, count; + + /* It is okay for this to not resemble handle_one_xevent so much. + Differences in event handling code are much less nasty than + stuble differences in the graphics code. */ + count = 0; hlinfo = &dpyinfo->mouse_highlight; *finish = ANDROID_EVENT_NORMAL; any = android_window_to_frame (dpyinfo, event->xany.window); + EVENT_INIT (inev.ie); + switch (event->type) { case ANDROID_CONFIGURE_NOTIFY: @@ -192,12 +321,15 @@ handle_one_android_event (struct android_display_info *dpyinfo, case ANDROID_KEY_PRESS: + /* Set f to any. There are no ``outer windows'' on Android. */ + f = any; + /* If mouse-highlight is an integer, input clears out mouse highlighting. */ if (!hlinfo->mouse_face_hidden && FIXNUMP (Vmouse_highlight) && (any == 0 - || !EQ (f->tool_bar_window, hlinfo->mouse_face_window) - || !EQ (f->tab_bar_window, hlinfo->mouse_face_window))) + || !EQ (any->tool_bar_window, hlinfo->mouse_face_window) + || !EQ (any->tab_bar_window, hlinfo->mouse_face_window))) { mouse_frame = hlinfo->mouse_face_mouse_frame; @@ -208,13 +340,90 @@ handle_one_android_event (struct android_display_info *dpyinfo, android_flush_dirty_back_buffer_on (mouse_frame); } + event->xkey.state + |= android_emacs_to_android_modifiers (dpyinfo, + extra_keyboard_modifiers); + modifiers = event->xkey.state; + + /* Common for all keysym input events. */ + XSETFRAME (inev.ie.frame_or_window, any); + inev.ie.modifiers + = android_android_to_emacs_modifiers (dpyinfo, modifiers); + inev.ie.timestamp = event->xkey.time; + + /* First deal with keysyms which have defined translations to + characters. */ + + if (event->xkey.unicode_char >= 32 + && event->xkey.unicode_char < 128) + { + inev.ie.kind = ASCII_KEYSTROKE_EVENT; + inev.ie.code = event->xkey.unicode_char; + } + else if (event->xkey.unicode_char < 32) + { + /* If the key is a modifier key, just return. */ + if (ANDROID_IS_MODIFIER_KEY (event->xkey.keycode)) + goto done_keysym; + + /* Next, deal with special ``characters'' by giving the + keycode to keyboard.c. */ + inev.ie.kind = NON_ASCII_KEYSTROKE_EVENT; + inev.ie.code = event->xkey.keycode; + } + else + { + /* Finally, deal with Unicode characters. */ + inev.ie.kind = MULTIBYTE_CHAR_KEYSTROKE_EVENT; + inev.ie.code = event->xkey.unicode_char; + } + + goto done_keysym; + + done_keysym: + goto OTHER; + + case ANDROID_FOCUS_IN: + case ANDROID_FOCUS_OUT: + android_detect_focus_change (dpyinfo, any, event, &inev.ie); + goto OTHER; + + case ANDROID_WINDOW_ACTION: + + f = any; + + if (event->xaction.action == 0) + { + /* Action 0 either means to destroy a frame or to create a + new frame, depending on whether or not + event->xaction.window exists. */ + + if (event->xaction.window) + { + if (!f) + goto OTHER; + + inev.ie.kind = DELETE_WINDOW_EVENT; + XSETFRAME (inev.ie.frame_or_window, f); + } + else + /* A new frame must be created. */; + } + + goto OTHER; + default: goto OTHER; } OTHER: + if (inev.ie.kind != NO_EVENT) + { + kbd_buffer_store_buffered_event (&inev, hold_quit); + count++; + } - return 0; + return count; } static int @@ -378,15 +587,52 @@ android_focus_frame (struct frame *f, bool noactivate) } static void -android_frame_rehighlight (struct frame *f) +android_frame_rehighlight (struct android_display_info *dpyinfo) { - /* TODO */ + struct frame *old_highlight; + + old_highlight = dpyinfo->highlight_frame; + + if (dpyinfo->focus_frame) + { + dpyinfo->highlight_frame + = ((FRAMEP (FRAME_FOCUS_FRAME (dpyinfo->focus_frame))) + ? XFRAME (FRAME_FOCUS_FRAME (dpyinfo->focus_frame)) + : dpyinfo->focus_frame); + if (!FRAME_LIVE_P (dpyinfo->highlight_frame)) + { + fset_focus_frame (dpyinfo->focus_frame, Qnil); + dpyinfo->highlight_frame = dpyinfo->focus_frame; + } + } + else + dpyinfo->highlight_frame = 0; + + if (dpyinfo->highlight_frame != old_highlight) + { + /* This is not yet required on Android. */ +#if 0 + if (old_highlight) + android_frame_unhighlight (old_highlight); + if (dpyinfo->highlight_frame) + android_frame_highlight (dpyinfo->highlight_frame); +#endif + } +} + +static void +android_frame_rehighlight_hook (struct frame *f) +{ + android_frame_rehighlight (FRAME_DISPLAY_INFO (f)); } static void android_frame_raise_lower (struct frame *f, bool raise_flag) { - /* TODO */ + if (raise_flag) + android_raise_frame (f); + else + android_lower_frame (f); } void @@ -401,6 +647,10 @@ android_make_frame_visible (struct frame *f) void android_make_frame_invisible (struct frame *f) { + /* Don't keep the highlight on an invisible frame. */ + if (FRAME_DISPLAY_INFO (f)->highlight_frame == f) + FRAME_DISPLAY_INFO (f)->highlight_frame = 0; + android_unmap_window (FRAME_ANDROID_WINDOW (f)); SET_FRAME_VISIBLE (f, false); @@ -584,6 +834,8 @@ android_free_frame_resources (struct frame *f) if (f == dpyinfo->focus_frame) dpyinfo->focus_frame = 0; + if (f == dpyinfo->x_focus_event_frame) + dpyinfo->x_focus_event_frame = 0; if (f == dpyinfo->highlight_frame) dpyinfo->highlight_frame = 0; if (f == hlinfo->mouse_face_mouse_frame) @@ -783,7 +1035,7 @@ android_draw_fringe_bitmap (struct window *w, struct glyph_row *row, if (p->overlay_p) { clipmask = android_create_pixmap_from_bitmap_data (bits, p->wd, p->h, - 1, 0, 0); + 1, 0, 1); gcv.clip_mask = clipmask; gcv.clip_x_origin = p->x; @@ -1606,158 +1858,47 @@ android_draw_image_foreground (struct glyph_string *s) if (s->img->pixmap) { - if (s->img->mask) - { - unsigned long mask = (ANDROID_GC_CLIP_MASK - | ANDROID_GC_CLIP_X_ORIGIN - | ANDROID_GC_CLIP_Y_ORIGIN - | ANDROID_GC_FUNCTION); - struct android_gc_values xgcv; - struct android_rectangle clip_rect, image_rect, r; - - xgcv.clip_mask = s->img->mask; - xgcv.clip_x_origin = x - s->slice.x; - xgcv.clip_y_origin = y - s->slice.y; - xgcv.function = ANDROID_GC_COPY; - android_change_gc (s->gc, mask, &xgcv); - - get_glyph_string_clip_rect (s, &clip_rect); - image_rect.x = x; - image_rect.y = y; - image_rect.width = s->slice.width; - image_rect.height = s->slice.height; - - if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) - android_copy_area (s->img->pixmap, FRAME_ANDROID_WINDOW (s->f), - s->gc, s->slice.x + r.x - x, - s->slice.y + r.y - y, - r.x, r.y, r.width, r.height); - } - else + unsigned long mask = (ANDROID_GC_CLIP_MASK + | ANDROID_GC_CLIP_X_ORIGIN + | ANDROID_GC_CLIP_Y_ORIGIN + | ANDROID_GC_FUNCTION); + struct android_gc_values xgcv; + struct android_rectangle clip_rect, image_rect, r; + + xgcv.clip_mask = s->img->mask; + xgcv.clip_x_origin = x - s->slice.x; + xgcv.clip_y_origin = y - s->slice.y; + xgcv.function = ANDROID_GC_COPY; + android_change_gc (s->gc, mask, &xgcv); + + get_glyph_string_clip_rect (s, &clip_rect); + image_rect.x = x; + image_rect.y = y; + image_rect.width = s->slice.width; + image_rect.height = s->slice.height; + + if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) + android_copy_area (s->img->pixmap, + FRAME_ANDROID_WINDOW (s->f), + s->gc, s->slice.x + r.x - x, + s->slice.y + r.y - y, + r.width, r.height, r.x, r.y); + + /* When the image has a mask, we can expect that at least part + of a mouse highlight or a block cursor will be visible. If + the image doesn't have a mask, make a block cursor visible by + drawing a rectangle around the image. I believe it's looking + better if we do nothing here for mouse-face. */ + if (s->hl == DRAW_CURSOR && !s->img->mask) { - unsigned long mask = (ANDROID_GC_CLIP_MASK - | ANDROID_GC_CLIP_X_ORIGIN - | ANDROID_GC_CLIP_Y_ORIGIN - | ANDROID_GC_FUNCTION); - struct android_gc_values xgcv; - struct android_rectangle clip_rect, image_rect, r; - - xgcv.clip_mask = s->img->mask; - xgcv.clip_x_origin = x - s->slice.x; - xgcv.clip_y_origin = y - s->slice.y; - xgcv.function = ANDROID_GC_COPY; - android_change_gc (s->gc, mask, &xgcv); - - get_glyph_string_clip_rect (s, &clip_rect); - image_rect.x = x; - image_rect.y = y; - image_rect.width = s->slice.width; - image_rect.height = s->slice.height; - - if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) - android_copy_area (s->img->pixmap, - FRAME_ANDROID_WINDOW (s->f), - s->gc, s->slice.x + r.x - x, - s->slice.y + r.y - y, - r.x, r.y, r.width, r.height); - - /* When the image has a mask, we can expect that at - least part of a mouse highlight or a block cursor will - be visible. If the image doesn't have a mask, make - a block cursor visible by drawing a rectangle around - the image. I believe it's looking better if we do - nothing here for mouse-face. */ - if (s->hl == DRAW_CURSOR) - { - int relief = eabs (s->img->relief); - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, - x - relief, y - relief, - s->slice.width + relief*2 - 1, - s->slice.height + relief*2 - 1); - } + int relief = eabs (s->img->relief); + android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + x - relief, y - relief, + s->slice.width + relief*2 - 1, + s->slice.height + relief*2 - 1); } - } - else - /* Draw a rectangle if image could not be loaded. */ - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, x, y, - s->slice.width - 1, s->slice.height - 1); -} - -/* Draw the foreground of image glyph string S to PIXMAP. */ - -static void -android_draw_image_foreground_1 (struct glyph_string *s, - android_pixmap pixmap) -{ - int x = 0; - int y = s->ybase - s->y - image_ascent (s->img, s->face, &s->slice); - - /* If first glyph of S has a left box line, start drawing it to the - right of that line. */ - if (s->face->box != FACE_NO_BOX - && s->first_glyph->left_box_line_p - && s->slice.x == 0) - x += max (s->face->box_vertical_line_width, 0); - - /* If there is a margin around the image, adjust x- and y-position - by that margin. */ - if (s->slice.x == 0) - x += s->img->hmargin; - if (s->slice.y == 0) - y += s->img->vmargin; - if (s->img->pixmap) - { - if (s->img->mask) - { - unsigned long mask = (ANDROID_GC_CLIP_MASK - | ANDROID_GC_CLIP_X_ORIGIN - | ANDROID_GC_CLIP_Y_ORIGIN - | ANDROID_GC_FUNCTION); - struct android_gc_values xgcv; - struct android_rectangle clip_rect, image_rect, r; - - xgcv.clip_mask = s->img->mask; - xgcv.clip_x_origin = x - s->slice.x; - xgcv.clip_y_origin = y - s->slice.y; - xgcv.function = ANDROID_GC_COPY; - android_change_gc (s->gc, mask, &xgcv); - - get_glyph_string_clip_rect (s, &clip_rect); - image_rect.x = x; - image_rect.y = y; - image_rect.width = s->slice.width; - image_rect.height = s->slice.height; - - if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) - android_copy_area (s->img->pixmap, pixmap, s->gc, - s->slice.x + r.x - x, - s->slice.y + r.y - y, - r.x, r.y, r.width, r.height); - - android_set_clip_mask (s->gc, ANDROID_NONE); - } - else - { - android_copy_area (s->img->pixmap, pixmap, s->gc, - s->slice.x, s->slice.y, - s->slice.width, s->slice.height, x, y); - - /* When the image has a mask, we can expect that at - least part of a mouse highlight or a block cursor will - be visible. If the image doesn't have a mask, make - a block cursor visible by drawing a rectangle around - the image. I believe it's looking better if we do - nothing here for mouse-face. */ - if (s->hl == DRAW_CURSOR) - { - int r = eabs (s->img->relief); - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), - s->gc, x - r, y - r, - s->slice.width + r*2 - 1, - s->slice.height + r*2 - 1); - } - } + android_set_clip_mask (s->gc, ANDROID_NONE); } else /* Draw a rectangle if image could not be loaded. */ @@ -1771,7 +1912,6 @@ android_draw_image_glyph_string (struct glyph_string *s) int box_line_hwidth = max (s->face->box_vertical_line_width, 0); int box_line_vwidth = max (s->face->box_horizontal_line_width, 0); int height; - android_pixmap pixmap = ANDROID_NONE; height = s->height; if (s->slice.y == 0) @@ -1793,79 +1933,27 @@ android_draw_image_glyph_string (struct glyph_string *s) if (s->stippled_p) s->row->stipple_p = true; - if (s->img->mask) - { - /* Create a pixmap as large as the glyph string. Fill it - with the background color. Copy the image to it, using - its mask. Copy the temporary pixmap to the display. */ - int depth = FRAME_DISPLAY_INFO (s->f)->n_planes; - - /* Create a pixmap as large as the glyph string. */ - pixmap = android_create_pixmap (s->background_width, - s->height, depth); - - /* Don't clip in the following because we're working on the - pixmap. */ - android_set_clip_mask (s->gc, ANDROID_NONE); - - /* Fill the pixmap with the background color/stipple. */ - if (s->stippled_p) - { - /* Fill background with a stipple pattern. */ - android_set_fill_style (s->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_set_ts_origin (s->gc, - s->x, - s->y); - android_fill_rectangle (pixmap, s->gc, - 0, 0, s->background_width, s->height); - android_set_fill_style (s->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_set_ts_origin (s->gc, 0, 0); - } - else - { - struct android_gc_values xgcv; + int x = s->x; + int y = s->y; + int width = s->background_width; - android_get_gc_values (s->gc, (ANDROID_GC_FOREGROUND - | ANDROID_GC_BACKGROUND), - &xgcv); - android_set_foreground (s->gc, xgcv.background); - android_fill_rectangle (pixmap, s->gc, - 0, 0, s->background_width, s->height); - android_set_background (s->gc, xgcv.foreground); - } - } - else + if (s->first_glyph->left_box_line_p + && s->slice.x == 0) { - int x = s->x; - int y = s->y; - int width = s->background_width; + x += box_line_hwidth; + width -= box_line_hwidth; + } - if (s->first_glyph->left_box_line_p - && s->slice.x == 0) - { - x += box_line_hwidth; - width -= box_line_hwidth; - } + if (s->slice.y == 0) + y += box_line_vwidth; - if (s->slice.y == 0) - y += box_line_vwidth; - - android_draw_glyph_string_bg_rect (s, x, y, width, height); - } + android_draw_glyph_string_bg_rect (s, x, y, width, height); s->background_filled_p = true; } /* Draw the foreground. */ - if (pixmap != ANDROID_NONE) - { - android_draw_image_foreground_1 (s, pixmap); - android_set_glyph_string_clipping (s); - android_copy_area (pixmap, FRAME_ANDROID_WINDOW (s->f), s->gc, - 0, 0, s->background_width, s->height, s->x, - s->y); - android_free_pixmap (pixmap); - } - else - android_draw_image_foreground (s); + android_draw_image_foreground (s); /* If we must draw a relief around the image, do it. */ if (s->img->relief @@ -3047,7 +3135,7 @@ android_create_terminal (struct android_display_info *dpyinfo) terminal->mouse_position_hook = android_mouse_position; terminal->get_focus_frame = android_get_focus_frame; terminal->focus_frame_hook = android_focus_frame; - terminal->frame_rehighlight_hook = android_frame_rehighlight; + terminal->frame_rehighlight_hook = android_frame_rehighlight_hook; terminal->frame_raise_lower_hook = android_frame_raise_lower; terminal->frame_visible_invisible_hook = android_make_frame_visible_invisible; @@ -3117,9 +3205,12 @@ android_term_init (void) dpyinfo->color_map = color_map; - /* TODO! */ - dpyinfo->resx = 96.0; - dpyinfo->resy = 96.0; +#ifndef ANDROID_STUBIFY + + dpyinfo->resx = android_pixel_density_x; + dpyinfo->resy = android_pixel_density_y; + +#endif /* https://lists.gnu.org/r/emacs-devel/2015-11/msg00194.html */ dpyinfo->smallest_font_height = 1; @@ -3130,6 +3221,9 @@ android_term_init (void) /* The display "connection" is now set up, and it must never go away. */ terminal->reference_count = 30000; + + /* Set the baud rate to the same value it gets set to on X. */ + baud_rate = 19200; } diff --git a/src/androidterm.h b/src/androidterm.h index 3a0c9f60555..c834ffb70e5 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -28,8 +28,8 @@ along with GNU Emacs. If not, see . */ struct android_bitmap_record { - /* The image backing the bitmap. */ - Emacs_Pixmap img; + /* The image backing the bitmap and its mask. */ + android_pixmap pixmap, mask; /* The file from which it comes. */ char *file; @@ -37,8 +37,11 @@ struct android_bitmap_record /* The number of references to it. */ int refcount; - /* The height and width. */ - int height, width; + /* The height and width and the depth. */ + int height, width, depth; + + /* Whether or not there is a mask. */ + bool have_mask; }; struct android_display_info @@ -95,6 +98,9 @@ struct android_display_info /* The frame currently with the input focus. */ struct frame *focus_frame; + /* The last frame mentioned in a focus event. */ + struct frame *x_focus_event_frame; + /* The frame which currently has the visual highlight, and should get keyboard input. It points to the focus frame's selected window's frame, but can differ. */ @@ -206,8 +212,24 @@ struct android_output /* The background for which the above relief GCs were set up. They are changed only when a different background is involved. */ unsigned long relief_background; + + /* Focus state. Only present for consistency with X; it is actually + a boolean. */ + int focus_state; }; +enum + { + /* Values for focus_state, used as bit mask. EXPLICIT means we + received a FocusIn for the frame and know it has the focus. + IMPLICIT means we received an EnterNotify and the frame may + have the focus if no window manager is running. FocusOut and + LeaveNotify clears EXPLICIT/IMPLICIT. */ + FOCUS_NONE = 0, + FOCUS_IMPLICIT = 1, + FOCUS_EXPLICIT = 2 + }; + /* Return the Android output data for frame F. */ #define FRAME_ANDROID_OUTPUT(f) ((f)->output_data.android) #define FRAME_OUTPUT_DATA(f) ((f)->output_data.android) diff --git a/src/dispextern.h b/src/dispextern.h index e521dffc37c..c9b3495934b 100644 --- a/src/dispextern.h +++ b/src/dispextern.h @@ -160,8 +160,8 @@ typedef Emacs_Pixmap Emacs_Pix_Context; #ifdef HAVE_ANDROID #include "androidgui.h" typedef struct android_display_info Display_Info; -typedef Emacs_Pixmap Emacs_Pix_Container; -typedef Emacs_Pixmap Emacs_Pix_Context; +typedef struct android_image *Emacs_Pix_Container; +typedef struct android_image *Emacs_Pix_Context; #endif #ifdef HAVE_WINDOW_SYSTEM @@ -3119,6 +3119,13 @@ struct image int original_width, original_height; # endif #endif /* HAVE_X_WINDOWS */ +#ifdef HAVE_ANDROID + /* Android images of the image, corresponding to the above Pixmaps. + Non-NULL means it and its Pixmap counterpart may be out of sync + and the latter is outdated. NULL means the X image has been + synchronized to Pixmap. */ + struct android_image *ximg, *mask_img; +#endif /* HAVE_ANDROID */ #ifdef HAVE_NTGUI XFORM xform; #endif diff --git a/src/emacs.c b/src/emacs.c index 9ef58ca412e..d3c53dd8051 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -1440,9 +1440,8 @@ main (int argc, char **argv) bool only_version = false; sort_args (argc, argv); old_argc = argc, argc = 0; - while (argv[argc] - /* Don't allow going past argv. */ - && argc < old_argc) argc++; + /* Don't allow going past argv. */ + while (argc < old_argc && argv[argc]) argc++; skip_args = 0; if (argmatch (argv, argc, "-version", "--version", 3, NULL, &skip_args)) diff --git a/src/fileio.c b/src/fileio.c index 1d22c51ebaa..0f205283d7e 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -898,6 +898,10 @@ user_homedir (char const *name) p[length] = 0; struct passwd *pw = getpwnam (p); SAFE_FREE (); +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + if (pw && !pw->pw_dir && pw->pw_uid == getuid ()) + return (char *) android_get_home_directory (); +#endif if (!pw || (pw->pw_dir && !IS_ABSOLUTE_FILE_NAME (pw->pw_dir))) return NULL; return pw->pw_dir; @@ -1888,6 +1892,11 @@ get_homedir (void) pw = getpwuid (getuid ()); if (pw) home = pw->pw_dir; + +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + if (!home && pw && pw->pw_uid == getuid ()) + return android_get_home_directory (); +#endif if (!home) return ""; } diff --git a/src/font.h b/src/font.h index c1ab26b87cb..1db94a9f3f8 100644 --- a/src/font.h +++ b/src/font.h @@ -547,8 +547,14 @@ CHECK_FONT_GET_OBJECT (Lisp_Object x) return XFONT_OBJECT (x); } +#ifndef HAVE_ANDROID /* Number of pt per inch (from the TeXbook). */ #define PT_PER_INCH 72.27 +#else +/* Android uses this value instead to compensate for different device + dimensions. */ +#define PT_PER_INCH 160.00 +#endif /* Return a pixel size (integer) corresponding to POINT size (double) on resolution DPI. */ diff --git a/src/fringe.c b/src/fringe.c index 5d7c8dca998..f10bc424239 100644 --- a/src/fringe.c +++ b/src/fringe.c @@ -1422,25 +1422,30 @@ If BITMAP overrides a standard fringe bitmap, the original bitmap is restored. On X, we bit-swap the built-in bitmaps and reduce bitmap from short to char array if width is <= 8 bits. + The Android port tries to follow X as closely as possible, so do + that there too. + On MAC with big-endian CPU, we need to byte-swap each short. On W32 and MAC (little endian), there's no need to do this. */ -#if defined (HAVE_X_WINDOWS) || defined (HAVE_PGTK) -static const unsigned char swap_nibble[16] = { - 0x0, 0x8, 0x4, 0xc, /* 0000 1000 0100 1100 */ - 0x2, 0xa, 0x6, 0xe, /* 0010 1010 0110 1110 */ - 0x1, 0x9, 0x5, 0xd, /* 0001 1001 0101 1101 */ - 0x3, 0xb, 0x7, 0xf}; /* 0011 1011 0111 1111 */ -#endif /* HAVE_X_WINDOWS */ +#if defined (HAVE_X_WINDOWS) || defined (HAVE_PGTK) || defined (HAVE_ANDROID) +static const unsigned char swap_nibble[16] = + { + 0x0, 0x8, 0x4, 0xc, /* 0000 1000 0100 1100 */ + 0x2, 0xa, 0x6, 0xe, /* 0010 1010 0110 1110 */ + 0x1, 0x9, 0x5, 0xd, /* 0001 1001 0101 1101 */ + 0x3, 0xb, 0x7, 0xf, /* 0011 1011 0111 1111 */ + }; +#endif static void init_fringe_bitmap (int which, struct fringe_bitmap *fb, int once_p) { if (once_p || fb->dynamic) { -#if defined (HAVE_X_WINDOWS) +#if defined (HAVE_X_WINDOWS) || defined (HAVE_ANDROID) unsigned short *bits = fb->bits; int j; @@ -1488,7 +1493,7 @@ init_fringe_bitmap (int which, struct fringe_bitmap *fb, int once_p) } } #endif /* not USE_CAIRO */ -#endif /* HAVE_X_WINDOWS */ +#endif /* HAVE_X_WINDOWS || HAVE_ANDROID */ #if !defined(HAVE_X_WINDOWS) && defined (HAVE_PGTK) unsigned short *bits = fb->bits; diff --git a/src/image.c b/src/image.c index 986dd7aada4..08d092e2c6e 100644 --- a/src/image.c +++ b/src/image.c @@ -177,11 +177,14 @@ typedef struct haiku_bitmap_record Bitmap_Record; #ifdef HAVE_ANDROID #include "androidterm.h" + typedef struct android_bitmap_record Bitmap_Record; -/* TODO: implement images on Android. */ -#define GET_PIXEL(ximg, x, y) 0 -#define PUT_PIXEL(ximg, x, y, pixel) ((void) (pixel)) +typedef struct android_image XImage; +typedef android_pixmap Pixmap; + +#define GET_PIXEL(ximg, x, y) android_get_pixel (ximg, x, y) +#define PUT_PIXEL(ximg, x, y, pixel) android_put_pixel (ximg, x, y, pixel) #define NO_PIXMAP 0 #define PIX_MASK_RETAIN 0 @@ -507,6 +510,18 @@ image_create_bitmap_from_data (struct frame *f, char *bits, return -1; #endif /* HAVE_X_WINDOWS */ +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + android_pixmap bitmap; + + bitmap = android_create_bitmap_from_data (bits, width, height); + + if (!bitmap) + return -1; +#else + ((void) dpyinfo); + emacs_abort (); +#endif + #ifdef HAVE_NTGUI Lisp_Object frame UNINIT; /* The value is not used. */ Emacs_Pixmap bitmap; @@ -619,14 +634,14 @@ image_create_bitmap_from_data (struct frame *f, char *bits, dpyinfo->bitmaps[id - 1].width = width; dpyinfo->bitmaps[id - 1].refcount = 1; -#ifdef HAVE_X_WINDOWS +#if defined HAVE_X_WINDOWS && defined HAVE_ANDROID dpyinfo->bitmaps[id - 1].pixmap = bitmap; dpyinfo->bitmaps[id - 1].have_mask = false; dpyinfo->bitmaps[id - 1].depth = 1; #ifdef USE_CAIRO dpyinfo->bitmaps[id - 1].stipple = NULL; #endif /* USE_CAIRO */ -#endif /* HAVE_X_WINDOWS */ +#endif /* HAVE_X_WINDOWS || HAVE_ANDROID */ #ifdef HAVE_NTGUI dpyinfo->bitmaps[id - 1].pixmap = bitmap; @@ -637,7 +652,7 @@ image_create_bitmap_from_data (struct frame *f, char *bits, return id; } -#if defined HAVE_HAIKU || defined HAVE_NS +#if defined HAVE_HAIKU || defined HAVE_NS || defined HAVE_ANDROID static char *slurp_file (int, ptrdiff_t *); static Lisp_Object image_find_image_fd (Lisp_Object, int *); static bool xbm_read_bitmap_data (struct frame *, char *, char *, @@ -861,8 +876,62 @@ image_create_bitmap_from_file (struct frame *f, Lisp_Object file) /* This function should never be called when building stubs. */ emacs_abort (); #else - /* you lose, not yet implemented TODO */ - return 0; + ptrdiff_t id, size; + int fd, width, height, rc; + char *contents, *data; + Lisp_Object found; + android_pixmap bitmap; + + /* Look for an existing bitmap with the same name. */ + for (id = 0; id < dpyinfo->bitmaps_last; ++id) + { + if (dpyinfo->bitmaps[id].refcount + && dpyinfo->bitmaps[id].file + && !strcmp (dpyinfo->bitmaps[id].file, SSDATA (file))) + { + ++dpyinfo->bitmaps[id].refcount; + return id + 1; + } + } + + /* Search bitmap-file-path for the file, if appropriate. */ + if (openp (Vx_bitmap_file_path, file, Qnil, &found, + make_fixnum (R_OK), false, false) + < 0) + return -1; + + if (!STRINGP (image_find_image_fd (file, &fd)) + && !STRINGP (image_find_image_fd (found, &fd))) + return -1; + + contents = slurp_file (fd, &size); + + if (!contents) + return -1; + + rc = xbm_read_bitmap_data (f, contents, contents + size, + &width, &height, &data, 0); + + if (!rc) + { + xfree (contents); + return -1; + } + + xfree (contents); + bitmap = android_create_bitmap_from_data (data, width, height); + xfree (data); + + id = image_allocate_bitmap_record (f); + dpyinfo->bitmaps[id - 1].pixmap = bitmap; + dpyinfo->bitmaps[id - 1].have_mask = false; + dpyinfo->bitmaps[id - 1].refcount = 1; + dpyinfo->bitmaps[id - 1].file = xlispstrdup (file); + dpyinfo->bitmaps[id - 1].depth = 1; + dpyinfo->bitmaps[id - 1].height = height; + dpyinfo->bitmaps[id - 1].width = width; + + return id; #endif #endif } @@ -882,6 +951,13 @@ free_bitmap_record (Display_Info *dpyinfo, Bitmap_Record *bm) #endif /* USE_CAIRO */ #endif /* HAVE_X_WINDOWS */ +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + android_free_pixmap (bm->pixmap); + + if (bm->have_mask) + android_free_pixmap (bm->pixmap); +#endif + #ifdef HAVE_NTGUI DeleteObject (bm->pixmap); #endif /* HAVE_NTGUI */ @@ -969,7 +1045,7 @@ static void image_unget_x_image (struct image *, bool, Emacs_Pix_Container); image_unget_x_image (img, mask_p, ximg) #endif -#ifdef HAVE_X_WINDOWS +#if defined HAVE_X_WINDOWS || defined HAVE_ANDROID #ifndef USE_CAIRO static void image_sync_to_pixmaps (struct frame *, struct image *); @@ -983,6 +1059,8 @@ static bool x_create_x_image_and_pixmap (struct frame *, int, int, int, XImage **, Pixmap *); static void x_destroy_x_image (XImage *); +#if defined HAVE_X_WINDOWS + /* Create a mask of a bitmap. Note is this not a perfect mask. It's nicer with some borders in this context */ @@ -1079,7 +1157,9 @@ x_create_bitmap_mask (struct frame *f, ptrdiff_t id) x_destroy_x_image (mask_img); } -#endif /* HAVE_X_WINDOWS */ +#endif + +#endif /* HAVE_X_WINDOWS || defined HAVE_ANDROID*/ /*********************************************************************** Image types @@ -1113,7 +1193,7 @@ struct image_type #if defined HAVE_RSVG || defined HAVE_PNG || defined HAVE_GIF || \ defined HAVE_TIFF || defined HAVE_JPEG || defined HAVE_XPM || \ defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_PGTK || \ - defined HAVE_WEBP + defined HAVE_WEBP || defined HAVE_ANDROID # ifdef WINDOWSNT # define IMAGE_TYPE_INIT(f) f # else @@ -1615,7 +1695,7 @@ prepare_image_for_display (struct frame *f, struct image *img) } unblock_input (); } -#elif defined HAVE_X_WINDOWS +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID if (!img->load_failed_p) { block_input (); @@ -1822,7 +1902,7 @@ image_clear_image_1 (struct frame *f, struct image *img, int flags) /* NOTE (HAVE_NS): background color is NOT an indexed color! */ img->background_valid = 0; } -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO if (img->ximg) { image_destroy_x_image (img->ximg); @@ -1840,7 +1920,7 @@ image_clear_image_1 (struct frame *f, struct image *img, int flags) img->mask = NO_PIXMAP; img->background_transparent_valid = 0; } -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO if (img->mask_img) { image_destroy_x_image (img->mask_img); @@ -2212,11 +2292,11 @@ image_size_in_bytes (struct image *img) if (msk) size += msk->height * msk->bytes_per_line; -#elif defined HAVE_X_WINDOWS - /* Use a nominal depth of 24 bpp for pixmap and 1 bpp for mask, - to avoid having to query the server. */ +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID + /* Use a nominal depth of 24 and a bpp of 32 for pixmap and 1 bpp + for mask, to avoid having to query the server. */ if (img->pixmap != NO_PIXMAP) - size += img->width * img->height * 3; + size += img->width * img->height * 4; if (img->mask != NO_PIXMAP) size += img->width * img->height / 8; @@ -3195,9 +3275,12 @@ mark_image_cache (struct image_cache *c) /*********************************************************************** X / NS / W32 support code + Most of this code is shared with Android to make + it easier to maintain. ***********************************************************************/ -#ifdef HAVE_X_WINDOWS +#if defined HAVE_X_WINDOWS || defined HAVE_ANDROID + static bool x_check_image_size (XImage *ximg, int width, int height) { @@ -3213,7 +3296,11 @@ x_check_image_size (XImage *ximg, int width, int height) int bitmap_pad, depth, bytes_per_line; if (ximg) { +#ifndef HAVE_ANDROID bitmap_pad = ximg->bitmap_pad; +#else + bitmap_pad = (ximg->depth == 1 ? 8 : 32); +#endif depth = ximg->depth; bytes_per_line = ximg->bytes_per_line; } @@ -3231,16 +3318,23 @@ static bool x_create_x_image_and_pixmap (struct frame *f, int width, int height, int depth, XImage **ximg, Pixmap *pixmap) { +#ifndef HAVE_ANDROID Display *display = FRAME_X_DISPLAY (f); Drawable drawable = FRAME_X_DRAWABLE (f); +#endif eassert (input_blocked_p ()); if (depth <= 0) depth = FRAME_DISPLAY_INFO (f)->n_planes; +#ifndef HAVE_ANDROID *ximg = XCreateImage (display, FRAME_X_VISUAL (f), depth, ZPixmap, 0, NULL, width, height, depth > 16 ? 32 : depth > 8 ? 16 : 8, 0); +#else + *ximg = android_create_image (depth, ANDROID_Z_PIXMAP, NULL, width, + height); +#endif if (*ximg == NULL) { image_error ("Unable to allocate X image"); @@ -3260,7 +3354,15 @@ x_create_x_image_and_pixmap (struct frame *f, int width, int height, int depth, (*ximg)->data = xmalloc ((*ximg)->bytes_per_line * height); /* Allocate a pixmap of the same size. */ +#ifndef HAVE_ANDROID *pixmap = XCreatePixmap (display, drawable, width, height, depth); +#else +#ifndef ANDROID_STUBIFY + *pixmap = android_create_pixmap (width, height, depth); +#else + emacs_abort (); +#endif +#endif if (*pixmap == NO_PIXMAP) { x_destroy_x_image (*ximg); @@ -3281,7 +3383,11 @@ x_destroy_x_image (XImage *ximg) ximg->data = NULL; } +#ifndef HAVE_ANDROID XDestroyImage (ximg); +#else + android_destroy_image (ximg); +#endif } # if !defined USE_CAIRO && defined HAVE_XRENDER @@ -3348,7 +3454,7 @@ x_create_xrender_picture (struct frame *f, Emacs_Pixmap pixmap, int depth) static bool image_check_image_size (Emacs_Pix_Container ximg, int width, int height) { -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO return x_check_image_size (ximg, width, height); #else /* FIXME: Implement this check for the HAVE_NS and HAVE_NTGUI cases. @@ -3372,16 +3478,6 @@ image_create_x_image_and_pixmap_1 (struct frame *f, int width, int height, int d Emacs_Pix_Container *pimg, Emacs_Pixmap *pixmap, Picture *picture) { -#ifdef HAVE_ANDROID -#ifdef ANDROID_STUBIFY - /* This function should never be called when building stubs. */ - emacs_abort (); -#else - /* you lose, not yet implemented TODO */ - return false; -#endif -#endif - #ifdef USE_CAIRO eassert (input_blocked_p ()); @@ -3396,7 +3492,7 @@ image_create_x_image_and_pixmap_1 (struct frame *f, int width, int height, int d *pimg = *pixmap; return 1; -#elif defined HAVE_X_WINDOWS +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID if (!x_create_x_image_and_pixmap (f, width, height, depth, pimg, pixmap)) return 0; # ifdef HAVE_XRENDER @@ -3542,7 +3638,7 @@ image_create_x_image_and_pixmap_1 (struct frame *f, int width, int height, int d static void image_destroy_x_image (Emacs_Pix_Container pimg) { -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO x_destroy_x_image (pimg); #else eassert (input_blocked_p ()); @@ -3581,7 +3677,9 @@ gui_put_x_image (struct frame *f, Emacs_Pix_Container pimg, XPutImage (FRAME_X_DISPLAY (f), pixmap, gc, pimg, 0, 0, 0, 0, pimg->width, pimg->height); XFreeGC (FRAME_X_DISPLAY (f), gc); -#endif /* HAVE_X_WINDOWS */ +#elif defined HAVE_ANDROID + android_put_image (pixmap, pimg); +#endif #ifdef HAVE_NS eassert (pimg == pixmap); @@ -3618,7 +3716,7 @@ static void image_put_x_image (struct frame *f, struct image *img, Emacs_Pix_Container ximg, bool mask_p) { -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO if (!mask_p) { eassert (img->ximg == NULL); @@ -3636,7 +3734,7 @@ image_put_x_image (struct frame *f, struct image *img, Emacs_Pix_Container ximg, #endif } -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO /* Put the X images recorded in IMG on frame F into pixmaps, then free the X images and their buffers. */ @@ -3690,19 +3788,9 @@ image_unget_x_image_or_dc (struct image *img, bool mask_p, static Emacs_Pix_Container image_get_x_image (struct frame *f, struct image *img, bool mask_p) { -#ifdef HAVE_ANDROID -#ifdef ANDROID_STUBIFY - /* This function should never be called when building stubs. */ - emacs_abort (); -#else - /* you lose, not yet implemented TODO */ - return 0; -#endif -#endif - #if defined USE_CAIRO || defined (HAVE_HAIKU) return !mask_p ? img->pixmap : img->mask; -#elif defined HAVE_X_WINDOWS +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID XImage *ximg_in_img = !mask_p ? img->ximg : img->mask_img; if (ximg_in_img) @@ -3712,9 +3800,15 @@ image_get_x_image (struct frame *f, struct image *img, bool mask_p) return XGetImage (FRAME_X_DISPLAY (f), !mask_p ? img->pixmap : img->mask, 0, 0, img->original_width, img->original_height, ~0, ZPixmap); #endif +#ifndef HAVE_ANDROID else return XGetImage (FRAME_X_DISPLAY (f), !mask_p ? img->pixmap : img->mask, 0, 0, img->width, img->height, ~0, ZPixmap); +#else + else + return android_get_image (!mask_p ? img->pixmap : img->mask, + ANDROID_Z_PIXMAP); +#endif #elif defined (HAVE_NS) Emacs_Pix_Container pixmap = !mask_p ? img->pixmap : img->mask; @@ -3727,13 +3821,18 @@ static void image_unget_x_image (struct image *img, bool mask_p, Emacs_Pix_Container ximg) { #ifdef USE_CAIRO -#elif defined HAVE_X_WINDOWS +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID XImage *ximg_in_img = !mask_p ? img->ximg : img->mask_img; if (ximg_in_img) eassert (ximg == ximg_in_img); +#ifdef HAVE_ANDROID + else + android_destroy_image (ximg); +#else else XDestroyImage (ximg); +#endif #elif defined (HAVE_NS) ns_release_object (ximg); #endif @@ -4256,6 +4355,15 @@ Create_Pixmap_From_Bitmap_Data (struct frame *f, struct image *img, char *data, img->picture = x_create_xrender_picture (f, img->pixmap, 0); # endif +#elif defined HAVE_ANDROID +#ifndef ANDROID_STUBIFY + img->pixmap + = android_create_pixmap_from_bitmap_data (data, img->width, img->height, + fg, bg, + FRAME_DISPLAY_INFO (f)->n_planes); +#else + emacs_abort (); +#endif #elif defined HAVE_NTGUI img->pixmap = w32_create_pixmap_from_bitmap_data (img->width, img->height, data); @@ -4686,7 +4794,8 @@ xbm_load (struct frame *f, struct image *img) XPM images ***********************************************************************/ -#if defined (HAVE_XPM) || defined (HAVE_NS) || defined (HAVE_PGTK) +#if defined (HAVE_XPM) || defined (HAVE_NS) || defined (HAVE_PGTK) \ + || defined (HAVE_ANDROID) static bool xpm_image_p (Lisp_Object object); static bool xpm_load (struct frame *f, struct image *img); @@ -4716,7 +4825,8 @@ static bool xpm_load (struct frame *f, struct image *img); #endif /* not HAVE_NTGUI */ #endif /* HAVE_XPM */ -#if defined HAVE_XPM || defined USE_CAIRO || defined HAVE_NS || defined HAVE_HAIKU +#if defined HAVE_XPM || defined USE_CAIRO || defined HAVE_NS \ + || defined HAVE_HAIKU || defined HAVE_ANDROID /* Indices of image specification fields in xpm_format, below. */ @@ -4736,7 +4846,8 @@ enum xpm_keyword_index XPM_LAST }; -#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_PGTK +#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU \ + || defined HAVE_PGTK || defined HAVE_ANDROID /* Vector of image_keyword structures describing the format of valid XPM image specifications. */ @@ -4978,7 +5089,8 @@ init_xpm_functions (void) #endif /* WINDOWSNT */ -#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_PGTK +#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU \ + || defined HAVE_PGTK || defined HAVE_ANDROID /* Value is true if COLOR_SYMBOLS is a valid color symbols list for XPM images. Such a list must consist of conses whose car and cdr are strings. */ @@ -5014,9 +5126,9 @@ xpm_image_p (Lisp_Object object) && (! fmt[XPM_COLOR_SYMBOLS].count || xpm_valid_color_symbols_p (fmt[XPM_COLOR_SYMBOLS].value))); } -#endif /* HAVE_XPM || HAVE_NS || HAVE_HAIKU || HAVE_PGTK */ +#endif /* HAVE_XPM || HAVE_NS || HAVE_HAIKU || HAVE_PGTK || HAVE_ANDROID */ -#endif /* HAVE_XPM || USE_CAIRO || HAVE_NS || HAVE_HAIKU */ +#endif /* HAVE_XPM || USE_CAIRO || HAVE_NS || HAVE_HAIKU || HAVE_ANDROID */ #if defined HAVE_XPM && defined HAVE_X_WINDOWS && !defined USE_GTK ptrdiff_t @@ -5389,10 +5501,12 @@ xpm_load (struct frame *f, struct image *img) #if (defined USE_CAIRO && defined HAVE_XPM) \ || (defined HAVE_NS && !defined HAVE_XPM) \ || (defined HAVE_HAIKU && !defined HAVE_XPM) \ - || (defined HAVE_PGTK && !defined HAVE_XPM) + || (defined HAVE_PGTK && !defined HAVE_XPM) \ + || (defined HAVE_ANDROID && !defined HAVE_XPM) -/* XPM support functions for NS and Haiku where libxpm is not available, and for - Cairo. Only XPM version 3 (without any extensions) is supported. */ +/* XPM support functions for NS, Haiku and Android where libxpm is not + available, and for Cairo. Only XPM version 3 (without any + extensions) is supported. */ static void xpm_put_color_table_v (Lisp_Object, const char *, int, Lisp_Object); @@ -6492,11 +6606,16 @@ image_pixmap_draw_cross (struct frame *f, Emacs_Pixmap pixmap, #elif HAVE_HAIKU be_draw_cross_on_pixmap (pixmap, x, y, width, height, color); #elif HAVE_ANDROID -#ifdef ANDROID_STUBIFY - /* This function should never be called when building stubs. */ - emacs_abort (); +#ifndef ANDROID_STUBIFY + struct android_gc *gc; + + gc = android_create_gc (0, NULL); + android_set_foreground (gc, color); + android_draw_line (pixmap, gc, x, y, x + width - 1, y + height - 1); + android_draw_line (pixmap, gc, x, y + height - 1, x + width - 1, y); + android_free_gc (gc); #else - /* you lose, not yet implemented TODO */ + emacs_abort (); #endif #endif } @@ -6552,7 +6671,7 @@ image_disable_image (struct frame *f, struct image *img) #define MaskForeground(f) PIX_MASK_DRAW #endif /* USE_CAIRO || HAVE_HAIKU */ -#if !defined USE_CAIRO && !defined HAVE_HAIKU && !defined HAVE_ANDROID +#if !defined USE_CAIRO && !defined HAVE_HAIKU image_sync_to_pixmaps (f, img); #endif /* !USE_CAIRO && !HAVE_HAIKU */ image_pixmap_draw_cross (f, img->pixmap, 0, 0, img->width, img->height, @@ -12079,7 +12198,8 @@ static struct image_type const image_types[] = { SYMBOL_INDEX (Qjpeg), jpeg_image_p, jpeg_load, image_clear_image, IMAGE_TYPE_INIT (init_jpeg_functions) }, #endif -#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_PGTK +#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU \ + || defined HAVE_PGTK || defined HAVE_ANDROID { SYMBOL_INDEX (Qxpm), xpm_image_p, xpm_load, image_clear_image, IMAGE_TYPE_INIT (init_xpm_functions) }, #endif @@ -12138,7 +12258,7 @@ syms_of_image (void) DEFVAR_LISP ("image-types", Vimage_types, doc: /* List of potentially supported image types. Each element of the list is a symbol for an image type, like `jpeg' or `png'. -To check whether it is really supported, use `image-type-available-p'. */); + check whether it is really supported, use `image-type-available-p'. */); Vimage_types = Qnil; DEFVAR_LISP ("max-image-size", Vmax_image_size, @@ -12241,7 +12361,8 @@ non-numeric, there is no explicit limit on the size of images. */); add_image_type (Qxbm); #if defined (HAVE_XPM) || defined (HAVE_NS) \ - || defined (HAVE_HAIKU) || defined (HAVE_PGTK) + || defined (HAVE_HAIKU) || defined (HAVE_PGTK) \ + || defined (HAVE_ANDROID) DEFSYM (Qxpm, "xpm"); add_image_type (Qxpm); #endif diff --git a/src/keyboard.c b/src/keyboard.c index 7bf89ac7d4b..8eec833a9ca 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -4909,7 +4909,58 @@ static const char *const lispy_accent_keys[] = "dead-horn", }; -#ifdef HAVE_NTGUI +#ifdef HAVE_ANDROID +#define FUNCTION_KEY_OFFSET 0 + +const char *const lispy_function_keys[] = + { + /* All elements in this array default to 0, except for the few + function keys that Emacs recognizes. */ + [111] = "escape", + [121] = "break", + [122] = "home", + [123] = "end", + [124] = "insert", + [131] = "f1", + [132] = "f2", + [133] = "f3", + [134] = "f4", + [135] = "f5", + [136] = "f6", + [137] = "f7", + [138] = "f8", + [139] = "f9", + [140] = "f10", + [141] = "f11", + [142] = "f12", + [160] = "kp-ret", + [19] = "up", + [20] = "down", + [213] = "muhenkan", + [214] = "henkan", + [215] = "hiragana-katakana", + [218] = "kana", + [21] = "left", + [22] = "right", + [259] = "help", + [268] = "kp-up-left", + [269] = "kp-down-left", + [270] = "kp-up-right", + [271] = "kp-down-right", + [277] = "cut", + [278] = "copy", + [279] = "paste", + [28] = "clear", + [4] = "back", + [61] = "tab", + [66] = "return", + [67] = "backspace", + [82] = "menu", + [92] = "page-up", + [93] = "page-down", + }; + +#elif defined HAVE_NTGUI #define FUNCTION_KEY_OFFSET 0x0 const char *const lispy_function_keys[] = diff --git a/src/lread.c b/src/lread.c index eb029f01623..15273ad34fa 100644 --- a/src/lread.c +++ b/src/lread.c @@ -2073,7 +2073,7 @@ static void build_load_history (Lisp_Object filename, bool entire) { Lisp_Object tail, prev, newelt; - Lisp_Object tem, tem2; + Lisp_Object tem, tem2, association; bool foundit = 0; tail = Vload_history; @@ -2088,7 +2088,7 @@ build_load_history (Lisp_Object filename, bool entire) { foundit = 1; - /* If we're loading the entire file, remove old data. */ + /* If we're loading the entire file, remove old data. */ if (entire) { if (NILP (prev)) @@ -2096,8 +2096,8 @@ build_load_history (Lisp_Object filename, bool entire) else Fsetcdr (prev, XCDR (tail)); } - - /* Otherwise, cons on new symbols that are not already members. */ + /* Otherwise, cons on new symbols that are not already + members. */ else { tem2 = Vcurrent_load_list; @@ -2122,8 +2122,16 @@ build_load_history (Lisp_Object filename, bool entire) front of load-history, the most-recently-loaded position. Also do this if we didn't find an existing member for the file. */ if (entire || !foundit) - Vload_history = Fcons (Fnreverse (Vcurrent_load_list), - Vload_history); + { + association = Fnreverse (Vcurrent_load_list); + + if (!NILP (association) && STRINGP (XCAR (association))) + /* readevalloop can be called with SOURCENAME set to some + nonsense value, meaning the car of ASSOCIATION will be nil + (or worse something else), leading to an invalid + Vload_history. Ignore such invalid entries. */ + Vload_history = Fcons (association, Vload_history); + } } static void -- cgit v1.3 From 695e26079eb60d10ffe25bb8ae91ebc6131fb27d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 8 Jan 2023 15:39:02 +0800 Subject: Update Java part of Android port * java/org/gnu/emacs/EmacsCopyArea.java (EmacsCopyArea, perform) (paintTo): * java/org/gnu/emacs/EmacsDrawLine.java (EmacsDrawLine): * java/org/gnu/emacs/EmacsDrawPoint.java (EmacsDrawPoint): * java/org/gnu/emacs/EmacsDrawRectangle.java (EmacsDrawRectangle) (paintTo): * java/org/gnu/emacs/EmacsDrawable.java (EmacsDrawable): * java/org/gnu/emacs/EmacsFillPolygon.java (EmacsFillPolygon): * java/org/gnu/emacs/EmacsFillRectangle.java (EmacsFillRectangle): * java/org/gnu/emacs/EmacsFontDriver.java (EmacsFontDriver): * java/org/gnu/emacs/EmacsGC.java (EmacsGC): * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsPixmap.java (EmacsPixmap): * java/org/gnu/emacs/EmacsSdk23FontDriver.java (EmacsSdk23FontDriver): * java/org/gnu/emacs/EmacsSdk7FontDriver.java (EmacsSdk7FontDriver, textExtents1, textExtents, draw): * java/org/gnu/emacs/EmacsService.java (EmacsService, copyArea): * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): * java/org/gnu/emacs/EmacsView.java (EmacsView, onLayout) (onFocusChanged): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, run) (resizeWindow, lockCanvas, getBitmap, onKeyDown, onKeyUp) (onActivityDetached): Move rendering to main thread. Make drawing operations completely static. --- java/org/gnu/emacs/EmacsCopyArea.java | 131 +++++++++------------- java/org/gnu/emacs/EmacsDrawLine.java | 120 +++++--------------- java/org/gnu/emacs/EmacsDrawPoint.java | 11 +- java/org/gnu/emacs/EmacsDrawRectangle.java | 160 +++++++++++++-------------- java/org/gnu/emacs/EmacsDrawable.java | 1 - java/org/gnu/emacs/EmacsFillPolygon.java | 128 ++++++--------------- java/org/gnu/emacs/EmacsFillRectangle.java | 152 ++++++++++++------------- java/org/gnu/emacs/EmacsFontDriver.java | 2 +- java/org/gnu/emacs/EmacsGC.java | 77 ++++++------- java/org/gnu/emacs/EmacsNative.java | 22 ++++ java/org/gnu/emacs/EmacsPixmap.java | 9 +- java/org/gnu/emacs/EmacsSdk23FontDriver.java | 71 ++++++++++++ java/org/gnu/emacs/EmacsSdk7FontDriver.java | 139 +++++++---------------- java/org/gnu/emacs/EmacsService.java | 122 ++------------------ java/org/gnu/emacs/EmacsSurfaceView.java | 22 ++-- java/org/gnu/emacs/EmacsView.java | 133 ++++++++++++++++------ java/org/gnu/emacs/EmacsWindow.java | 141 +++++++++++++++++++---- 17 files changed, 685 insertions(+), 756 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 0dd5b2c1fb6..00e817bb97d 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -27,54 +27,16 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsCopyArea implements EmacsPaintReq +public class EmacsCopyArea { - private int src_x, src_y, dest_x, dest_y, width, height; - private EmacsDrawable destination, source; - private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu, overAlu; + private static Xfermode overAlu; static { overAlu = new PorterDuffXfermode (Mode.SRC_OVER); - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); }; - public - EmacsCopyArea (EmacsDrawable source, EmacsDrawable destination, - int src_x, int src_y, int width, int height, - int dest_x, int dest_y, EmacsGC immutableGC) - { - Bitmap bitmap; - - this.destination = destination; - this.source = source; - this.src_x = src_x; - this.src_y = src_y; - this.width = width; - this.height = height; - this.dest_x = dest_x; - this.dest_y = dest_y; - this.immutableGC = immutableGC; - } - - @Override - public Rect - getRect () - { - return new Rect (dest_x, dest_y, dest_x + width, - dest_y + height); - } - - @Override - public EmacsDrawable - getDrawable () - { - return destination; - } - - private void + private static void insetRectBy (Rect rect, int left, int top, int right, int bottom) { @@ -84,30 +46,38 @@ public class EmacsCopyArea implements EmacsPaintReq rect.bottom -= bottom; } - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) + public static void + perform (EmacsDrawable source, EmacsGC gc, + EmacsDrawable destination, + int src_x, int src_y, int width, int height, + int dest_x, int dest_y) { - int alu; + int i; Bitmap bitmap; - Paint maskPaint; - Canvas maskCanvas; + Paint maskPaint, paint; + Canvas maskCanvas, canvas; Bitmap srcBitmap, maskBitmap, clipBitmap; Rect rect, maskRect, srcRect, dstRect, maskDestRect; boolean needFill; /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; - alu = immutableGC.function; + paint = gc.gcPaint; + + canvas = destination.lockCanvas (); + + if (canvas == null) + return; + + canvas.save (); + + if (gc.real_clip_rects != null) + { + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); + } /* A copy must be created or drawBitmap could end up overwriting itself. */ @@ -137,14 +107,10 @@ public class EmacsCopyArea implements EmacsPaintReq if (src_y + height > srcBitmap.getHeight ()) height = srcBitmap.getHeight () - src_y; - rect = getRect (); - - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + rect = new Rect (dest_x, dest_y, dest_x + width, + dest_y + height); - if (immutableGC.clip_mask == null) + if (gc.clip_mask == null) { bitmap = Bitmap.createBitmap (srcBitmap, src_x, src_y, width, @@ -156,17 +122,17 @@ public class EmacsCopyArea implements EmacsPaintReq /* Drawing with a clip mask involves calculating the intersection of the clip mask with the dst rect, and extrapolating the corresponding part of the src rect. */ - clipBitmap = immutableGC.clip_mask.bitmap; + clipBitmap = gc.clip_mask.bitmap; dstRect = new Rect (dest_x, dest_y, dest_x + width, dest_y + height); - maskRect = new Rect (immutableGC.clip_x_origin, - immutableGC.clip_y_origin, - (immutableGC.clip_x_origin + maskRect = new Rect (gc.clip_x_origin, + gc.clip_y_origin, + (gc.clip_x_origin + clipBitmap.getWidth ()), - (immutableGC.clip_y_origin + (gc.clip_y_origin + clipBitmap.getHeight ())); - clipBitmap = immutableGC.clip_mask.bitmap; + clipBitmap = gc.clip_mask.bitmap; if (!maskRect.setIntersect (dstRect, maskRect)) /* There is no intersection between the clip mask and the @@ -191,20 +157,21 @@ public class EmacsCopyArea implements EmacsPaintReq /* Draw the mask onto the maskBitmap. */ maskCanvas = new Canvas (maskBitmap); - maskRect.offset (-immutableGC.clip_x_origin, - -immutableGC.clip_y_origin); - maskCanvas.drawBitmap (immutableGC.clip_mask.bitmap, - maskRect, new Rect (0, 0, - maskRect.width (), - maskRect.height ()), - paint); - maskRect.offset (immutableGC.clip_x_origin, - immutableGC.clip_y_origin); + maskPaint = new Paint (); + maskRect.offset (-gc.clip_x_origin, + -gc.clip_y_origin); + maskCanvas.drawBitmap (gc.clip_mask.bitmap, + maskRect, + new Rect (0, 0, + maskRect.width (), + maskRect.height ()), + maskPaint); + maskRect.offset (gc.clip_x_origin, + gc.clip_y_origin); /* Set the transfer mode to SRC_IN to preserve only the parts of the source that overlap with the mask. */ - maskPaint = new Paint (); - maskPaint.setXfermode (srcInAlu); + maskPaint.setXfermode (EmacsGC.srcInAlu); /* Draw the source. */ maskDestRect = new Rect (0, 0, srcRect.width (), @@ -215,6 +182,10 @@ public class EmacsCopyArea implements EmacsPaintReq /* Finally, draw the mask bitmap to the destination. */ paint.setXfermode (overAlu); canvas.drawBitmap (maskBitmap, null, maskRect, paint); + gc.resetXfermode (); } + + canvas.restore (); + destination.damageRect (rect); } } diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 6389031bbfa..8941d4c217f 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -29,109 +29,49 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsDrawLine implements EmacsPaintReq +public class EmacsDrawLine { - private int x, y, x2, y2; - private EmacsDrawable drawable; - private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu; - - static + public static void + perform (EmacsDrawable drawable, EmacsGC gc, + int x, int y, int x2, int y2) { - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); - }; + Rect rect; + Canvas canvas; + Paint paint; + int i; - public - EmacsDrawLine (EmacsDrawable drawable, int x, int y, - int x2, int y2, EmacsGC immutableGC) - { - this.drawable = drawable; - this.x = x; - this.y = y; - this.x2 = x2; - this.y2 = y2; - this.immutableGC = immutableGC; - } + /* TODO implement stippling. */ + if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + return; - @Override - public Rect - getRect () - { - return new Rect (Math.min (x, x2 + 1), + paint = gc.gcPaint; + rect = new Rect (Math.min (x, x2 + 1), Math.min (y, y2 + 1), Math.max (x2 + 1, x), Math.max (y2 + 1, y)); - } - - @Override - public EmacsDrawable - getDrawable () - { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) - { - int alu; - Paint maskPaint; - Canvas maskCanvas; - Bitmap maskBitmap; - Rect rect, srcRect; - int width, height; + canvas = drawable.lockCanvas (); - /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + if (canvas == null) return; - alu = immutableGC.function; - rect = getRect (); - width = rect.width (); - height = rect.height (); - - paint.setStyle (Paint.Style.STROKE); - - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + canvas.save (); - if (immutableGC.clip_mask == null) - { - paint.setColor (immutableGC.foreground | 0xff000000); - canvas.drawLine ((float) x, (float) y, - (float) x2, (float) y2, - paint); - } - else + if (gc.real_clip_rects != null) { - maskPaint = new Paint (); - maskBitmap - = immutableGC.clip_mask.bitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) - return; - - maskPaint.setXfermode (srcInAlu); - maskPaint.setColor (immutableGC.foreground | 0xff000000); - maskCanvas = new Canvas (maskBitmap); - srcRect = new Rect (0, 0, maskBitmap.getWidth (), - maskBitmap.getHeight ()); - maskCanvas.drawLine (0.0f, 0.0f, (float) Math.abs (x - x2), - (float) Math.abs (y - y2), maskPaint); - canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); } - paint.setXfermode (null); + paint.setStyle (Paint.Style.STROKE); + + if (gc.clip_mask == null) + canvas.drawLine ((float) x, (float) y, + (float) x2, (float) y2, + paint); + + /* DrawLine with clip mask not implemented; it is not used by + Emacs. */ + canvas.restore (); + drawable.damageRect (rect); } } diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java index 772757ff424..3bc7be17961 100644 --- a/java/org/gnu/emacs/EmacsDrawPoint.java +++ b/java/org/gnu/emacs/EmacsDrawPoint.java @@ -19,12 +19,13 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -public class EmacsDrawPoint extends EmacsDrawRectangle +public class EmacsDrawPoint { - public - EmacsDrawPoint (EmacsDrawable drawable, int x, int y, - EmacsGC immutableGC) + public static void + perform (EmacsDrawable drawable, + EmacsGC immutableGC, int x, int y) { - super (drawable, x, y, 1, 1, immutableGC); + EmacsDrawRectangle.perform (drawable, immutableGC, + x, y, 1, 1); } } diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index e3f28227146..b42e9556e8c 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -22,110 +22,104 @@ package org.gnu.emacs; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Xfermode; -public class EmacsDrawRectangle implements EmacsPaintReq -{ - private int x, y, width, height; - private EmacsDrawable drawable; - private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu; - - static - { - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); - }; - - public - EmacsDrawRectangle (EmacsDrawable drawable, int x, int y, - int width, int height, - EmacsGC immutableGC) - { - this.drawable = drawable; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.immutableGC = immutableGC; - } +import android.util.Log; - @Override - public Rect - getRect () - { - /* Canvas.drawRect actually behaves exactly like PolyRectangle wrt - to where the lines are placed, so extend the width and height - by 1 in the damage rectangle. */ - return new Rect (x, y, x + width + 1, y + height + 1); - } - - @Override - public EmacsDrawable - getDrawable () - { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) +public class EmacsDrawRectangle +{ + public static void + perform (EmacsDrawable drawable, EmacsGC gc, + int x, int y, int width, int height) { - int alu; - Paint maskPaint; + int i; + Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; - Rect rect, srcRect; + Rect rect; + Rect maskRect, dstRect; + Canvas canvas; + Bitmap clipBitmap; /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; - alu = immutableGC.function; - rect = new Rect (x, y, x + width, y + height); + canvas = drawable.lockCanvas (); - paint.setStyle (Paint.Style.STROKE); - paint.setStrokeWidth (1); + if (canvas == null) + return; - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + canvas.save (); - if (immutableGC.clip_mask == null) + if (gc.real_clip_rects != null) { - paint.setColor (immutableGC.foreground | 0xff000000); - canvas.drawRect (rect, paint); + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); } + + paint = gc.gcPaint; + rect = new Rect (x, y, x + width, y + height); + + paint.setStyle (Paint.Style.STROKE); + + if (gc.clip_mask == null) + canvas.drawRect (rect, paint); else { - maskPaint = new Paint (); - maskBitmap - = immutableGC.clip_mask.bitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) + /* Drawing with a clip mask involves calculating the + intersection of the clip mask with the dst rect, and + extrapolating the corresponding part of the src rect. */ + clipBitmap = gc.clip_mask.bitmap; + dstRect = new Rect (x, y, x + width, y + height); + maskRect = new Rect (gc.clip_x_origin, + gc.clip_y_origin, + (gc.clip_x_origin + + clipBitmap.getWidth ()), + (gc.clip_y_origin + + clipBitmap.getHeight ())); + clipBitmap = gc.clip_mask.bitmap; + + if (!maskRect.setIntersect (dstRect, maskRect)) + /* There is no intersection between the clip mask and the + dest rect. */ return; - maskPaint.setXfermode (srcInAlu); - maskPaint.setColor (immutableGC.foreground | 0xff000000); + /* Finally, create a temporary bitmap that is the size of + maskRect. */ + + maskBitmap + = Bitmap.createBitmap (maskRect.width (), maskRect.height (), + Bitmap.Config.ARGB_8888); + + /* Draw the mask onto the maskBitmap. */ maskCanvas = new Canvas (maskBitmap); - srcRect = new Rect (0, 0, maskBitmap.getWidth (), - maskBitmap.getHeight ()); - maskCanvas.drawRect (srcRect, maskPaint); - canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + maskRect.offset (-gc.clip_x_origin, + -gc.clip_y_origin); + maskCanvas.drawBitmap (gc.clip_mask.bitmap, + maskRect, new Rect (0, 0, + maskRect.width (), + maskRect.height ()), + paint); + maskRect.offset (gc.clip_x_origin, + gc.clip_y_origin); + + /* Set the transfer mode to SRC_IN to preserve only the parts + of the source that overlap with the mask. */ + maskPaint = new Paint (); + maskPaint.setXfermode (EmacsGC.srcInAlu); + maskPaint.setStyle (Paint.Style.STROKE); + + /* Draw the source. */ + maskCanvas.drawRect (maskRect, maskPaint); + + /* Finally, draw the mask bitmap to the destination. */ + paint.setXfermode (null); + canvas.drawBitmap (maskBitmap, null, maskRect, paint); } - paint.setXfermode (null); + canvas.restore (); + drawable.damageRect (new Rect (x, y, x + width + 1, + y + height + 1)); } } diff --git a/java/org/gnu/emacs/EmacsDrawable.java b/java/org/gnu/emacs/EmacsDrawable.java index 19062137213..6a6199ff214 100644 --- a/java/org/gnu/emacs/EmacsDrawable.java +++ b/java/org/gnu/emacs/EmacsDrawable.java @@ -26,7 +26,6 @@ import android.graphics.Canvas; public interface EmacsDrawable { public Canvas lockCanvas (); - public void unlockCanvas (); public void damageRect (Rect damageRect); public Bitmap getBitmap (); public boolean isDestroyed (); diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java index 3198c7f07c4..42b73886dff 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -26,34 +26,35 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.Xfermode; -public class EmacsFillPolygon implements EmacsPaintReq +public class EmacsFillPolygon { - private EmacsDrawable drawable; - private EmacsGC immutableGC; - private Path path; + public static void + perform (EmacsDrawable drawable, EmacsGC gc, Point points[]) + { + Canvas canvas; + Path path; + Paint paint; + Rect rect; + RectF rectF; + int i; - private static Xfermode xorAlu, srcInAlu; + canvas = drawable.lockCanvas (); - static - { - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); - }; + if (canvas == null) + return; - public - EmacsFillPolygon (EmacsDrawable drawable, Point points[], - EmacsGC immutableGC) - { - int i; + paint = gc.gcPaint; + + canvas.save (); - this.drawable = drawable; - this.immutableGC = immutableGC; + if (gc.real_clip_rects != null) + { + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); + } /* Build the path from the given array of points. */ path = new Path (); @@ -67,84 +68,25 @@ public class EmacsFillPolygon implements EmacsPaintReq path.close (); } - } - - @Override - public Rect - getRect () - { - RectF rect; - rect = new RectF (0, 0, 0, 0); - path.computeBounds (rect, true); + /* Compute the damage rectangle. */ + rectF = new RectF (0, 0, 0, 0); + path.computeBounds (rectF, true); - return new Rect ((int) Math.floor (rect.left), - (int) Math.floor (rect.top), - (int) Math.ceil (rect.right), - (int) Math.ceil (rect.bottom)); - } + rect = new Rect ((int) Math.floor (rectF.left), + (int) Math.floor (rectF.top), + (int) Math.ceil (rectF.right), + (int) Math.ceil (rectF.bottom)); - @Override - public EmacsDrawable - getDrawable () - { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) - { - int alu; - Paint maskPaint; - Canvas maskCanvas; - Bitmap maskBitmap; - Rect rect; - - /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) - return; - - alu = immutableGC.function; - rect = getRect (); + paint.setStyle (Paint.Style.FILL); - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + if (gc.clip_mask == null) + canvas.drawPath (path, paint); - paint.setStyle (Paint.Style.FILL); + canvas.restore (); + drawable.damageRect (rect); - if (immutableGC.clip_mask == null) - { - paint.setColor (immutableGC.foreground | 0xff000000); - canvas.drawPath (path, paint); - } - else - { - maskPaint = new Paint (); - maskBitmap = immutableGC.clip_mask.bitmap; - maskBitmap = maskBitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) - return; - - maskPaint.setXfermode (srcInAlu); - maskPaint.setColor (immutableGC.foreground | 0xff000000); - maskCanvas = new Canvas (maskBitmap); - path.offset (-rect.left, -rect.top, null); - maskCanvas.drawPath (path, maskPaint); - canvas.drawBitmap (maskBitmap, new Rect (0, 0, rect.width (), - rect.height ()), - rect, paint); - } + /* FillPolygon with clip mask not implemented; it is not used by + Emacs. */ } } diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java index 7246c13de7f..b733b417d6b 100644 --- a/java/org/gnu/emacs/EmacsFillRectangle.java +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -22,108 +22,102 @@ package org.gnu.emacs; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Xfermode; import android.util.Log; -public class EmacsFillRectangle implements EmacsPaintReq +public class EmacsFillRectangle { - private int x, y, width, height; - private EmacsDrawable drawable; - private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu; - - static - { - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); - }; - - public - EmacsFillRectangle (EmacsDrawable drawable, int x, int y, - int width, int height, - EmacsGC immutableGC) - { - this.drawable = drawable; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.immutableGC = immutableGC; - } - - @Override - public Rect - getRect () - { - return new Rect (x, y, x + width, y + height); - } - - @Override - public EmacsDrawable - getDrawable () + public static void + perform (EmacsDrawable drawable, EmacsGC gc, + int x, int y, int width, int height) { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) - { - int alu; - Paint maskPaint; + int i; + Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; - Rect rect, srcRect; + Rect rect; + Rect maskRect, dstRect; + Canvas canvas; + Bitmap clipBitmap; /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; - alu = immutableGC.function; - rect = getRect (); + canvas = drawable.lockCanvas (); - paint.setStyle (Paint.Style.FILL); + if (canvas == null) + return; - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + canvas.save (); - if (immutableGC.clip_mask == null) + if (gc.real_clip_rects != null) { - paint.setColor (immutableGC.foreground | 0xff000000); - canvas.drawRect (rect, paint); + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); } + + paint = gc.gcPaint; + rect = new Rect (x, y, x + width, y + height); + + paint.setStyle (Paint.Style.FILL); + + if (gc.clip_mask == null) + canvas.drawRect (rect, paint); else { - maskPaint = new Paint (); - maskBitmap - = immutableGC.clip_mask.bitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) + /* Drawing with a clip mask involves calculating the + intersection of the clip mask with the dst rect, and + extrapolating the corresponding part of the src rect. */ + clipBitmap = gc.clip_mask.bitmap; + dstRect = new Rect (x, y, x + width, y + height); + maskRect = new Rect (gc.clip_x_origin, + gc.clip_y_origin, + (gc.clip_x_origin + + clipBitmap.getWidth ()), + (gc.clip_y_origin + + clipBitmap.getHeight ())); + clipBitmap = gc.clip_mask.bitmap; + + if (!maskRect.setIntersect (dstRect, maskRect)) + /* There is no intersection between the clip mask and the + dest rect. */ return; - maskPaint.setXfermode (srcInAlu); - maskPaint.setColor (immutableGC.foreground | 0xff000000); + /* Finally, create a temporary bitmap that is the size of + maskRect. */ + + maskBitmap + = Bitmap.createBitmap (maskRect.width (), maskRect.height (), + Bitmap.Config.ARGB_8888); + + /* Draw the mask onto the maskBitmap. */ maskCanvas = new Canvas (maskBitmap); - srcRect = new Rect (0, 0, maskBitmap.getWidth (), - maskBitmap.getHeight ()); - maskCanvas.drawRect (srcRect, maskPaint); - canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + maskRect.offset (-gc.clip_x_origin, + -gc.clip_y_origin); + maskCanvas.drawBitmap (gc.clip_mask.bitmap, + maskRect, new Rect (0, 0, + maskRect.width (), + maskRect.height ()), + paint); + maskRect.offset (gc.clip_x_origin, + gc.clip_y_origin); + + /* Set the transfer mode to SRC_IN to preserve only the parts + of the source that overlap with the mask. */ + maskPaint = new Paint (); + maskPaint.setXfermode (EmacsGC.srcInAlu); + + /* Draw the source. */ + maskCanvas.drawRect (maskRect, maskPaint); + + /* Finally, draw the mask bitmap to the destination. */ + paint.setXfermode (null); + canvas.drawBitmap (maskBitmap, null, maskRect, paint); } - paint.setXfermode (null); + canvas.restore (); + drawable.damageRect (rect); } } diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java index 9f40aa04c44..1d1e6f7b33f 100644 --- a/java/org/gnu/emacs/EmacsFontDriver.java +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -164,7 +164,7 @@ public abstract class EmacsFontDriver public static EmacsFontDriver createFontDriver () { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) return new EmacsSdk23FontDriver (); return new EmacsSdk7FontDriver (); diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java index 0becb04519d..caa5c91edd4 100644 --- a/java/org/gnu/emacs/EmacsGC.java +++ b/java/org/gnu/emacs/EmacsGC.java @@ -20,6 +20,11 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import android.graphics.Rect; +import android.graphics.Paint; + +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Xfermode; /* X like graphics context structures. Keep the enums in synch with androidgui.h! */ @@ -32,14 +37,21 @@ public class EmacsGC extends EmacsHandleObject public static final int GC_FILL_SOLID = 0; public static final int GC_FILL_OPAQUE_STIPPLED = 1; + public static final Xfermode xorAlu, srcInAlu; + public int function, fill_style; public int foreground, background; public int clip_x_origin, clip_y_origin; public int ts_origin_x, ts_origin_y; - public Rect clip_rects[]; + public Rect clip_rects[], real_clip_rects[]; public EmacsPixmap clip_mask, stipple; - private boolean dirty; - private EmacsGC immutableGC; + public Paint gcPaint; + + static + { + xorAlu = new PorterDuffXfermode (Mode.XOR); + srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); + } /* The following fields are only set on immutable GCs. */ @@ -55,62 +67,41 @@ public class EmacsGC extends EmacsHandleObject fill_style = GC_FILL_SOLID; function = GC_COPY; foreground = 0; - background = 0xffffffff; + background = 0xffffff; + gcPaint = new Paint (); } - public - EmacsGC (EmacsGC source) - { - super ((short) 0); + /* Mark this GC as dirty. Apply parameters to the paint and + recompute real_clip_rects. */ + public void + markDirty () + { int i; - function = source.function; - fill_style = source.fill_style; - foreground = source.foreground; - background = source.background; - clip_x_origin = source.clip_x_origin; - clip_y_origin = source.clip_y_origin; - clip_rects = source.clip_rects; - clip_mask = source.clip_mask; - stipple = source.stipple; - ts_origin_x = source.ts_origin_x; - ts_origin_y = source.ts_origin_y; - - /* Offset all the clip rects by ts_origin_x and ts_origin_y. */ - if ((ts_origin_x != 0 || ts_origin_y != 0) && clip_rects != null) { - clip_rects = new Rect[clip_rects.length]; + real_clip_rects = new Rect[clip_rects.length]; for (i = 0; i < clip_rects.length; ++i) { - clip_rects[i] = new Rect (source.clip_rects[i]); - clip_rects[i].offset (ts_origin_x, - ts_origin_y); + real_clip_rects[i] = new Rect (clip_rects[i]); + real_clip_rects[i].offset (ts_origin_x, ts_origin_y); } } - } + else + real_clip_rects = clip_rects; - /* Mark this GC as dirty. This means immutableGC will return a new - copy of this GC the next time it is called. */ + gcPaint.setColor (foreground | 0xff000000); + gcPaint.setXfermode (function == GC_XOR + ? xorAlu : srcInAlu); + } public void - markDirty () + resetXfermode () { - dirty = true; + gcPaint.setXfermode (function == GC_XOR + ? xorAlu : srcInAlu); } - - public EmacsGC - immutableGC () - { - if (immutableGC == null || dirty) - { - immutableGC = new EmacsGC (this); - dirty = false; - } - - return immutableGC; - }; }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index c80339031a8..ae48ce30408 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -79,6 +79,28 @@ public class EmacsNative /* Send an ANDROID_WINDOW_ACTION event. */ public static native void sendWindowAction (short window, int action); + /* Send an ANDROID_ENTER_NOTIFY event. */ + public static native void sendEnterNotify (short window, int x, int y, + long time); + + /* Send an ANDROID_LEAVE_NOTIFY event. */ + public static native void sendLeaveNotify (short window, int x, int y, + long time); + + /* Send an ANDROID_MOTION_NOTIFY event. */ + public static native void sendMotionNotify (short window, int x, int y, + long time); + + /* Send an ANDROID_BUTTON_PRESS event. */ + public static native void sendButtonPress (short window, int x, int y, + long time, int state, + int button); + + /* Send an ANDROID_BUTTON_RELEASE event. */ + public static native void sendButtonRelease (short window, int x, int y, + long time, int state, + int button); + static { System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index 897902c5b57..15452f007c4 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -93,6 +93,8 @@ public class EmacsPixmap extends EmacsHandleObject break; } + bitmap.eraseColor (0xff000000); + this.width = width; this.height = height; this.depth = depth; @@ -108,13 +110,6 @@ public class EmacsPixmap extends EmacsHandleObject return canvas; } - @Override - public void - unlockCanvas () - { - - } - @Override public void damageRect (Rect damageRect) diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java index 34e2b1803a2..11e128d5769 100644 --- a/java/org/gnu/emacs/EmacsSdk23FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java @@ -20,9 +20,80 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import android.graphics.Paint; +import android.graphics.Rect; public class EmacsSdk23FontDriver extends EmacsSdk7FontDriver { + private void + textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics, + Paint paint, Rect bounds) + { + char[] text; + + text = new char[2]; + text[0] = (char) code; + text[1] = 'c'; + + paint.getTextBounds (text, 0, 1, bounds); + + metrics.lbearing = (short) bounds.left; + metrics.rbearing = (short) bounds.right; + metrics.ascent = (short) -bounds.top; + metrics.descent = (short) bounds.bottom; + metrics.width + = (short) paint.getRunAdvance (text, 0, 1, 0, 1, false, 1); + } + + @Override + public void + textExtents (FontObject font, int code[], FontMetrics fontMetrics) + { + int i; + Paint paintCache; + Rect boundsCache; + Sdk7FontObject fontObject; + char[] text; + float width; + + fontObject = (Sdk7FontObject) font; + paintCache = fontObject.typeface.typefacePaint; + paintCache.setTextSize (fontObject.pixelSize); + boundsCache = new Rect (); + + if (code.length == 0) + { + fontMetrics.lbearing = 0; + fontMetrics.rbearing = 0; + fontMetrics.ascent = 0; + fontMetrics.descent = 0; + fontMetrics.width = 0; + } + else if (code.length == 1) + textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics, + paintCache, boundsCache); + else + { + text = new char[code.length + 1]; + + for (i = 0; i < code.length; ++i) + text[i] = (char) code[i]; + + text[code.length] = 'c'; + + paintCache.getTextBounds (text, 0, code.length, + boundsCache); + width = paintCache.getRunAdvance (text, 0, code.length, 0, + code.length, + false, code.length); + + fontMetrics.lbearing = (short) boundsCache.left; + fontMetrics.rbearing = (short) boundsCache.right; + fontMetrics.ascent = (short) -boundsCache.top; + fontMetrics.descent = (short) boundsCache.bottom; + fontMetrics.width = (short) width; + } + } + @Override public int hasChar (FontSpec font, char charCode) diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index 437f38e62db..8a9426050ae 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -243,93 +243,6 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } }; - private class Sdk7DrawString implements EmacsPaintReq - { - private boolean drawBackground; - private Sdk7FontObject fontObject; - private char[] chars; - private EmacsGC immutableGC; - private EmacsDrawable drawable; - private Rect rect; - private int originX, originY; - - public - Sdk7DrawString (Sdk7FontObject fontObject, char[] chars, - EmacsGC immutableGC, EmacsDrawable drawable, - boolean drawBackground, Rect rect, - int originX, int originY) - { - this.fontObject = fontObject; - this.chars = chars; - this.immutableGC = immutableGC; - this.drawable = drawable; - this.drawBackground = drawBackground; - this.rect = rect; - this.originX = originX; - this.originY = originY; - } - - @Override - public EmacsDrawable - getDrawable () - { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) - { - int scratch; - - paint.setStyle (Paint.Style.FILL); - - if (drawBackground) - { - paint.setColor (immutableGC.background | 0xff000000); - canvas.drawRect (rect, paint); - } - - paint.setTextSize (fontObject.pixelSize); - paint.setColor (immutableGC.foreground | 0xff000000); - paint.setTypeface (fontObject.typeface.typeface); - paint.setAntiAlias (true); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) - /* Disable hinting as that leads to displayed text not - matching the computed metrics. */ - paint.setHinting (Paint.HINTING_OFF); - - canvas.drawText (chars, 0, chars.length, originX, originY, paint); - paint.setAntiAlias (false); - } - - @Override - public Rect - getRect () - { - Rect rect; - - rect = new Rect (); - - fontObject.typeface.typefacePaint.setTextSize (fontObject.pixelSize); - fontObject.typeface.typefacePaint.getTextBounds (chars, 0, chars.length, - rect); - - /* Add the background rect to the damage as well. */ - rect.union (this.rect); - - return rect; - } - }; - private String[] fontFamilyList; private Sdk7Typeface[] typefaceList; private Sdk7Typeface fallbackTypeface; @@ -498,14 +411,11 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver Paint paint, Rect bounds) { char[] text; - float[] width; text = new char[1]; text[0] = (char) code; paint.getTextBounds (text, 0, 1, bounds); - width = new float[1]; - paint.getTextWidths (text, 0, 1, width); /* bounds is the bounding box of the glyph corresponding to CODE. Translate these into XCharStruct values. @@ -526,7 +436,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver metrics.rbearing = (short) bounds.right; metrics.ascent = (short) -bounds.top; metrics.descent = (short) bounds.bottom; - metrics.width = (short) Math.round (width[0]); + metrics.width = (short) paint.measureText ("" + text[0]); } @Override @@ -563,7 +473,8 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver for (i = 0; i < code.length; ++i) text[i] = (char) code[i]; - paintCache.getTextBounds (text, 0, 1, boundsCache); + paintCache.getTextBounds (text, 0, code.length, + boundsCache); width = paintCache.measureText (text, 0, code.length); fontMetrics.lbearing = (short) boundsCache.left; @@ -587,11 +498,12 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver int[] chars, int x, int y, int backgroundWidth, boolean withBackground) { - Rect backgroundRect; + Rect backgroundRect, bounds; Sdk7FontObject sdk7FontObject; - Sdk7DrawString op; char[] charsArray; int i; + Canvas canvas; + Paint paint; sdk7FontObject = (Sdk7FontObject) fontObject; charsArray = new char[chars.length]; @@ -605,12 +517,41 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver backgroundRect.right = x + backgroundWidth; backgroundRect.bottom = y + sdk7FontObject.descent; - op = new Sdk7DrawString (sdk7FontObject, charsArray, - gc.immutableGC (), drawable, - withBackground, - backgroundRect, x, y); + canvas = drawable.lockCanvas (); + + if (canvas == null) + return 0; + + canvas.save (); + paint = gc.gcPaint; + + if (gc.real_clip_rects != null) + { + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); + } + + paint.setStyle (Paint.Style.FILL); + + if (withBackground) + { + paint.setColor (gc.background | 0xff000000); + canvas.drawRect (backgroundRect, paint); + paint.setColor (gc.foreground | 0xff000000); + } - EmacsService.SERVICE.appendPaintOperation (op); + paint.setTextSize (sdk7FontObject.pixelSize); + paint.setTypeface (sdk7FontObject.typeface.typeface); + paint.setAntiAlias (true); + canvas.drawText (charsArray, 0, chars.length, x, y, paint); + + canvas.restore (); + bounds = new Rect (); + paint.getTextBounds (charsArray, 0, chars.length, bounds); + bounds.offset (x, y); + bounds.union (backgroundRect); + drawable.damageRect (bounds); + paint.setAntiAlias (false); return 1; } }; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 41a45b0bd85..4444b7f1c56 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -19,7 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.lang.Runnable; import java.io.IOException; import java.util.List; import java.util.ArrayList; @@ -59,7 +58,6 @@ public class EmacsService extends Service private EmacsThread thread; private Handler handler; - private EmacsPaintQueue paintQueue; /* Display metrics used by font backends. */ public DisplayMetrics metrics; @@ -191,126 +189,42 @@ public class EmacsService extends Service return view.thing; } - /* X drawing operations. These are quite primitive operations. The - drawing queue is kept on the Emacs thread, but is periodically - flushed to the application thread, upon buffers swaps and once it - gets too big. */ - - - - private void - ensurePaintQueue () - { - if (paintQueue == null) - paintQueue = new EmacsPaintQueue (); - } - - public void - flushPaintQueue () - { - final EmacsPaintQueue queue; - - if (paintQueue == null) - return; - - if (paintQueue.numRequests < 1) - /* No requests to flush. */ - return; - - queue = paintQueue; - - handler.post (new Runnable () { - @Override - public void - run () - { - queue.run (); - } - }); - - /* Clear the paint queue. */ - paintQueue = null; - } - - private void - checkFlush () - { - if (paintQueue != null - && paintQueue.numRequests > MAX_PENDING_REQUESTS) - flushPaintQueue (); - } - public void fillRectangle (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsFillRectangle (drawable, x, y, - width, height, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsFillRectangle.perform (drawable, gc, x, y, + width, height); } public void fillPolygon (EmacsDrawable drawable, EmacsGC gc, Point points[]) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsFillPolygon (drawable, points, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsFillPolygon.perform (drawable, gc, points); } public void drawRectangle (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsDrawRectangle (drawable, x, y, - width, height, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsDrawRectangle.perform (drawable, gc, x, y, + width, height); } public void drawLine (EmacsDrawable drawable, EmacsGC gc, int x, int y, int x2, int y2) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsDrawLine (drawable, x, y, - x2, y2, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsDrawLine.perform (drawable, gc, x, y, + x2, y2); } public void drawPoint (EmacsDrawable drawable, EmacsGC gc, int x, int y) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsDrawPoint (drawable, x, y, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsDrawPoint.perform (drawable, gc, x, y); } public void @@ -319,15 +233,9 @@ public class EmacsService extends Service int srcX, int srcY, int width, int height, int destX, int destY) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsCopyArea (srcDrawable, dstDrawable, - srcX, srcY, width, height, destX, - destY, gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsCopyArea.perform (srcDrawable, gc, dstDrawable, + srcX, srcY, width, height, destX, + destY); } public void @@ -342,12 +250,4 @@ public class EmacsService extends Service { window.clearArea (x, y, width, height); } - - public void - appendPaintOperation (EmacsPaintReq op) - { - ensurePaintQueue (); - paintQueue.appendPaintOperation (op); - checkFlush (); - } }; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index b8b828e4820..5efb8882263 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -29,6 +29,7 @@ import android.graphics.Rect; public class EmacsSurfaceView extends SurfaceView { + public Object surfaceChangeLock; private boolean created; public @@ -36,33 +37,36 @@ public class EmacsSurfaceView extends SurfaceView { super (view.getContext ()); + surfaceChangeLock = new Object (); + getHolder ().addCallback (new SurfaceHolder.Callback () { @Override public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) { - /* Force a buffer swap now to get the contents of the Emacs - view on screen. */ - view.swapBuffers (true); + view.swapBuffers (); } @Override public void surfaceCreated (SurfaceHolder holder) { - created = true; - - /* Force a buffer swap now to get the contents of the Emacs - view on screen. */ - view.swapBuffers (true); + synchronized (surfaceChangeLock) + { + created = true; + view.swapBuffers (); + } } @Override public void surfaceDestroyed (SurfaceHolder holder) { - created = false; + synchronized (surfaceChangeLock) + { + created = false; + } } }); } diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 7b48eaf0aa6..169a1e42ee3 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -23,6 +23,7 @@ import android.content.res.ColorStateList; import android.view.View; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.ViewGroup; import android.graphics.Bitmap; @@ -64,6 +65,14 @@ public class EmacsView extends ViewGroup /* The associated surface view. */ private EmacsSurfaceView surfaceView; + /* Whether or not a configure event must be sent for the next layout + event regardless of what changed. */ + public boolean mustReportLayout; + + /* If non-null, whether or not bitmaps must be recreated upon the + next call to getBitmap. */ + private Rect bitmapDirty; + public EmacsView (EmacsWindow window) { @@ -81,6 +90,43 @@ public class EmacsView extends ViewGroup addView (this.surfaceView); } + private void + handleDirtyBitmap () + { + /* Recreate the front and back buffer bitmaps. */ + bitmap + = Bitmap.createBitmap (bitmapDirty.width (), + bitmapDirty.height (), + Bitmap.Config.ARGB_8888); + bitmap.eraseColor (0xffffffff); + + /* And canvases. */ + canvas = new Canvas (bitmap); + + /* If Emacs is drawing to the bitmap right now from the + main thread, the image contents are lost until the next + ConfigureNotify and complete garbage. Sorry! */ + bitmapDirty = null; + } + + public synchronized Bitmap + getBitmap () + { + if (bitmapDirty != null) + handleDirtyBitmap (); + + return bitmap; + } + + public synchronized Canvas + getCanvas () + { + if (bitmapDirty != null) + handleDirtyBitmap (); + + return canvas; + } + @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) @@ -114,7 +160,7 @@ public class EmacsView extends ViewGroup } @Override - protected void + protected synchronized void onLayout (boolean changed, int left, int top, int right, int bottom) { @@ -122,19 +168,15 @@ public class EmacsView extends ViewGroup View child; Rect windowRect; - if (changed) + if (changed || mustReportLayout) { + mustReportLayout = false; window.viewLayout (left, top, right, bottom); - - /* Recreate the front and back buffer bitmaps. */ - bitmap - = Bitmap.createBitmap (right - left, bottom - top, - Bitmap.Config.ARGB_8888); - - /* And canvases. */ - canvas = new Canvas (bitmap); } + if (changed) + bitmapDirty = new Rect (left, top, right, bottom); + count = getChildCount (); for (i = 0; i < count; ++i) @@ -159,47 +201,60 @@ public class EmacsView extends ViewGroup } } - public void + public synchronized void damageRect (Rect damageRect) { damageRegion.union (damageRect); } - public void + /* This method is called from both the UI thread and the Emacs + thread. */ + + public synchronized void swapBuffers (boolean force) { - Bitmap back; Canvas canvas; Rect damageRect; + Bitmap bitmap; if (damageRegion.isEmpty ()) return; - if (!surfaceView.isCreated ()) - return; + bitmap = getBitmap (); - if (bitmap == null) - return; + /* Emacs must take the following lock to ensure the access to the + canvas occurs with the surface created. Otherwise, Android + will throttle calls to lockCanvas. */ + + synchronized (surfaceView.surfaceChangeLock) + { + damageRect = damageRegion.getBounds (); - /* Lock the canvas with the specified damage. */ - damageRect = damageRegion.getBounds (); - canvas = surfaceView.lockCanvas (damageRect); + if (!surfaceView.isCreated ()) + return; - /* Return if locking the canvas failed. */ - if (canvas == null) - return; + if (bitmap == null) + return; + + /* Lock the canvas with the specified damage. */ + canvas = surfaceView.lockCanvas (damageRect); - /* Copy from the back buffer to the canvas. If damageRect was - made empty, then draw the entire back buffer. */ + /* Return if locking the canvas failed. */ + if (canvas == null) + return; - if (damageRect.isEmpty ()) - canvas.drawBitmap (bitmap, 0f, 0f, paint); - else - canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + /* Copy from the back buffer to the canvas. If damageRect was + made empty, then draw the entire back buffer. */ - /* Unlock the canvas and clear the damage. */ - surfaceView.unlockCanvasAndPost (canvas); - damageRegion.setEmpty (); + if (damageRect.isEmpty ()) + canvas.drawBitmap (bitmap, 0f, 0f, paint); + else + canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + + /* Unlock the canvas and clear the damage. */ + surfaceView.unlockCanvasAndPost (canvas); + damageRegion.setEmpty (); + } } public void @@ -241,4 +296,18 @@ public class EmacsView extends ViewGroup super.onFocusChanged (gainFocus, direction, previouslyFocusedRect); } + + @Override + public boolean + onGenericMotionEvent (MotionEvent motion) + { + return window.onSomeKindOfMotionEvent (motion); + } + + @Override + public boolean + onTouchEvent (MotionEvent motion) + { + return window.onSomeKindOfMotionEvent (motion); + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 26e788a20a8..a9a24b61869 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -31,8 +31,13 @@ import android.graphics.Point; import android.view.View; import android.view.ViewGroup; import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.InputDevice; import android.content.Intent; +import android.util.Log; + +import android.os.Build; /* This defines a window, which is a handle. Windows represent a rectangular subset of the screen with their own contents. @@ -68,6 +73,10 @@ public class EmacsWindow extends EmacsHandleObject window background. */ private EmacsGC scratchGC; + /* The button state and keyboard modifier mask at the time of the + last button press or release event. */ + private int lastButtonState, lastModifiers; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height) @@ -212,6 +221,7 @@ public class EmacsWindow extends EmacsHandleObject public void run () { + view.mustReportLayout = true; view.requestLayout (); } }); @@ -224,6 +234,8 @@ public class EmacsWindow extends EmacsHandleObject { rect.right = rect.left + width; rect.bottom = rect.top + height; + + requestViewLayout (); } } @@ -279,17 +291,7 @@ public class EmacsWindow extends EmacsHandleObject public Canvas lockCanvas () { - if (view.canvas != null) - return view.canvas; - - return null; - } - - @Override - public void - unlockCanvas () - { - + return view.getCanvas (); } @Override @@ -302,17 +304,7 @@ public class EmacsWindow extends EmacsHandleObject public void swapBuffers () { - /* Before calling swapBuffers, make sure to flush the paint - queue. */ - EmacsService.SERVICE.flushPaintQueue (); - view.post (new Runnable () { - @Override - public void - run () - { - view.swapBuffers (); - } - }); + view.swapBuffers (); } public void @@ -337,7 +329,7 @@ public class EmacsWindow extends EmacsHandleObject public Bitmap getBitmap () { - return view.bitmap; + return view.getBitmap (); } public void @@ -357,6 +349,7 @@ public class EmacsWindow extends EmacsHandleObject recognized as an ASCII key press event. */ event.getUnicodeChar (state)); + lastModifiers = event.getModifiers (); } public void @@ -372,6 +365,7 @@ public class EmacsWindow extends EmacsHandleObject event.getModifiers (), keyCode, event.getUnicodeChar (state)); + lastModifiers = event.getModifiers (); } public void @@ -386,4 +380,105 @@ public class EmacsWindow extends EmacsHandleObject /* Destroy the associated frame when the activity is detached. */ EmacsNative.sendWindowAction (this.handle, 0); } + + /* Look through the button state to determine what button EVENT was + generated from. DOWN is true if EVENT is a button press event, + false otherwise. Value is the X number of the button. */ + + private int + whatButtonWasIt (MotionEvent event, boolean down) + { + int eventState, notIn; + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + /* Earlier versions of Android only support one mouse + button. */ + return 1; + + eventState = event.getButtonState (); + notIn = (down ? eventState & ~lastButtonState + : lastButtonState & ~eventState); + + if ((notIn & MotionEvent.BUTTON_PRIMARY) != 0) + return 1; + + if ((notIn & MotionEvent.BUTTON_SECONDARY) != 0) + return 3; + + if ((notIn & MotionEvent.BUTTON_TERTIARY) != 0) + return 2; + + /* Not a real value. */ + return 4; + } + + public boolean + onSomeKindOfMotionEvent (MotionEvent event) + { + if (!event.isFromSource (InputDevice.SOURCE_CLASS_POINTER)) + return false; + + switch (event.getAction ()) + { + case MotionEvent.ACTION_HOVER_ENTER: + EmacsNative.sendEnterNotify (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime ()); + return true; + + case MotionEvent.ACTION_MOVE: + case MotionEvent.ACTION_HOVER_MOVE: + EmacsNative.sendMotionNotify (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime ()); + return true; + + case MotionEvent.ACTION_HOVER_EXIT: + EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime ()); + return true; + + case MotionEvent.ACTION_BUTTON_PRESS: + /* Find the button which was pressed. */ + EmacsNative.sendButtonPress (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime (), + lastModifiers, + whatButtonWasIt (event, true)); + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + return true; + + lastButtonState = event.getButtonState (); + return true; + + case MotionEvent.ACTION_BUTTON_RELEASE: + /* Find the button which was released. */ + EmacsNative.sendButtonRelease (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime (), + lastModifiers, + whatButtonWasIt (event, false)); + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + return true; + + lastButtonState = event.getButtonState (); + return true; + + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_UP: + /* Emacs must return true even though touch events are not yet + handled, because the value of this function is used by the + system to decide whether or not Emacs gets ACTION_MOVE + events. */ + return true; + } + + return false; + } }; -- cgit v1.3 From f9732131cf3c67e24db74a3d49f256d3c189a7e3 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 13 Jan 2023 15:53:08 +0800 Subject: Update Android port * configure.ac (ANDROID_MIN_SDK): New variable. (DX): Remove and replace with D8. (XCONFIGURE): Check for the minimum version of Android the cross compiler compiles for. Generate java/AndroidManifest.xml from java/AndroidManifest.xml.in. Allow using Zlib on Android. * java/AndroidManifest.xml.in: New file. Use the minimum SDK detected by configure. * java/Makefile.in (top_srcdir, version): New variables. (DX, D8): Replace with D8. (ANDROID_MIN_SDK, APK_NAME): New variables. (.PHONY): (.PRECIOUS): (classes.dex): (emacs.apk): Generate $(APK_NAME) instead of `emacs.apk'. * java/debug.sh: New option --attach-existing. Attach to an existing Emacs instance when specified. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): New field `isPaused'. (invalidateFocus1): Fix infinite recursion. (detachWindow): Deiconify window. (attachWindow): Iconify the window if the activity is paused. (onCreate): Use the ``no title bar'' theme. (onPause, onResume): New functions. * java/org/gnu/emacs/EmacsNative.java (sendTouchUp, sendTouchDown) (sendTouchMove, sendWheel, sendIconified, sendDeiconified): New functions. * java/org/gnu/emacs/EmacsSdk7FontDriver.java (Sdk7Typeface): (list): Remove logging for code that is mostly going to be unused. * java/org/gnu/emacs/EmacsService.java (ringBell, queryTree) (getScreenWidth, getScreenHeight, detectMouse): New functions. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView) (surfaceChanged, surfaceCreated, surfaceDestroyed): Add extra debug logging. Avoid deadlock in surfaceCreated. * java/org/gnu/emacs/EmacsView.java (EmacsView): Try very hard to make the SurfaceView respect Z order. It didn't work. (handleDirtyBitmap): Copy over the contents from the old bitmap. (explicitlyDirtyBitmap): New function. (onLayout): Don't dirty bitmap if unnecessary. (damageRect, swapBuffers): Don't synchronize so hard. (onTouchEvent): Call window.onTouchEvent instead. (moveChildToBack, raise, lower): New functions. * java/org/gnu/emacs/EmacsWindow.java (Coordinate): New subclass. (pointerMap, isMapped, isIconified, dontFocusOnMap) (dontAcceptFocus): New fields. (EmacsWindow): Don't immediately register unmapped window. (viewLayout): Send configure event outside the lock. (requestViewLayout): Explicitly dirty the bitmap. (mapWindow): Register the window now. Respect dontFocusOnMap. (unmapWindow): Unregister the window now. (figureChange, onTouchEvent): New functions. (onSomeKindOfMotionEvent): Handle scroll wheel events. (reparentTo, makeInputFocus, raise, lower, getWindowGeometry) (noticeIconified, noticeDeiconified, setDontAcceptFocus) (setDontFocusOnMap, getDontFocusOnMap): New functions. * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (registerWindow, detachWindow): Synchronize. (noticeIconified, noticeDeiconified): New functions. (copyWindows): New function. * lisp/frame.el (frame-geometry, frame-edges) (mouse-absolute-pixel-position, set-mouse-absolute-pixel-position) (frame-list-z-order, frame-restack, display-mouse-p) (display-monitor-attributes-list): Implement on Android. * lisp/mwheel.el (mouse-wheel-down-event): (mouse-wheel-up-event): (mouse-wheel-left-event): (mouse-wheel-right-event): Define on Android. * src/android.c (struct android_emacs_service): New methods `ringBell', `queryTree', `getScreenWidth', `getScreenHeight', and `detectMouse'. (struct android_event_queue, android_init_events) (android_next_event, android_write_event): Remove write limit. (android_file_access_p): Handle directories correcty. (android_close): Fix coding style. (android_fclose): New function. (android_init_emacs_service): Initialize new methods. (android_reparent_window): Implement function. (android_bell, android_set_input_focus, android_raise_window) (android_lower_window, android_query_tree, android_get_geometry) (android_get_screen_width, android_get_screen_height) (android_get_mm_width, android_get_mm_height, android_detect_mouse) (android_set_dont_focus_on_map, android_set_dont_accept_focus): New functions. (struct android_dir): New structure. (android_opendir, android_readdir, android_closedir): New functions. (emacs_abort): Implement here on Android and poke debuggerd into generating a tombstone. * src/android.h: Update prototypes. * src/androidfns.c (android_set_parent_frame): New function. (android_default_font_parameter): Use sane font size by default. (Fx_display_pixel_width, Fx_display_pixel_height) (Fx_display_mm_width, Fx_display_mm_height) (Fx_display_monitor_attributes_list): Rename to start with `android-'. Implement. Fiddle with documentation to introduce Android specific nuances. (Fandroid_display_monitor_attributes_list): New function. (Fx_frame_geometry, frame_geometry): New function. (Fandroid_frame_geometry): Implement correctly. (Fx_frame_list_z_order): Rename to start with `android-'. (android_frame_list_z_order, Fandroid_frame_list_z_order): Implement. (Fx_frame_restack): Rename to start with `android-'. (Fandroid_frame_restack): ``Implement''. (Fx_mouse_absolute_pixel_position): Rename to start with `android-'. (Fandroid_mouse_absolute_pixel_position): ``Implement''. (Fx_set_mouse_absolute_pixel_position): Rename to start with `android-'. (Fandroid_set_mouse_absolute_pixel_position): ``Implement''. (Fandroid_detect_mouse): New function. (android_set_menu_bar_lines): Use FRAME_ANDROID_DRAWABLE when clearing area. (android_set_no_focus_on_map, android_set_no_accept_focus): New functions. (android_frame_parm_handlers): Register new frame parameter handlers. (syms_of_androidfns): Update appropriately. * src/androidfont.c (androidfont_draw): Use FRAME_ANDROID_DRAWABLE instead of FRAME_ANDROID_WINDOW. * src/androidgui.h (enum android_event_type): New events. (struct android_touch_event, struct android_wheel_event) (struct android_iconify_event): New structures. (union android_event): Add new events. * src/androidterm.c (android_clear_frame): Use FRAME_ANDROID_DRAWABLE instead of FRAME_ANDROID_WINDOW. (android_flash, android_ring_bell): Implement bell ringing. (android_toggle_invisible_pointer): Don't TODO function that can't be implemented. (show_back_buffer, android_flush_dirty_back_buffer_on): Check if a buffer flip is required before doing the flip. (android_lower_frame, android_raise_frame): Implement functions. (android_update_tools, android_find_tool): New functions. (handle_one_android_event): Handle new iconification, wheel and touch events. (android_read_socket): Implement pending-autoraise-frames. (android_frame_up_to_date): Implement bell ringing. (android_buffer_flipping_unblocked_hook): Check if a buffer flip is required before doing the flip. (android_focus_frame, android_frame_highlight) (android_frame_unhighlight): New function. (android_frame_rehighlight): Implement functions. (android_iconify_frame): Always display error. (android_set_alpha): Update commentary. (android_free_frame_resources): Free frame touch points. (android_scroll_run, android_flip_and_flush) (android_clear_rectangle, android_draw_fringe_bitmap) (android_draw_glyph_string_background, android_fill_triangle) (android_clear_point, android_draw_relief_rect) (android_draw_box_rect, android_draw_glyph_string_bg_rect) (android_draw_image_foreground, android_draw_stretch_glyph_string) (android_draw_underwave, android_draw_glyph_string_foreground) (android_draw_composite_glyph_string_foreground) (android_draw_glyphless_glyph_string_foreground) (android_draw_glyph_string, android_clear_frame_area) (android_clear_under_internal_border, android_draw_hollow_cursor) (android_draw_bar_cursor, android_draw_vertical_window_border) (android_draw_window_divider): Use FRAME_ANDROID_DRAWABLE instead of FRAME_ANDROID_WINDOW for drawing operations. * src/androidterm.h (struct android_touch_point): New structure. (struct android_output): New fields. (FRAME_ANDROID_NEED_BUFFER_FLIP): New macro. * src/dired.c (emacs_readdir, open_directory) (directory_files_internal_unwind, read_dirent) (directory_files_internal, file_name_completion): Add indirection over readdir and opendir. Use android variants on Android. * src/dispnew.c (Fopen_termscript): * src/fileio.c (fclose_unwind): Use emacs_fclose. (Faccess_file): Call android_file_access_p. (file_accessible_directory_p): Append right suffix to Android assets directory. (do_auto_save_unwind): Use emacs_fclose. * src/keyboard.c (lispy_function_keys): Use right function key for page up and page down. (Fopen_dribble_file): Use emacs_fclose. * src/lisp.h: New prototype emacs_fclose. * src/lread.c (close_infile_unwind): Use emacs_fclose. * src/sfnt.c (sfnt_curve_is_flat): Fix area-squared computation. (sfnt_prepare_raster): Compute raster width and height consistently with outline building. (sfnt_build_outline_edges): Use the same offsets used to set offy and offx. (main): Adjust debug code. * src/sfntfont-android.c (sfntfont_android_saturate32): Delete function. (sfntfont_android_blend, sfntfont_android_blendrgb): Remove unnecessary debug code. (sfntfont_android_composite_bitmap): Prevent out of bounds write. (sfntfont_android_put_glyphs): Use FRAME_ANDROID_DRAWABLE. (init_sfntfont_android): Initialize Monospace Serif font to something sensible. * src/sfntfont.c (sfntfont_text_extents): Clear glyph metrics before summing up pcm. (sfntfont_draw): Use s->font instead of s->face->font. * src/sysdep.c (emacs_fclose): Wrap around android_fclose on android. * src/term.c (Fsuspend_tty): (delete_tty): Use emacs_fclose. * src/verbose.mk.in (AM_V_DX): Replace with D8 version. --- configure.ac | 35 +- java/AndroidManifest.xml.in | 84 +++ java/Makefile.in | 30 +- java/debug.sh | 45 +- java/org/gnu/emacs/EmacsActivity.java | 37 +- java/org/gnu/emacs/EmacsNative.java | 25 +- java/org/gnu/emacs/EmacsSdk7FontDriver.java | 7 - java/org/gnu/emacs/EmacsService.java | 116 ++++ java/org/gnu/emacs/EmacsSurfaceView.java | 22 +- java/org/gnu/emacs/EmacsView.java | 189 +++++-- java/org/gnu/emacs/EmacsWindow.java | 498 +++++++++++++++-- .../gnu/emacs/EmacsWindowAttachmentManager.java | 54 +- lisp/frame.el | 32 +- lisp/mwheel.el | 12 +- src/android.c | 551 ++++++++++++++++++- src/android.h | 23 + src/androidfns.c | 471 ++++++++++++++-- src/androidfont.c | 8 +- src/androidgui.h | 83 +++ src/androidterm.c | 589 ++++++++++++++++++--- src/androidterm.h | 28 + src/dired.c | 50 +- src/dispnew.c | 3 +- src/fileio.c | 42 +- src/keyboard.c | 6 +- src/lisp.h | 2 + src/lread.c | 2 +- src/sfnt.c | 35 +- src/sfntfont-android.c | 41 +- src/sfntfont.c | 5 +- src/sysdep.c | 17 +- src/term.c | 10 +- src/verbose.mk.in | 2 +- 33 files changed, 2809 insertions(+), 345 deletions(-) create mode 100644 java/AndroidManifest.xml.in (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/configure.ac b/configure.ac index 68de5f02b97..333a28cc142 100644 --- a/configure.ac +++ b/configure.ac @@ -816,8 +816,8 @@ a valid path to android.jar. See config.log for more details.]) Please verify that the path to the SDK build tools you specified is correct]) fi - AC_PATH_PROGS([DX], [dx d8], [], "${SDK_BUILD_TOOLS}:$PATH") - if test "DX" = ""; then + AC_PATH_PROGS([D8], [d8], [], "${SDK_BUILD_TOOLS}:$PATH") + if test "D8" = ""; then AC_MSG_ERROR([The Android dexer was not found. Please verify that the path to the SDK build tools you specified is correct]) fi @@ -867,6 +867,25 @@ in the ANDROID_CC variable when you ran configure.]) ANDROID_ABI=$android_abi + dnl Obtain the minimum SDK version of the resulting Emacs binary + dnl built with this NDK. + + ANDROID_MIN_SDK=8 + AC_MSG_CHECKING([for the lowest Android version Emacs can run on]) + [android_sdk=`echo "$cc_target" | grep -oE 'android([0-9][0-9]?)'`] + + if test -n "$android_sdk"; then + android_sdk=`echo "$android_sdk" | sed -n 's/android//p'` + AC_MSG_RESULT([$android_sdk]) + ANDROID_MIN_SDK=$android_sdk + else + AC_MSG_RESULT([unknown ($cc_target); assuming 8]) + AC_MSG_WARN([configure could not determine the versions of Android \ +a binary built with this compiler will run on. The generated application \ +package will likely install on older systems but crash on startup.]) + fi + AC_SUBST([ANDROID_MIN_SDK]) + # Save confdefs.h and config.log for now. mv -f confdefs.h _confdefs.h mv -f config.log _config.log @@ -899,7 +918,7 @@ fi AC_SUBST([ANDROID]) AC_SUBST([JAVAC]) AC_SUBST([AAPT]) -AC_SUBST([DX]) +AC_SUBST([D8]) AC_SUBST([ZIPALIGN]) AC_SUBST([ANDROID_JAR]) AC_SUBST([ANDROID_ABI]) @@ -914,7 +933,7 @@ fi AC_SUBST([XCONFIGURE]) if test "$ANDROID" = "yes"; then - # When --with-android is specified, all build options must be + # When --with-android is specified, almost all build options must be # disabled, both within the recursive invocation of configure and # outside. with_xpm=no @@ -938,11 +957,12 @@ if test "$ANDROID" = "yes"; then with_gpm=no with_dbus=no with_gsettings=no - with_selinx=no + with_selinux=no with_gnutls=no - with_zlib=no with_modules=no with_threads=no + + # zlib is available in android. fi dnl This used to use changequote, but, apart from 'changequote is evil' @@ -7211,6 +7231,9 @@ fi ARCH_INDEPENDENT_CONFIG_FILES([java/Makefile]) ARCH_INDEPENDENT_CONFIG_FILES([xcompile/Makefile]) +# Make java/AndroidManifest.xml +ARCH_INDEPENDENT_CONFIG_FILES([java/AndroidManifest.xml]) + AC_OUTPUT if test ! "$with_mailutils"; then diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in new file mode 100644 index 00000000000..b680137a9d0 --- /dev/null +++ b/java/AndroidManifest.xml.in @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/Makefile.in b/java/Makefile.in index 31bd22b5a2e..05e61dede89 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -18,13 +18,15 @@ # along with GNU Emacs. If not, see . top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +version = @version@ -include ${top_builddir}/src/verbose.mk SHELL = @SHELL@ JAVAC = @JAVAC@ AAPT = @AAPT@ -DX = @DX@ +D8 = @D8@ ZIPALIGN = @ZIPALIGN@ JARSIGNER = @JARSIGNER@ ANDROID_JAR = @ANDROID_JAR@ @@ -39,6 +41,12 @@ SIGN_EMACS = -keystore emacs.keystore -storepass emacs1 JAVA_FILES = $(shell find . -type f -name *.java) CLASS_FILES = $(foreach file,$(JAVA_FILES),$(basename $(file)).class) +# Compute the name for the Emacs application package. This should be: +# emacs---.apk + +ANDROID_MIN_SDK = @ANDROID_MIN_SDK@ +APK_NAME = emacs-$(version)-$(ANDROID_MIN_SDK)-$(ANDROID_ABI).apk + # How this stuff works. # emacs.apk depends on emacs.apk-in, which is simply a ZIP archive @@ -55,7 +63,7 @@ CLASS_FILES = $(foreach file,$(JAVA_FILES),$(basename $(file)).class) # assets/lisp/ .PHONY: emacs.apk-in all -all: emacs.apk +all: $(APK_NAME) # Binaries to cross-compile. CROSS_BINS = ../xcompile/src/android-emacs ../xcompile/lib-src/ctags \ @@ -118,6 +126,18 @@ emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) AndroidManifest.xml pushd install_temp; $(AAPT) add ../$@ `find assets -type f`; popd rm -rf install_temp +# Makefile itself. +.PRECIOUS: ../config.status Makefile +../config.status: $(top_srcdir)/configure.ac $(top_srcdir)/m4/*.m4 + $(MAKE) -C $(dir $@) $(notdir $@) +Makefile: ../config.status $(top_builddir)/java/Makefile.in + $(MAKE) -C .. java/$@ + +# AndroidManifest.xml: +AndroidManifest.xml: $(top_srcdir)/configure.ac $(top_srcdir)/m4/*.m4 \ + AndroidManifest.xml.in + pushd ..; ./config.status java/AndroidManifest.xml; popd + .SUFFIXES: .java .class .java.class &: $(AM_V_JAVAC) $(JAVAC) $(JAVAFLAGS) $< @@ -126,7 +146,7 @@ emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) AndroidManifest.xml # nested classes. classes.dex: $(CLASS_FILES) - $(AM_V_DX) $(DX) --classpath $(ANDROID_JAR) \ + $(AM_V_D8) $(D8) --classpath $(ANDROID_JAR) \ $(subst $$,\$$,$(shell find . -type f -name *.class)) # When emacs.keystore expires, regenerate it with: @@ -136,7 +156,7 @@ classes.dex: $(CLASS_FILES) .PHONY: clean maintainer-clean -emacs.apk: classes.dex emacs.apk-in emacs.keystore +$(APK_NAME): classes.dex emacs.apk-in emacs.keystore cp -f emacs.apk-in $@.unaligned $(AAPT) add $@.unaligned classes.dex $(JARSIGNER) $(SIGN_EMACS) $@.unaligned "Emacs keystore" @@ -144,7 +164,7 @@ emacs.apk: classes.dex emacs.apk-in emacs.keystore rm -f $@.unaligned clean: - rm -f emacs.apk emacs.apk-in *.dex *.unaligned *.class + rm -f *.apk emacs.apk-in *.dex *.unaligned *.class rm -rf install-temp find . -name '*.class' -delete diff --git a/java/debug.sh b/java/debug.sh index dd710dc31af..3e3e3d9c281 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -30,6 +30,7 @@ activity=org.gnu.emacs.EmacsActivity gdb_port=5039 jdb_port=64013 jdb=no +attach_existing=no while [ $# -gt 0 ]; do case "$1" in @@ -48,6 +49,7 @@ while [ $# -gt 0 ]; do echo " --port PORT run the GDB server on a specific port" echo " --jdb-port PORT run the JDB server on a specific port" echo " --jdb run JDB instead of GDB" + echo " --attach-existing attach to an existing process" echo " --help print this message" echo "" echo "Available devices:" @@ -63,6 +65,9 @@ while [ $# -gt 0 ]; do "--port" ) gdb_port=$1 ;; + "--attach-existing" ) + attach_existing=yes + ;; "--" ) shift gdbargs=$@ @@ -120,30 +125,32 @@ package_pids=`awk -- '{ print $1 }' <<< $package_pids` -# Finally, kill each existing process. -for pid in $package_pids; do - echo "Killing existing process $pid..." - adb -s $device shell run-as $package kill -9 $pid &> /dev/null -done - -# Now run the main activity. This must be done as the adb user and -# not as the package user. -echo "Starting activity $activity and attaching debugger" - -# Exit if the activity could not be started. -adb -s $device shell am start -D "$package/$activity" -if [ ! $? ]; then - exit 1; -fi +if [ "$attach_existing" != "yes" ]; then + # Finally, kill each existing process. + for pid in $package_pids; do + echo "Killing existing process $pid..." + adb -s $device shell run-as $package kill -9 $pid &> /dev/null + done + + # Now run the main activity. This must be done as the adb user and + # not as the package user. + echo "Starting activity $activity and attaching debugger" + + # Exit if the activity could not be started. + adb -s $device shell am start -D "$package/$activity" + if [ ! $? ]; then + exit 1; + fi -# Now look for processes matching the package again. -package_pids=`adb -s $device shell run-as $package ps -u $package_uid -o PID,CMD` + # Now look for processes matching the package again. + package_pids=`adb -s $device shell run-as $package ps -u $package_uid -o PID,CMD` -# Next, remove lines matching "ps" itself. -package_pids=`awk -- '{ + # Next, remove lines matching "ps" itself. + package_pids=`awk -- '{ if (!match ($0, /(PID|ps)/)) print $1 }' <<< $package_pids` +fi pid=$package_pids num_pids=`wc -w <<< "$package_pids"` diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 67f411a38c3..2b661024842 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -48,6 +48,9 @@ public class EmacsActivity extends Activity /* The currently focused window. */ public static EmacsWindow focusedWindow; + /* Whether or not this activity is paused. */ + private boolean isPaused; + static { focusedActivities = new ArrayList (); @@ -60,7 +63,7 @@ public class EmacsActivity extends Activity focusedWindow = window; for (EmacsWindow child : window.children) - invalidateFocus1 (window); + invalidateFocus1 (child); } public static void @@ -103,6 +106,9 @@ public class EmacsActivity extends Activity /* Clear the window's pointer to this activity and remove the window's view. */ window.setConsumer (null); + + /* The window can't be iconified any longer. */ + window.noticeDeiconified (); layout.removeView (window.view); window = null; @@ -114,6 +120,8 @@ public class EmacsActivity extends Activity public void attachWindow (EmacsWindow child) { + Log.d (TAG, "attachWindow: " + child); + if (window != null) throw new IllegalStateException ("trying to attach window when one" + " already exists"); @@ -124,6 +132,10 @@ public class EmacsActivity extends Activity layout.addView (window.view); child.setConsumer (this); + /* If the activity is iconified, send that to the window. */ + if (isPaused) + window.noticeIconified (); + /* Invalidate the focus. */ invalidateFocus (); } @@ -148,6 +160,9 @@ public class EmacsActivity extends Activity { FrameLayout.LayoutParams params; + /* Set the theme to one without a title bar. */ + setTheme (android.R.style.Theme_NoTitleBar); + params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); @@ -192,4 +207,24 @@ public class EmacsActivity extends Activity invalidateFocus (); } + + @Override + public void + onPause () + { + isPaused = true; + + EmacsWindowAttachmentManager.MANAGER.noticeIconified (this); + super.onResume (); + } + + @Override + public void + onResume () + { + isPaused = false; + + EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this); + super.onResume (); + } }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index ae48ce30408..a11e509cd7f 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -96,11 +96,34 @@ public class EmacsNative long time, int state, int button); - /* Send an ANDROID_BUTTON_RELEASE event. */ + /* Send an ANDROID_BUTTON_RELEASE event. */ public static native void sendButtonRelease (short window, int x, int y, long time, int state, int button); + /* Send an ANDROID_TOUCH_DOWN event. */ + public static native void sendTouchDown (short window, int x, int y, + long time, int pointerID); + + /* Send an ANDROID_TOUCH_UP event. */ + public static native void sendTouchUp (short window, int x, int y, + long time, int pointerID); + + /* Send an ANDROID_TOUCH_MOVE event. */ + public static native void sendTouchMove (short window, int x, int y, + long time, int pointerID); + + /* Send an ANDROID_WHEEL event. */ + public static native void sendWheel (short window, int x, int y, + long time, int state, + float xDelta, float yDelta); + + /* Send an ANDROID_ICONIFIED event. */ + public static native void sendIconified (short window); + + /* Send an ANDROID_DEICONIFIED event. */ + public static native void sendDeiconified (short window); + static { System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index 8a9426050ae..c0f24c7433a 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -149,8 +149,6 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } else familyName = fileName; - - Log.d (TAG, "Initialized new typeface " + familyName); } @Override @@ -321,17 +319,12 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver list = new LinkedList (); - Log.d (TAG, ("Looking for fonts matching font spec: " - + fontSpec.toString ())); - for (i = 0; i < typefaceList.length; ++i) { if (checkMatch (typefaceList[i], fontSpec)) list.add (new Sdk7FontEntity (typefaceList[i])); } - Log.d (TAG, "Found font entities: " + list.toString ()); - return (FontEntity[]) list.toArray (new FontEntity[0]); } diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 4444b7f1c56..01a1695f385 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -28,20 +28,27 @@ import android.graphics.Bitmap; import android.graphics.Point; import android.view.View; +import android.view.InputDevice; import android.annotation.TargetApi; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; + import android.os.Build; import android.os.Looper; import android.os.IBinder; import android.os.Handler; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.os.VibrationEffect; import android.util.Log; import android.util.DisplayMetrics; +import android.hardware.input.InputManager; + class Holder { T thing; @@ -250,4 +257,113 @@ public class EmacsService extends Service { window.clearArea (x, y, width, height); } + + @SuppressWarnings ("deprecation") + public void + ringBell () + { + Vibrator vibrator; + VibrationEffect effect; + VibratorManager vibratorManager; + Object tem; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + { + tem = getSystemService (Context.VIBRATOR_MANAGER_SERVICE); + vibratorManager = (VibratorManager) tem; + vibrator = vibratorManager.getDefaultVibrator (); + } + else + vibrator + = (Vibrator) getSystemService (Context.VIBRATOR_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + effect + = VibrationEffect.createOneShot (50, + VibrationEffect.DEFAULT_AMPLITUDE); + vibrator.vibrate (effect); + } + else + vibrator.vibrate (50); + } + + public short[] + queryTree (EmacsWindow window) + { + short[] array; + List windowList; + int i; + + if (window == null) + /* Just return all the windows without a parent. */ + windowList = EmacsWindowAttachmentManager.MANAGER.copyWindows (); + else + windowList = window.children; + + array = new short[windowList.size () + 1]; + i = 1; + + array[0] = window.parent != null ? 0 : window.parent.handle; + + for (EmacsWindow treeWindow : windowList) + array[i++] = treeWindow.handle; + + return array; + } + + public int + getScreenWidth (boolean mmWise) + { + DisplayMetrics metrics; + + metrics = getResources ().getDisplayMetrics (); + + if (!mmWise) + return metrics.widthPixels; + else + return (int) ((metrics.widthPixels / metrics.xdpi) * 2540.0); + } + + public int + getScreenHeight (boolean mmWise) + { + DisplayMetrics metrics; + + metrics = getResources ().getDisplayMetrics (); + + if (!mmWise) + return metrics.heightPixels; + else + return (int) ((metrics.heightPixels / metrics.ydpi) * 2540.0); + } + + public boolean + detectMouse () + { + InputManager manager; + InputDevice device; + int[] ids; + int i; + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.JELLY_BEAN) + return false; + + manager = (InputManager) getSystemService (Context.INPUT_SERVICE); + ids = manager.getInputDeviceIds (); + + for (i = 0; i < ids.length; ++i) + { + device = manager.getInputDevice (ids[i]); + + if (device == null) + continue; + + if (device.supportsSource (InputDevice.SOURCE_MOUSE)) + return true; + } + + return false; + } }; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 5efb8882263..f713818d4bc 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -27,8 +27,12 @@ import android.os.Build; import android.graphics.Canvas; import android.graphics.Rect; +import android.util.Log; + public class EmacsSurfaceView extends SurfaceView { + private static final String TAG = "EmacsSurfaceView"; + public Object surfaceChangeLock; private boolean created; @@ -45,6 +49,7 @@ public class EmacsSurfaceView extends SurfaceView surfaceChanged (SurfaceHolder holder, int format, int width, int height) { + Log.d (TAG, "surfaceChanged: " + view); view.swapBuffers (); } @@ -54,9 +59,13 @@ public class EmacsSurfaceView extends SurfaceView { synchronized (surfaceChangeLock) { + Log.d (TAG, "surfaceCreated: " + view); created = true; - view.swapBuffers (); } + + /* Drop the lock when doing this, or a deadlock can + result. */ + view.swapBuffers (); } @Override @@ -65,6 +74,7 @@ public class EmacsSurfaceView extends SurfaceView { synchronized (surfaceChangeLock) { + Log.d (TAG, "surfaceDestroyed: " + view); created = false; } } @@ -93,6 +103,16 @@ public class EmacsSurfaceView extends SurfaceView return holder.lockCanvas (damage); } + @Override + protected void + onLayout (boolean changed, int left, int top, int right, + int bottom) + { + Log.d (TAG, ("onLayout: " + left + " " + top + " " + right + + " " + bottom + " -- " + changed + " visibility " + + getVisibility ())); + } + /* This method is only used during debugging when it seems damage isn't working correctly. */ diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 169a1e42ee3..41acabab97b 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -87,12 +87,27 @@ public class EmacsView extends ViewGroup /* Create the surface view. */ this.surfaceView = new EmacsSurfaceView (this); + this.surfaceView.setZOrderMediaOverlay (true); addView (this.surfaceView); + + /* Not sure exactly what this does but it makes things magically + work. Why is something as simple as XRaiseWindow so involved + on Android? */ + setChildrenDrawingOrderEnabled (true); + + /* Get rid of the foreground and background tint. */ + setBackgroundTintList (null); + setForegroundTintList (null); } private void handleDirtyBitmap () { + Bitmap oldBitmap; + + /* Save the old bitmap. */ + oldBitmap = bitmap; + /* Recreate the front and back buffer bitmaps. */ bitmap = Bitmap.createBitmap (bitmapDirty.width (), @@ -103,12 +118,23 @@ public class EmacsView extends ViewGroup /* And canvases. */ canvas = new Canvas (bitmap); - /* If Emacs is drawing to the bitmap right now from the - main thread, the image contents are lost until the next - ConfigureNotify and complete garbage. Sorry! */ + /* Copy over the contents of the old bitmap. */ + if (oldBitmap != null) + canvas.drawBitmap (oldBitmap, 0f, 0f, new Paint ()); + bitmapDirty = null; } + public synchronized void + explicitlyDirtyBitmap (Rect rect) + { + if (bitmapDirty == null + && (bitmap == null + || rect.width () != bitmap.getWidth () + || rect.height () != bitmap.getHeight ())) + bitmapDirty = rect; + } + public synchronized Bitmap getBitmap () { @@ -168,25 +194,31 @@ public class EmacsView extends ViewGroup View child; Rect windowRect; + count = getChildCount (); + if (changed || mustReportLayout) { mustReportLayout = false; window.viewLayout (left, top, right, bottom); } - if (changed) + if (changed + /* Check that a change has really happened. */ + && (bitmapDirty == null + || bitmapDirty.width () != right - left + || bitmapDirty.height () != bottom - top)) bitmapDirty = new Rect (left, top, right, bottom); - count = getChildCount (); - for (i = 0; i < count; ++i) { child = getChildAt (i); + Log.d (TAG, "onLayout: " + child); + if (child == surfaceView) /* The child is the surface view, so give it the entire view. */ - child.layout (left, top, right, bottom); + child.layout (0, 0, right - left, bottom - top); else if (child.getVisibility () != GONE) { if (!(child instanceof EmacsView)) @@ -201,59 +233,68 @@ public class EmacsView extends ViewGroup } } - public synchronized void + public void damageRect (Rect damageRect) { - damageRegion.union (damageRect); + synchronized (damageRegion) + { + damageRegion.union (damageRect); + } } /* This method is called from both the UI thread and the Emacs thread. */ - public synchronized void + public void swapBuffers (boolean force) { Canvas canvas; Rect damageRect; Bitmap bitmap; - if (damageRegion.isEmpty ()) - return; + /* Code must always take damageRegion, and then surfaceChangeLock, + never the other way around! */ - bitmap = getBitmap (); + synchronized (damageRegion) + { + if (damageRegion.isEmpty ()) + return; - /* Emacs must take the following lock to ensure the access to the - canvas occurs with the surface created. Otherwise, Android - will throttle calls to lockCanvas. */ + bitmap = getBitmap (); - synchronized (surfaceView.surfaceChangeLock) - { - damageRect = damageRegion.getBounds (); + /* Emacs must take the following lock to ensure the access to the + canvas occurs with the surface created. Otherwise, Android + will throttle calls to lockCanvas. */ - if (!surfaceView.isCreated ()) - return; + synchronized (surfaceView.surfaceChangeLock) + { + damageRect = damageRegion.getBounds (); - if (bitmap == null) - return; + if (!surfaceView.isCreated ()) + return; - /* Lock the canvas with the specified damage. */ - canvas = surfaceView.lockCanvas (damageRect); + if (bitmap == null) + return; - /* Return if locking the canvas failed. */ - if (canvas == null) - return; + /* Lock the canvas with the specified damage. */ + canvas = surfaceView.lockCanvas (damageRect); - /* Copy from the back buffer to the canvas. If damageRect was - made empty, then draw the entire back buffer. */ + /* Return if locking the canvas failed. */ + if (canvas == null) + return; - if (damageRect.isEmpty ()) - canvas.drawBitmap (bitmap, 0f, 0f, paint); - else - canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + /* Copy from the back buffer to the canvas. If damageRect was + made empty, then draw the entire back buffer. */ - /* Unlock the canvas and clear the damage. */ - surfaceView.unlockCanvasAndPost (canvas); - damageRegion.setEmpty (); + if (damageRect.isEmpty ()) + canvas.drawBitmap (bitmap, 0f, 0f, paint); + else + canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + + /* Unlock the canvas and clear the damage. */ + surfaceView.unlockCanvasAndPost (canvas); + damageRegion.setEmpty (); + } } } @@ -308,6 +349,78 @@ public class EmacsView extends ViewGroup public boolean onTouchEvent (MotionEvent motion) { - return window.onSomeKindOfMotionEvent (motion); + return window.onTouchEvent (motion); + } + + private void + moveChildToBack (View child) + { + int index; + + index = indexOfChild (child); + + if (index > 0) + { + detachViewFromParent (index); + + /* The view at 0 is the surface view. */ + attachViewToParent (child, 1, + child.getLayoutParams()); + } + } + + + /* The following two functions must not be called if the view has no + parent, or is parented to an activity. */ + + public void + raise () + { + EmacsView parent; + + parent = (EmacsView) getParent (); + + Log.d (TAG, "raise: parent " + parent); + + if (parent.indexOfChild (this) + == parent.getChildCount () - 1) + return; + + parent.bringChildToFront (this); + + /* Yes, all of this is really necessary! */ + parent.requestLayout (); + parent.invalidate (); + requestLayout (); + invalidate (); + + /* The surface view must be destroyed and recreated. */ + removeView (surfaceView); + addView (surfaceView, 0); + } + + public void + lower () + { + EmacsView parent; + + parent = (EmacsView) getParent (); + + Log.d (TAG, "lower: parent " + parent); + + if (parent.indexOfChild (this) == 1) + return; + + parent.moveChildToBack (this); + + /* Yes, all of this is really necessary! */ + parent.requestLayout (); + parent.invalidate (); + requestLayout (); + invalidate (); + + /* The surface view must be removed and attached again. */ + removeView (surfaceView); + addView (surfaceView, 0); } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index a9a24b61869..1f8596dba50 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -22,6 +22,7 @@ package org.gnu.emacs; import java.lang.IllegalStateException; import java.util.ArrayList; import java.util.List; +import java.util.HashMap; import android.graphics.Rect; import android.graphics.Canvas; @@ -50,9 +51,29 @@ import android.os.Build; Views are also drawables, meaning they can accept drawing requests. */ +/* Help wanted. What does not work includes `EmacsView.raise', + `EmacsView.lower', reparenting a window onto another window. + + All three are likely undocumented restrictions within + EmacsSurface. */ + public class EmacsWindow extends EmacsHandleObject implements EmacsDrawable { + private static final String TAG = "EmacsWindow"; + + private class Coordinate + { + /* Integral coordinate. */ + int x, y; + + Coordinate (int x, int y) + { + this.x = x; + this.y = y; + } + }; + /* The view associated with the window. */ public EmacsView view; @@ -60,12 +81,16 @@ public class EmacsWindow extends EmacsHandleObject private Rect rect; /* The parent window, or null if it is the root window. */ - private EmacsWindow parent; + public EmacsWindow parent; /* List of all children in stacking order. This must be kept consistent! */ public ArrayList children; + /* Map between pointer identifiers and last known position. Used to + compute which pointer changed upon a touch event. */ + private HashMap pointerMap; + /* The window consumer currently attached, if it exists. */ private EmacsWindowAttachmentManager.WindowConsumer attached; @@ -77,6 +102,14 @@ public class EmacsWindow extends EmacsHandleObject last button press or release event. */ private int lastButtonState, lastModifiers; + /* Whether or not the window is mapped, and whether or not it is + deiconified. */ + private boolean isMapped, isIconified; + + /* Whether or not to ask for focus upon being mapped, and whether or + not the window should be focusable. */ + private boolean dontFocusOnMap, dontAcceptFocus; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height) @@ -84,6 +117,7 @@ public class EmacsWindow extends EmacsHandleObject super (handle); rect = new Rect (x, y, x + width, y + height); + pointerMap = new HashMap (); /* Create the view from the context's UI thread. The window is unmapped, so the view is GONE. */ @@ -97,7 +131,7 @@ public class EmacsWindow extends EmacsHandleObject if (parent != null) { parent.children.add (this); - parent.view.post (new Runnable () { + EmacsService.SERVICE.runOnUiThread (new Runnable () { @Override public void run () @@ -106,23 +140,6 @@ public class EmacsWindow extends EmacsHandleObject } }); } - else - EmacsService.SERVICE.runOnUiThread (new Runnable () { - @Override - public void - run () - { - EmacsWindowAttachmentManager manager; - - manager = EmacsWindowAttachmentManager.MANAGER; - - /* If parent is the root window, notice that there are new - children available for interested activites to pick - up. */ - - manager.registerWindow (EmacsWindow.this); - } - }); scratchGC = new EmacsGC ((short) 0); } @@ -159,7 +176,7 @@ public class EmacsWindow extends EmacsHandleObject + "children!"); /* Remove the view from its parent and make it invisible. */ - view.post (new Runnable () { + EmacsService.SERVICE.runOnUiThread (new Runnable () { public void run () { @@ -174,7 +191,7 @@ public class EmacsWindow extends EmacsHandleObject parent = (View) view.getParent (); - if (parent != null && attached == null) + if (parent != null) ((ViewGroup) parent).removeView (view); manager.detachWindow (EmacsWindow.this); @@ -199,24 +216,33 @@ public class EmacsWindow extends EmacsHandleObject public void viewLayout (int left, int top, int right, int bottom) { + int rectWidth, rectHeight; + synchronized (this) { rect.left = left; rect.top = top; rect.right = right; rect.bottom = bottom; - - EmacsNative.sendConfigureNotify (this.handle, - System.currentTimeMillis (), - left, top, rect.width (), - rect.height ()); } + + rectWidth = right - left; + rectHeight = bottom - top; + + EmacsNative.sendConfigureNotify (this.handle, + System.currentTimeMillis (), + left, top, rectWidth, + rectHeight); } public void requestViewLayout () { - view.post (new Runnable () { + /* This is necessary because otherwise subsequent drawing on the + Emacs thread may be lost. */ + view.explicitlyDirtyBitmap (rect); + + EmacsService.SERVICE.runOnUiThread (new Runnable () { @Override public void run () @@ -261,28 +287,77 @@ public class EmacsWindow extends EmacsHandleObject public void mapWindow () { - view.post (new Runnable () { - @Override - public void - run () - { + if (isMapped) + return; - view.setVisibility (View.VISIBLE); - /* Eventually this should check no-focus-on-map. */ - view.requestFocus (); - } - }); + isMapped = true; + + if (parent == null) + { + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + EmacsWindowAttachmentManager manager; + + /* Make the view visible, first of all. */ + view.setVisibility (View.VISIBLE); + + manager = EmacsWindowAttachmentManager.MANAGER; + + /* If parent is the root window, notice that there are new + children available for interested activites to pick + up. */ + manager.registerWindow (EmacsWindow.this); + + if (!getDontFocusOnMap ()) + /* Eventually this should check no-focus-on-map. */ + view.requestFocus (); + } + }); + } + else + { + /* Do the same thing as above, but don't register this + window. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.setVisibility (View.VISIBLE); + + if (!getDontFocusOnMap ()) + /* Eventually this should check no-focus-on-map. */ + view.requestFocus (); + } + }); + } } public void unmapWindow () { + if (!isMapped) + return; + + isMapped = false; + view.post (new Runnable () { @Override public void run () { + EmacsWindowAttachmentManager manager; + + manager = EmacsWindowAttachmentManager.MANAGER; + view.setVisibility (View.GONE); + + /* Now that the window is unmapped, unregister it as + well. */ + manager.detachWindow (EmacsWindow.this); } }); } @@ -413,6 +488,161 @@ public class EmacsWindow extends EmacsHandleObject return 4; } + /* Return the ID of the pointer which changed in EVENT. Value is -1 + if it could not be determined, else the pointer that changed, or + -2 if -1 would have been returned, but there is also a pointer + that is a mouse. */ + + private int + figureChange (MotionEvent event) + { + int pointerID, i, truncatedX, truncatedY, pointerIndex; + Coordinate coordinate; + boolean mouseFlag; + + /* pointerID is always initialized but the Java compiler is too + dumb to know that. */ + pointerID = -1; + mouseFlag = false; + + switch (event.getActionMasked ()) + { + case MotionEvent.ACTION_DOWN: + /* Primary pointer pressed with index 0. */ + + /* Detect mice. If this is a mouse event, give it to + onSomeKindOfMotionEvent. */ + if ((Build.VERSION.SDK_INT + >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + && event.getToolType (0) == MotionEvent.TOOL_TYPE_MOUSE) + return -2; + + pointerID = event.getPointerId (0); + pointerMap.put (pointerID, + new Coordinate ((int) event.getX (0), + (int) event.getY (0))); + break; + + case MotionEvent.ACTION_UP: + /* Primary pointer released with index 0. */ + pointerID = event.getPointerId (0); + pointerMap.remove (pointerID); + break; + + case MotionEvent.ACTION_POINTER_DOWN: + /* New pointer. Find the pointer ID from the index and place + it in the map. */ + pointerIndex = event.getActionIndex (); + pointerID = event.getPointerId (pointerIndex); + pointerMap.put (pointerID, + new Coordinate ((int) event.getX (pointerID), + (int) event.getY (pointerID))); + break; + + case MotionEvent.ACTION_POINTER_UP: + /* Pointer removed. Remove it from the map. */ + pointerIndex = event.getActionIndex (); + pointerID = event.getPointerId (pointerIndex); + pointerMap.remove (pointerID); + break; + + default: + + /* Loop through each pointer in the event. */ + for (i = 0; i < event.getPointerCount (); ++i) + { + pointerID = event.getPointerId (i); + + /* Look up that pointer in the map. */ + coordinate = pointerMap.get (pointerID); + + if (coordinate != null) + { + /* See if coordinates have changed. */ + truncatedX = (int) event.getX (i); + truncatedY = (int) event.getY (i); + + if (truncatedX != coordinate.x + || truncatedY != coordinate.y) + { + /* The pointer changed. Update the coordinate and + break out of the loop. */ + coordinate.x = truncatedX; + coordinate.y = truncatedY; + + break; + } + } + + /* See if this is a mouse. If so, set the mouseFlag. */ + if ((Build.VERSION.SDK_INT + >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + && event.getToolType (i) == MotionEvent.TOOL_TYPE_MOUSE) + mouseFlag = true; + } + + /* Set the pointer ID to -1 if the loop failed to find any + changed pointer. If a mouse pointer was found, set it to + -2. */ + if (i == event.getPointerCount ()) + pointerID = (mouseFlag ? -2 : -1); + } + + /* Return the pointer ID. */ + return pointerID; + } + + public boolean + onTouchEvent (MotionEvent event) + { + int pointerID, index; + + /* Extract the ``touch ID'' (or in Android, the ``pointer + ID''.) */ + pointerID = figureChange (event); + + if (pointerID < 0) + { + /* If this is a mouse event, give it to + onSomeKindOfMotionEvent. */ + if (pointerID == -2) + return onSomeKindOfMotionEvent (event); + + return false; + } + + /* Find the pointer index corresponding to the event. */ + index = event.findPointerIndex (pointerID); + + switch (event.getActionMasked ()) + { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + /* Touch down event. */ + EmacsNative.sendTouchDown (this.handle, (int) event.getX (index), + (int) event.getY (index), + event.getEventTime (), pointerID); + return true; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + /* Touch up event. */ + EmacsNative.sendTouchUp (this.handle, (int) event.getX (index), + (int) event.getY (index), + event.getEventTime (), pointerID); + return true; + + case MotionEvent.ACTION_MOVE: + /* Pointer motion event. */ + EmacsNative.sendTouchMove (this.handle, (int) event.getX (index), + (int) event.getY (index), + event.getEventTime (), pointerID); + return true; + } + + return false; + } + public boolean onSomeKindOfMotionEvent (MotionEvent event) { @@ -472,13 +702,201 @@ public class EmacsWindow extends EmacsHandleObject case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_UP: - /* Emacs must return true even though touch events are not yet - handled, because the value of this function is used by the - system to decide whether or not Emacs gets ACTION_MOVE + /* Emacs must return true even though touch events are not + handled here, because the value of this function is used by + the system to decide whether or not Emacs gets ACTION_MOVE events. */ return true; + + case MotionEvent.ACTION_SCROLL: + /* Send a scroll event with the specified deltas. */ + EmacsNative.sendWheel (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime (), + lastModifiers, + event.getAxisValue (MotionEvent.AXIS_HSCROLL), + event.getAxisValue (MotionEvent.AXIS_VSCROLL)); + return true; } return false; } + + public void + reparentTo (final EmacsWindow otherWindow, int x, int y) + { + int width, height; + + /* Reparent this window to the other window. */ + + if (parent != null) + parent.children.remove (this); + + if (otherWindow != null) + otherWindow.children.add (this); + + parent = otherWindow; + + /* Move this window to the new location. */ + synchronized (this) + { + width = rect.width (); + height = rect.height (); + rect.left = x; + rect.top = y; + rect.right = x + width; + rect.bottom = y + height; + } + + /* Now do the work necessary on the UI thread to reparent the + window. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + EmacsWindowAttachmentManager manager; + View parent; + + /* First, detach this window if necessary. */ + manager = EmacsWindowAttachmentManager.MANAGER; + manager.detachWindow (EmacsWindow.this); + + /* Also unparent this view. */ + parent = (View) view.getParent (); + + if (parent != null) + ((ViewGroup) parent).removeView (view); + + /* Next, either add this window as a child of the new + parent's view, or make it available again. */ + if (otherWindow != null) + otherWindow.view.addView (view); + else if (EmacsWindow.this.isMapped) + manager.registerWindow (EmacsWindow.this); + + /* Request relayout. */ + view.requestLayout (); + } + }); + } + + public void + makeInputFocus (long time) + { + /* TIME is currently ignored. Request the input focus now. */ + + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.requestFocus (); + } + }); + } + + public void + raise () + { + /* This does nothing here. */ + if (parent == null) + return; + + /* Remove and add this view again. */ + parent.children.remove (this); + parent.children.add (this); + + /* Request a relayout. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.raise (); + } + }); + } + + public void + lower () + { + /* This does nothing here. */ + if (parent == null) + return; + + /* Remove and add this view again. */ + parent.children.remove (this); + parent.children.add (this); + + /* Request a relayout. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.lower (); + } + }); + } + + public int[] + getWindowGeometry () + { + int[] array; + Rect rect; + + array = new int[4]; + rect = getGeometry (); + + array[0] = rect.left; + array[1] = rect.top; + array[2] = rect.width (); + array[3] = rect.height (); + + return array; + } + + public void + noticeIconified () + { + isIconified = true; + EmacsNative.sendIconified (this.handle); + } + + public void + noticeDeiconified () + { + isIconified = false; + EmacsNative.sendDeiconified (this.handle); + } + + public synchronized void + setDontAcceptFocus (final boolean dontAcceptFocus) + { + this.dontAcceptFocus = dontAcceptFocus; + + /* Update the view's focus state. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.setFocusable (!dontAcceptFocus); + view.setFocusableInTouchMode (!dontAcceptFocus); + } + }); + } + + public synchronized void + setDontFocusOnMap (final boolean dontFocusOnMap) + { + this.dontFocusOnMap = dontFocusOnMap; + } + + public synchronized boolean + getDontFocusOnMap () + { + return dontFocusOnMap; + } }; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 34be2ab8789..15eb3bb65c2 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -19,6 +19,7 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -68,7 +69,7 @@ public class EmacsWindowAttachmentManager }; private List consumers; - private List windows; + public List windows; public EmacsWindowAttachmentManager () @@ -98,12 +99,19 @@ public class EmacsWindowAttachmentManager EmacsNative.sendWindowAction ((short) 0, 0); } - public void + public synchronized void registerWindow (EmacsWindow window) { Intent intent; - Log.d (TAG, "registerWindow " + window); + Log.d (TAG, "registerWindow (maybe): " + window); + + if (windows.contains (window)) + /* The window is already registered. */ + return; + + Log.d (TAG, "registerWindow: " + window); + windows.add (window); for (WindowConsumer consumer : consumers) @@ -146,7 +154,7 @@ public class EmacsWindowAttachmentManager consumers.remove (consumer); } - public void + public synchronized void detachWindow (EmacsWindow window) { WindowConsumer consumer; @@ -162,5 +170,43 @@ public class EmacsWindowAttachmentManager consumers.remove (consumer); consumer.destroy (); } + + windows.remove (window); + } + + public void + noticeIconified (WindowConsumer consumer) + { + EmacsWindow window; + + Log.d (TAG, "noticeIconified " + consumer); + + /* If a window is attached, send the appropriate iconification + events. */ + window = consumer.getAttachedWindow (); + + if (window != null) + window.noticeIconified (); + } + + public void + noticeDeiconified (WindowConsumer consumer) + { + EmacsWindow window; + + Log.d (TAG, "noticeDeiconified " + consumer); + + /* If a window is attached, send the appropriate iconification + events. */ + window = consumer.getAttachedWindow (); + + if (window != null) + window.noticeDeiconified (); + } + + public synchronized List + copyWindows () + { + return new ArrayList (windows); } }; diff --git a/lisp/frame.el b/lisp/frame.el index 436323f7d58..d35df71a6cc 100644 --- a/lisp/frame.el +++ b/lisp/frame.el @@ -1647,6 +1647,7 @@ live frame and defaults to the selected one." (declare-function ns-frame-geometry "nsfns.m" (&optional frame)) (declare-function pgtk-frame-geometry "pgtkfns.c" (&optional frame)) (declare-function haiku-frame-geometry "haikufns.c" (&optional frame)) +(declare-function android-frame-geometry "androidfns.c" (&optional frame)) (defun frame-geometry (&optional frame) "Return geometric attributes of FRAME. @@ -1700,6 +1701,8 @@ and width values are in pixels. (pgtk-frame-geometry frame)) ((eq frame-type 'haiku) (haiku-frame-geometry frame)) + ((eq frame-type 'android) + (android-frame-geometry frame)) (t (list '(outer-position 0 . 0) @@ -1826,6 +1829,7 @@ of frames like calls to map a frame or change its visibility." (declare-function ns-frame-edges "nsfns.m" (&optional frame type)) (declare-function pgtk-frame-edges "pgtkfns.c" (&optional frame type)) (declare-function haiku-frame-edges "haikufns.c" (&optional frame type)) +(declare-function android-frame-edges "androidfns.c" (&optional frame type)) (defun frame-edges (&optional frame type) "Return coordinates of FRAME's edges. @@ -1853,6 +1857,8 @@ FRAME." (pgtk-frame-edges frame type)) ((eq frame-type 'haiku) (haiku-frame-edges frame type)) + ((eq frame-type 'android) + (android-frame-edges frame type)) (t (list 0 0 (frame-width frame) (frame-height frame)))))) @@ -1861,6 +1867,7 @@ FRAME." (declare-function ns-mouse-absolute-pixel-position "nsfns.m") (declare-function pgtk-mouse-absolute-pixel-position "pgtkfns.c") (declare-function haiku-mouse-absolute-pixel-position "haikufns.c") +(declare-function android-mouse-absolute-pixel-position "androidfns.c") (defun mouse-absolute-pixel-position () "Return absolute position of mouse cursor in pixels. @@ -1879,6 +1886,8 @@ position (0, 0) of the selected frame's terminal." (pgtk-mouse-absolute-pixel-position)) ((eq frame-type 'haiku) (haiku-mouse-absolute-pixel-position)) + ((eq frame-type 'android) + (android-mouse-absolute-pixel-position)) (t (cons 0 0))))) @@ -1887,6 +1896,8 @@ position (0, 0) of the selected frame's terminal." (declare-function w32-set-mouse-absolute-pixel-position "w32fns.c" (x y)) (declare-function x-set-mouse-absolute-pixel-position "xfns.c" (x y)) (declare-function haiku-set-mouse-absolute-pixel-position "haikufns.c" (x y)) +(declare-function android-set-mouse-absolute-pixel-position + "androidfns.c" (x y)) (defun set-mouse-absolute-pixel-position (x y) "Move mouse pointer to absolute pixel position (X, Y). @@ -1903,7 +1914,9 @@ position (0, 0) of the selected frame's terminal." ((eq frame-type 'w32) (w32-set-mouse-absolute-pixel-position x y)) ((eq frame-type 'haiku) - (haiku-set-mouse-absolute-pixel-position x y))))) + (haiku-set-mouse-absolute-pixel-position x y)) + ((eq frame-type 'android) + (android-set-mouse-absolute-pixel-position x y))))) (defun frame-monitor-attributes (&optional frame) "Return the attributes of the physical monitor dominating FRAME. @@ -1999,6 +2012,7 @@ workarea attribute." ;; TODO: implement this on PGTK. ;; (declare-function pgtk-frame-list-z-order "pgtkfns.c" (&optional display)) (declare-function haiku-frame-list-z-order "haikufns.c" (&optional display)) +(declare-function android-frame-list-z-order "androidfns.c" (&optional display)) (defun frame-list-z-order (&optional display) "Return list of Emacs' frames, in Z (stacking) order. @@ -2024,13 +2038,17 @@ Return nil if DISPLAY contains no Emacs frame." ;; (pgtk-frame-list-z-order display) nil) ((eq frame-type 'haiku) - (haiku-frame-list-z-order display))))) + (haiku-frame-list-z-order display)) + ((eq frame-type 'android) + (android-frame-list-z-order display))))) (declare-function x-frame-restack "xfns.c" (frame1 frame2 &optional above)) (declare-function w32-frame-restack "w32fns.c" (frame1 frame2 &optional above)) (declare-function ns-frame-restack "nsfns.m" (frame1 frame2 &optional above)) (declare-function pgtk-frame-restack "pgtkfns.c" (frame1 frame2 &optional above)) (declare-function haiku-frame-restack "haikufns.c" (frame1 frame2 &optional above)) +(declare-function android-frame-restack "androidfns.c" (frame1 frame2 + &optional above)) (defun frame-restack (frame1 frame2 &optional above) "Restack FRAME1 below FRAME2. @@ -2064,7 +2082,9 @@ Some window managers may refuse to restack windows." ((eq frame-type 'haiku) (haiku-frame-restack frame1 frame2 above)) ((eq frame-type 'pgtk) - (pgtk-frame-restack frame1 frame2 above)))) + (pgtk-frame-restack frame1 frame2 above)) + ((eq frame-type 'android) + (android-frame-restack frame1 frame2 above)))) (error "Cannot restack frames"))) (defun frame-size-changed-p (&optional frame) @@ -2113,6 +2133,8 @@ frame's display)." (> w32-num-mouse-buttons 0))) ((memq frame-type '(x ns haiku pgtk)) t) ;; We assume X, NeXTstep, GTK, and Haiku *always* have a pointing device + ((eq frame-type 'android) + (android-detect-mouse)) (t (or (and (featurep 'xt-mouse) xterm-mouse-mode) @@ -2396,6 +2418,8 @@ If DISPLAY is omitted or nil, it defaults to the selected frame's display." (&optional terminal)) (declare-function haiku-display-monitor-attributes-list "haikufns.c" (&optional terminal)) +(declare-function android-display-monitor-attributes-list "androidfns.c" + (&optional terminal)) (defun display-monitor-attributes-list (&optional display) "Return a list of physical monitor attributes on DISPLAY. @@ -2449,6 +2473,8 @@ monitors." (pgtk-display-monitor-attributes-list display)) ((eq frame-type 'haiku) (haiku-display-monitor-attributes-list display)) + ((eq frame-type 'android) + (android-display-monitor-attributes-list display)) (t (let ((geometry (list 0 0 (display-pixel-width display) (display-pixel-height display)))) diff --git a/lisp/mwheel.el b/lisp/mwheel.el index 1be52d24e34..67d93c3558d 100644 --- a/lisp/mwheel.el +++ b/lisp/mwheel.el @@ -58,7 +58,8 @@ (defcustom mouse-wheel-down-event (if (or (featurep 'w32-win) (featurep 'ns-win) - (featurep 'haiku-win) (featurep 'pgtk-win)) + (featurep 'haiku-win) (featurep 'pgtk-win) + (featurep 'android-win)) 'wheel-up 'mouse-4) "Event used for scrolling down." @@ -79,7 +80,8 @@ (defcustom mouse-wheel-up-event (if (or (featurep 'w32-win) (featurep 'ns-win) - (featurep 'haiku-win) (featurep 'pgtk-win)) + (featurep 'haiku-win) (featurep 'pgtk-win) + (featurep 'android-win)) 'wheel-down 'mouse-5) "Event used for scrolling up." @@ -254,7 +256,8 @@ Also see `mouse-wheel-tilt-scroll'." (defvar mouse-wheel-left-event (if (or (featurep 'w32-win) (featurep 'ns-win) - (featurep 'haiku-win) (featurep 'pgtk-win)) + (featurep 'haiku-win) (featurep 'pgtk-win) + (featurep 'android-win)) 'wheel-left 'mouse-6) "Event used for scrolling left.") @@ -268,7 +271,8 @@ Also see `mouse-wheel-tilt-scroll'." (defvar mouse-wheel-right-event (if (or (featurep 'w32-win) (featurep 'ns-win) - (featurep 'haiku-win) (featurep 'pgtk-win)) + (featurep 'haiku-win) (featurep 'pgtk-win) + (featurep 'android-win)) 'wheel-right 'mouse-7) "Event used for scrolling right.") diff --git a/src/android.c b/src/android.c index 2f852662001..eab18dc6152 100644 --- a/src/android.c +++ b/src/android.c @@ -82,6 +82,11 @@ struct android_emacs_service jmethodID copy_area; jmethodID clear_window; jmethodID clear_area; + jmethodID ring_bell; + jmethodID query_tree; + jmethodID get_screen_width; + jmethodID get_screen_height; + jmethodID detect_mouse; }; struct android_emacs_pixmap @@ -196,9 +201,6 @@ struct android_event_queue /* The thread used to run select. */ pthread_t select_thread; - /* Condition variable for the writing side. */ - pthread_cond_t write_var; - /* Condition variables for the reading side. */ pthread_cond_t read_var; @@ -314,11 +316,6 @@ android_init_events (void) "pthread_mutex_init: %s", strerror (errno)); - if (pthread_cond_init (&event_queue.write_var, NULL)) - __android_log_print (ANDROID_LOG_FATAL, __func__, - "pthread_cond_init: %s", - strerror (errno)); - if (pthread_cond_init (&event_queue.read_var, NULL)) __android_log_print (ANDROID_LOG_FATAL, __func__, "pthread_cond_init: %s", @@ -387,8 +384,7 @@ android_next_event (union android_event *event_return) /* Free the container. */ free (container); - /* Signal that events can now be written. */ - pthread_cond_signal (&event_queue.write_var); + /* Unlock the queue. */ pthread_mutex_unlock (&event_queue.mutex); } @@ -407,12 +403,6 @@ android_write_event (union android_event *event) return; pthread_mutex_lock (&event_queue.mutex); - - /* The event queue is full, wait for events to be read. */ - if (event_queue.num_events >= 1024) - pthread_cond_wait (&event_queue.write_var, - &event_queue.mutex); - container->next = event_queue.events.next; container->last = &event_queue.events; container->next->last = container; @@ -421,6 +411,10 @@ android_write_event (union android_event *event) event_queue.num_events++; pthread_cond_signal (&event_queue.read_var); pthread_mutex_unlock (&event_queue.mutex); + + /* Now set pending_signals to true. This allows C-g to be handled + immediately even without SIGIO. */ + pending_signals = true; } int @@ -604,31 +598,60 @@ android_file_access_p (const char *name, int amode) { AAsset *asset; AAssetDir *directory; + int length; if (!asset_manager) return false; if (!(amode & W_OK) && (name = android_get_asset_name (name))) { + if (!strcmp (name, "") || !strcmp (name, "/")) + /* /assets always exists. */ + return true; + /* Check if the asset exists by opening it. Suboptimal! */ asset = AAssetManager_open (asset_manager, name, AASSET_MODE_UNKNOWN); if (!asset) { - /* See if it's a directory also. */ + /* See if it's a directory as well. To open a directory + with the asset manager, the trailing slash (if specified) + must be removed. */ directory = AAssetManager_openDir (asset_manager, name); if (directory) { + /* Make sure the directory actually has files in it. */ + + if (!AAssetDir_getNextFileName (directory)) + { + AAssetDir_close (directory); + errno = ENOENT; + return false; + } + AAssetDir_close (directory); return true; } + errno = ENOENT; return false; } AAsset_close (asset); + + /* If NAME is a directory name, but it was a regular file, set + errno to ENOTDIR and return false. This is to behave like + faccessat. */ + + length = strlen (name); + if (name[length - 1] == '/') + { + errno = ENOTDIR; + return false; + } + return true; } @@ -819,12 +842,31 @@ int android_close (int fd) { if (fd < ANDROID_MAX_ASSET_FD - && (android_table[fd].flags & ANDROID_FD_TABLE_ENTRY_IS_VALID)) + && (android_table[fd].flags + & ANDROID_FD_TABLE_ENTRY_IS_VALID)) android_table[fd].flags = 0; return close (fd); } +/* Like fclose. However, remove any information associated with + FILE's file descriptor from the asset table as well. */ + +int +android_fclose (FILE *stream) +{ + int fd; + + fd = fileno (stream); + + if (fd != -1 && fd < ANDROID_MAX_ASSET_FD + && (android_table[fd].flags + & ANDROID_FD_TABLE_ENTRY_IS_VALID)) + android_table[fd].flags = 0; + + return fclose (stream); +} + /* Return the current user's ``home'' directory, which is actually the app data directory on Android. */ @@ -1010,7 +1052,12 @@ android_init_emacs_service (void) "(Lorg/gnu/emacs/EmacsWindow;)V"); FIND_METHOD (clear_area, "clearArea", "(Lorg/gnu/emacs/EmacsWindow;IIII)V"); - + FIND_METHOD (ring_bell, "ringBell", "()V"); + FIND_METHOD (query_tree, "queryTree", + "(Lorg/gnu/emacs/EmacsWindow;)[S"); + FIND_METHOD (get_screen_width, "getScreenWidth", "(Z)I"); + FIND_METHOD (get_screen_height, "getScreenHeight", "(Z)I"); + FIND_METHOD (detect_mouse, "detectMouse", "()Z"); #undef FIND_METHOD } @@ -1342,6 +1389,97 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, android_write_event (&event); } +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, + jshort window, jint x, jint y, + jlong time, jint pointer_id) +{ + union android_event event; + + event.touch.type = ANDROID_TOUCH_DOWN; + event.touch.window = window; + event.touch.x = x; + event.touch.y = y; + event.touch.time = time; + event.touch.pointer_id = pointer_id; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, + jshort window, jint x, jint y, + jlong time, jint pointer_id) +{ + union android_event event; + + event.touch.type = ANDROID_TOUCH_UP; + event.touch.window = window; + event.touch.x = x; + event.touch.y = y; + event.touch.time = time; + event.touch.pointer_id = pointer_id; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, + jshort window, jint x, jint y, + jlong time, jint pointer_id) +{ + union android_event event; + + event.touch.type = ANDROID_TOUCH_MOVE; + event.touch.window = window; + event.touch.x = x; + event.touch.y = y; + event.touch.time = time; + event.touch.pointer_id = pointer_id; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, + jshort window, jint x, jint y, + jlong time, jint state, + jfloat x_delta, jfloat y_delta) +{ + union android_event event; + + event.wheel.type = ANDROID_WHEEL; + event.wheel.window = window; + event.wheel.x = x; + event.wheel.y = y; + event.wheel.time = time; + event.wheel.state = state; + event.wheel.x_delta = x_delta; + event.wheel.y_delta = y_delta; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, + jshort window) +{ + union android_event event; + + event.iconified.type = ANDROID_ICONIFIED; + event.iconified.window = window; +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, + jshort window) +{ + union android_event event; + + event.iconified.type = ANDROID_DEICONIFIED; + event.iconified.window = window; +} + #pragma clang diagnostic pop @@ -1979,10 +2117,22 @@ android_set_clip_rectangles (struct android_gc *gc, int clip_x_origin, } void -android_reparent_window (android_window w, android_window parent, +android_reparent_window (android_window w, android_window parent_handle, int x, int y) { - /* TODO */ + jobject window, parent; + jmethodID method; + + window = android_resolve_handle (w, ANDROID_HANDLE_WINDOW); + parent = android_resolve_handle (parent_handle, + ANDROID_HANDLE_WINDOW); + + method = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "reparentTo", + "(Lorg/gnu/emacs/EmacsWindow;II)V"); + (*android_java_env)->CallVoidMethod (android_java_env, window, + method, + parent, (jint) x, (jint) y); } /* Look up the method with SIGNATURE by NAME in CLASS. Abort if it @@ -2904,6 +3054,163 @@ android_put_image (android_pixmap handle, struct android_image *image) ANDROID_DELETE_LOCAL_REF (bitmap); } +void +android_bell (void) +{ + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.ring_bell); +} + +void +android_set_input_focus (android_window handle, unsigned long time) +{ + jobject window; + jmethodID make_input_focus; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + make_input_focus = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "makeInputFocus", "(J)V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + make_input_focus, (jlong) time); +} + +void +android_raise_window (android_window handle) +{ + jobject window; + jmethodID raise; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + raise = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "raise", "()V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + raise); +} + +void +android_lower_window (android_window handle) +{ + jobject window; + jmethodID lower; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + lower = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "lower", "()V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + lower); +} + +int +android_query_tree (android_window handle, android_window *root_return, + android_window *parent_return, + android_window **children_return, + unsigned int *nchildren_return) +{ + jobject window, array; + jsize nelements, i; + android_window *children; + jshort *shorts; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + + /* window can be NULL, so this is a service method. */ + array + = (*android_java_env)->CallObjectMethod (android_java_env, + emacs_service, + service_class.query_tree, + window); + if (!array) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + /* The first element of the array is the parent window. The rest + are the children. */ + nelements = (*android_java_env)->GetArrayLength (android_java_env, + array); + eassert (nelements); + + /* Now fill in the children. */ + children = xnmalloc (nelements - 1, sizeof *children); + + shorts + = (*android_java_env)->GetShortArrayElements (android_java_env, array, + NULL); + for (i = 1; i < nelements; ++i) + children[i] = shorts[i]; + + /* Finally, return the parent and other values. */ + *root_return = 0; + *parent_return = shorts[0]; + *children_return = children; + + /* Release the array contents. */ + (*android_java_env)->ReleaseShortArrayElements (android_java_env, array, + shorts, JNI_ABORT); + + ANDROID_DELETE_LOCAL_REF (array); + return 1; +} + +void +android_get_geometry (android_window handle, + android_window *root_return, + int *x_return, int *y_return, + unsigned int *width_return, + unsigned int *height_return, + unsigned int *border_width_return) +{ + jobject window; + jarray window_geometry; + jmethodID get_geometry; + jint *ints; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + get_geometry = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "getWindowGeometry", "()[I"); + + window_geometry + = (*android_java_env)->CallObjectMethod (android_java_env, + window, + get_geometry); + if (!window_geometry) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + /* window_geometry is an array containing x, y, width and + height. border_width is always 0 on Android. */ + eassert ((*android_java_env)->GetArrayLength (android_java_env, + window_geometry) + == 4); + + *root_return = 0; + *border_width_return = 0; + + ints + = (*android_java_env)->GetIntArrayElements (android_java_env, + window_geometry, + NULL); + + *x_return = ints[0]; + *y_return = ints[1]; + *width_return = ints[2]; + *height_return = ints[3]; + + (*android_java_env)->ReleaseIntArrayElements (android_java_env, + window_geometry, + ints, JNI_ABORT); + + /* Now free the local reference. */ + ANDROID_DELETE_LOCAL_REF (window_geometry); +} + /* Low level drawing primitives. */ @@ -2998,6 +3305,86 @@ android_damage_window (android_drawable handle, +/* Other misc system routines. */ + +int +android_get_screen_width (void) +{ + return (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.get_screen_width, + (jboolean) false); +} + +int +android_get_screen_height (void) +{ + return (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.get_screen_height, + (jboolean) false); +} + +int +android_get_mm_width (void) +{ + return (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.get_screen_width, + (jboolean) true); +} + +int +android_get_mm_height (void) +{ + return (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.get_screen_height, + (jboolean) true); +} + +bool +android_detect_mouse (void) +{ + return (*android_java_env)->CallBooleanMethod (android_java_env, + emacs_service, + service_class.detect_mouse); +} + +void +android_set_dont_focus_on_map (android_window handle, + bool no_focus_on_map) +{ + jmethodID method; + jobject window; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + method = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "setDontFocusOnMap", "(Z)V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + method, + (jboolean) no_focus_on_map); +} + +void +android_set_dont_accept_focus (android_window handle, + bool no_accept_focus) +{ + jmethodID method; + jobject window; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + method = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "setDontAcceptFocus", "(Z)V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + method, + (jboolean) no_accept_focus); +} + + + #undef faccessat /* Replace the system faccessat with one which understands AT_EACCESS. @@ -3015,6 +3402,128 @@ faccessat (int dirfd, const char *pathname, int mode, int flags) return real_faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); } + + +/* Directory listing emulation. */ + +struct android_dir +{ + /* The real DIR *, if it exists. */ + DIR *dir; + + /* Otherwise, the AAssetDir. */ + void *asset_dir; +}; + +/* Like opendir. However, return an asset directory if NAME points to + an asset. */ + +struct android_dir * +android_opendir (const char *name) +{ + struct android_dir *dir; + AAssetDir *asset_dir; + const char *asset_name; + + asset_name = android_get_asset_name (name); + + /* If the asset manager exists and NAME is an asset, return an asset + directory. */ + if (asset_manager && asset_name) + { + asset_dir = AAssetManager_openDir (asset_manager, + asset_name); + + if (!asset_dir) + { + errno = ENOENT; + return NULL; + } + + dir = xmalloc (sizeof *dir); + dir->dir = NULL; + dir->asset_dir = asset_dir; + return dir; + } + + /* Otherwise, open the directory normally. */ + dir = xmalloc (sizeof *dir); + dir->asset_dir = NULL; + dir->dir = opendir (name); + + if (!dir->dir) + { + xfree (dir); + return NULL; + } + + return dir; +} + +/* Like readdir, except it understands asset directories. */ + +struct dirent * +android_readdir (struct android_dir *dir) +{ + static struct dirent dirent; + const char *filename; + + if (dir->asset_dir) + { + filename = AAssetDir_getNextFileName (dir->asset_dir); + errno = 0; + + if (!filename) + return NULL; + + memset (&dirent, 0, sizeof dirent); + dirent.d_ino = 0; + dirent.d_off = 0; + dirent.d_reclen = sizeof dirent; + dirent.d_type = DT_UNKNOWN; + strncpy (dirent.d_name, filename, + sizeof dirent.d_name - 1); + return &dirent; + } + + return readdir (dir->dir); +} + +/* Like closedir, but it also closes asset manager directories. */ + +void +android_closedir (struct android_dir *dir) +{ + if (dir->dir) + closedir (dir->dir); + else + AAssetDir_close (dir->asset_dir); + + xfree (dir); +} + + + +/* emacs_abort implementation for Android. This logs a stack + trace. */ + +void +emacs_abort (void) +{ + volatile char *foo; + + __android_log_print (ANDROID_LOG_FATAL, __func__, + "emacs_abort called, please review the ensuing" + " stack trace"); + + /* Cause a NULL pointer dereference to make debuggerd generate a + tombstone. */ + foo = NULL; + *foo = '\0'; + + abort (); +} + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index 4cf194f23cf..cddd315b9a3 100644 --- a/src/android.h +++ b/src/android.h @@ -27,7 +27,9 @@ along with GNU Emacs. If not, see . */ #ifndef ANDROID_STUBIFY #include #include + #include +#include #include @@ -53,6 +55,7 @@ extern int android_fstat (int, struct stat *); extern int android_fstatat (int, const char *restrict, struct stat *restrict, int); extern int android_close (int); +extern int android_fclose (FILE *); extern const char *android_get_home_directory (void); extern double android_pixel_density_x, android_pixel_density_y; @@ -71,6 +74,26 @@ extern unsigned char *android_lock_bitmap (android_window, jobject *); extern void android_damage_window (android_window, struct android_rectangle *); +extern int android_get_screen_width (void); +extern int android_get_screen_height (void); +extern int android_get_mm_width (void); +extern int android_get_mm_height (void); +extern bool android_detect_mouse (void); + +extern void android_set_dont_focus_on_map (android_window, bool); +extern void android_set_dont_accept_focus (android_window, bool); + + + +/* Directory listing emulation. */ + +struct android_dir; + +extern struct android_dir *android_opendir (const char *); +extern struct dirent *android_readdir (struct android_dir *); +extern void android_closedir (struct android_dir *); + + #endif diff --git a/src/androidfns.c b/src/androidfns.c index 96c2746a21a..459e407b901 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -21,6 +21,7 @@ along with GNU Emacs. If not, see . */ #include #include "lisp.h" +#include "android.h" #include "androidterm.h" #include "blockinput.h" #include "keyboard.h" @@ -151,6 +152,36 @@ android_decode_color (struct frame *f, Lisp_Object color_name, int mono_color) signal_error ("Undefined color", color_name); } +static void +android_set_parent_frame (struct frame *f, Lisp_Object new_value, + Lisp_Object old_value) +{ + struct frame *p; + + p = NULL; + + if (!NILP (new_value) + && (!FRAMEP (new_value) + || !FRAME_LIVE_P (p = XFRAME (new_value)) + || !FRAME_ANDROID_P (p))) + { + store_frame_param (f, Qparent_frame, old_value); + error ("Invalid specification of `parent-frame'"); + } + + if (p != FRAME_PARENT_FRAME (f)) + { + block_input (); + android_reparent_window (FRAME_ANDROID_WINDOW (f), + (p ? FRAME_ANDROID_WINDOW (p) + : FRAME_DISPLAY_INFO (f)->root_window), + f->left_pos, f->top_pos); + unblock_input (); + + fset_parent_frame (f, new_value); + } +} + void android_implicitly_set_name (struct frame *f, Lisp_Object arg, Lisp_Object oldval) @@ -531,9 +562,9 @@ android_default_font_parameter (struct frame *f, Lisp_Object parms) if (! FONTP (font) && ! STRINGP (font)) { const char *names[] = { - "Droid Sans Mono", - "monospace", - "DroidSansMono", + "Droid Sans Mono-12", + "Monospace-12", + "DroidSansMono-12", NULL }; int i; @@ -1119,8 +1150,7 @@ DEFUN ("x-display-pixel-width", Fx_display_pixel_width, error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + return make_fixnum (android_get_screen_width ()); #endif } @@ -1133,8 +1163,7 @@ DEFUN ("x-display-pixel-height", Fx_display_pixel_height, error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + return make_fixnum (android_get_screen_height ()); #endif } @@ -1185,8 +1214,7 @@ DEFUN ("x-display-mm-width", Fx_display_mm_width, Sx_display_mm_width, error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + return make_fixnum (android_get_mm_width ()); #endif } @@ -1198,8 +1226,7 @@ DEFUN ("x-display-mm-height", Fx_display_mm_height, Sx_display_mm_height, error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + return make_fixnum (android_get_mm_height ()); #endif } @@ -1225,77 +1252,373 @@ DEFUN ("x-display-visual-class", Fx_display_visual_class, return Qtrue_color; } -DEFUN ("x-display-monitor-attributes-list", Fx_display_monitor_attributes_list, - Sx_display_monitor_attributes_list, +DEFUN ("android-display-monitor-attributes-list", + Fandroid_display_monitor_attributes_list, + Sandroid_display_monitor_attributes_list, 0, 1, 0, - doc: /* SKIP: real doc in xfns.c. */) + doc: /* Return a list of physical monitor attributes on the X display TERMINAL. + +The optional argument TERMINAL specifies which display to ask about. +TERMINAL should be a terminal object, a frame or a display name (a string). +If omitted or nil, that stands for the selected frame's display. + +Internal use only, use `display-monitor-attributes-list' instead. */) (Lisp_Object terminal) { #ifdef ANDROID_STUBIFY error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + struct MonitorInfo monitor; + + memset (&monitor, 0, sizeof monitor); + monitor.geom.width = android_get_screen_width (); + monitor.geom.height = android_get_screen_height (); + monitor.mm_width = android_get_mm_width (); + monitor.mm_height = android_get_mm_height (); + monitor.work = monitor.geom; + monitor.name = (char *) "Android device monitor"; + + /* What to do about monitor_frames? */ + return make_monitor_attribute_list (&monitor, 1, + 0, Qnil, NULL); #endif } -DEFUN ("x-frame-geometry", Fx_frame_geometry, Sx_frame_geometry, - 0, 1, 0, doc: /* SKIP: real doc in xfns.c. */) - (Lisp_Object terminal) +#ifndef ANDROID_STUBIFY + +static Lisp_Object +frame_geometry (Lisp_Object frame, Lisp_Object attribute) +{ + struct frame *f = decode_live_frame (frame); + android_window rootw; + unsigned int native_width, native_height, x_border_width = 0; + int x_native = 0, y_native = 0, xptr = 0, yptr = 0; + int left_off = 0, right_off = 0, top_off = 0, bottom_off = 0; + int outer_left, outer_top, outer_right, outer_bottom; + int native_left, native_top, native_right, native_bottom; + int inner_left, inner_top, inner_right, inner_bottom; + int internal_border_width; + bool menu_bar_external = false, tool_bar_external = false; + int menu_bar_height = 0, menu_bar_width = 0; + int tab_bar_height = 0, tab_bar_width = 0; + int tool_bar_height = 0, tool_bar_width = 0; + + if (FRAME_INITIAL_P (f) || !FRAME_ANDROID_P (f) + || !FRAME_ANDROID_WINDOW (f)) + return Qnil; + + block_input (); + android_get_geometry (FRAME_ANDROID_WINDOW (f), + &rootw, &x_native, &y_native, + &native_width, &native_height, &x_border_width); + unblock_input (); + + if (FRAME_PARENT_FRAME (f)) + { + Lisp_Object parent, edges; + + XSETFRAME (parent, FRAME_PARENT_FRAME (f)); + edges = Fandroid_frame_edges (parent, Qnative_edges); + if (!NILP (edges)) + { + x_native += XFIXNUM (Fnth (make_fixnum (0), edges)); + y_native += XFIXNUM (Fnth (make_fixnum (1), edges)); + } + + outer_left = x_native; + outer_top = y_native; + outer_right = outer_left + native_width + 2 * x_border_width; + outer_bottom = outer_top + native_height + 2 * x_border_width; + + native_left = x_native + x_border_width; + native_top = y_native + x_border_width; + native_right = native_left + native_width; + native_bottom = native_top + native_height; + } + else + { + outer_left = xptr; + outer_top = yptr; + outer_right = outer_left + left_off + native_width + right_off; + outer_bottom = outer_top + top_off + native_height + bottom_off; + + native_left = outer_left + left_off; + native_top = outer_top + top_off; + native_right = native_left + native_width; + native_bottom = native_top + native_height; + } + + internal_border_width = FRAME_INTERNAL_BORDER_WIDTH (f); + inner_left = native_left + internal_border_width; + inner_top = native_top + internal_border_width; + inner_right = native_right - internal_border_width; + inner_bottom = native_bottom - internal_border_width; + + menu_bar_height = FRAME_MENU_BAR_HEIGHT (f); + inner_top += menu_bar_height; + menu_bar_width = menu_bar_height ? native_width : 0; + + tab_bar_height = FRAME_TAB_BAR_HEIGHT (f); + tab_bar_width = (tab_bar_height + ? native_width - 2 * internal_border_width + : 0); + inner_top += tab_bar_height; + + tool_bar_height = FRAME_TOOL_BAR_HEIGHT (f); + tool_bar_width = (tool_bar_height + ? native_width - 2 * internal_border_width + : 0); + inner_top += tool_bar_height; + + /* Construct list. */ + if (EQ (attribute, Qouter_edges)) + return list4i (outer_left, outer_top, outer_right, outer_bottom); + else if (EQ (attribute, Qnative_edges)) + return list4i (native_left, native_top, native_right, native_bottom); + else if (EQ (attribute, Qinner_edges)) + return list4i (inner_left, inner_top, inner_right, inner_bottom); + else + return + list (Fcons (Qouter_position, + Fcons (make_fixnum (outer_left), + make_fixnum (outer_top))), + Fcons (Qouter_size, + Fcons (make_fixnum (outer_right - outer_left), + make_fixnum (outer_bottom - outer_top))), + /* Approximate. */ + Fcons (Qexternal_border_size, + Fcons (make_fixnum (right_off), + make_fixnum (bottom_off))), + Fcons (Qouter_border_width, make_fixnum (x_border_width)), + /* Approximate. */ + Fcons (Qtitle_bar_size, + Fcons (make_fixnum (0), + make_fixnum (top_off - bottom_off))), + Fcons (Qmenu_bar_external, menu_bar_external ? Qt : Qnil), + Fcons (Qmenu_bar_size, + Fcons (make_fixnum (menu_bar_width), + make_fixnum (menu_bar_height))), + Fcons (Qtab_bar_size, + Fcons (make_fixnum (tab_bar_width), + make_fixnum (tab_bar_height))), + Fcons (Qtool_bar_external, tool_bar_external ? Qt : Qnil), + Fcons (Qtool_bar_position, FRAME_TOOL_BAR_POSITION (f)), + Fcons (Qtool_bar_size, + Fcons (make_fixnum (tool_bar_width), + make_fixnum (tool_bar_height))), + Fcons (Qinternal_border_width, + make_fixnum (internal_border_width))); +} + +#endif + +DEFUN ("android-frame-geometry", Fandroid_frame_geometry, + Sandroid_frame_geometry, + 0, 1, 0, + doc: /* Return geometric attributes of FRAME. +FRAME must be a live frame and defaults to the selected one. The return +value is an association list of the attributes listed below. All height +and width values are in pixels. + +`outer-position' is a cons of the outer left and top edges of FRAME + relative to the origin - the position (0, 0) - of FRAME's display. + +`outer-size' is a cons of the outer width and height of FRAME. The + outer size includes the title bar and the external borders as well as + any menu and/or tool bar of frame. + +`external-border-size' is a cons of the horizontal and vertical width of + FRAME's external borders as supplied by the window manager. + +`title-bar-size' is a cons of the width and height of the title bar of + FRAME as supplied by the window manager. If both of them are zero, + FRAME has no title bar. If only the width is zero, Emacs was not + able to retrieve the width information. + +`menu-bar-external', if non-nil, means the menu bar is external (never + included in the inner edges of FRAME). + +`menu-bar-size' is a cons of the width and height of the menu bar of + FRAME. + +`tool-bar-external', if non-nil, means the tool bar is external (never + included in the inner edges of FRAME). + +`tool-bar-position' tells on which side the tool bar on FRAME is and can + be one of `left', `top', `right' or `bottom'. If this is nil, FRAME + has no tool bar. + +`tool-bar-size' is a cons of the width and height of the tool bar of + FRAME. + +`internal-border-width' is the width of the internal border of + FRAME. */) + (Lisp_Object frame) { #ifdef ANDROID_STUBIFY error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); + return frame_geometry (frame, Qnil); +#endif +} + +DEFUN ("android-frame-edges", Fandroid_frame_edges, Sandroid_frame_edges, 0, 2, 0, + doc: /* Return edge coordinates of FRAME. +FRAME must be a live frame and defaults to the selected one. The return +value is a list of the form (LEFT, TOP, RIGHT, BOTTOM). All values are +in pixels relative to the origin - the position (0, 0) - of FRAME's +display. + +If optional argument TYPE is the symbol `outer-edges', return the outer +edges of FRAME. The outer edges comprise the decorations of the window +manager (like the title bar or external borders) as well as any external +menu or tool bar of FRAME. If optional argument TYPE is the symbol +`native-edges' or nil, return the native edges of FRAME. The native +edges exclude the decorations of the window manager and any external +menu or tool bar of FRAME. If TYPE is the symbol `inner-edges', return +the inner edges of FRAME. These edges exclude title bar, any borders, +menu bar or tool bar of FRAME. */) + (Lisp_Object frame, Lisp_Object type) +{ +#ifndef ANDROID_STUBIFY + return frame_geometry (frame, ((EQ (type, Qouter_edges) + || EQ (type, Qinner_edges)) + ? type + : Qnative_edges)); +#else return Qnil; #endif } -DEFUN ("x-frame-list-z-order", Fx_frame_list_z_order, - Sx_frame_list_z_order, 0, 1, 0, - doc: /* SKIP: real doc in xfns.c. */) +#ifndef ANDROID_STUBIFY + +static Lisp_Object +android_frame_list_z_order (struct android_display_info *dpyinfo, + android_window window) +{ + android_window root, parent, *children; + unsigned int nchildren; + unsigned long i; + Lisp_Object frames; + + frames = Qnil; + + if (android_query_tree (window, &root, &parent, + &children, &nchildren)) + { + for (i = 0; i < nchildren; i++) + { + Lisp_Object frame, tail; + + FOR_EACH_FRAME (tail, frame) + { + struct frame *cf = XFRAME (frame); + + if (FRAME_ANDROID_P (cf) + && (FRAME_ANDROID_WINDOW (cf) == children[i])) + frames = Fcons (frame, frames); + } + } + + if (children) + xfree (children); + } + + return frames; +} + +#endif + +DEFUN ("android-frame-list-z-order", Fandroid_frame_list_z_order, + Sandroid_frame_list_z_order, 0, 1, 0, + doc: /* Return list of Emacs' frames, in Z (stacking) order. +The optional argument TERMINAL specifies which display to ask about. +TERMINAL should be either a frame or a display name (a string). If +omitted or nil, that stands for the selected frame's display. Return +nil if TERMINAL contains no Emacs frame. + +As a special case, if TERMINAL is non-nil and specifies a live frame, +return the child frames of that frame in Z (stacking) order. + +Frames are listed from topmost (first) to bottommost (last). + +On Android, the order of the frames returned is undefined unless +TERMINAL is a frame. */) (Lisp_Object terminal) { #ifdef ANDROID_STUBIFY error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + struct android_display_info *dpyinfo; + android_window window; + + dpyinfo = check_android_display_info (terminal); + + if (FRAMEP (terminal) && FRAME_LIVE_P (XFRAME (terminal))) + window = FRAME_ANDROID_WINDOW (XFRAME (terminal)); + else + window = dpyinfo->root_window; + + return android_frame_list_z_order (dpyinfo, window); #endif } -DEFUN ("x-frame-restack", Fx_frame_restack, Sx_frame_restack, 2, 3, 0, - doc: /* SKIP: real doc in xfns.c. */) +DEFUN ("android-frame-restack", Fandroid_frame_restack, + Sandroid_frame_restack, 2, 3, 0, + doc: /* Restack FRAME1 below FRAME2. +This means that if both frames are visible and the display areas of +these frames overlap, FRAME2 (partially) obscures FRAME1. If optional +third argument ABOVE is non-nil, restack FRAME1 above FRAME2. This +means that if both frames are visible and the display areas of these +frames overlap, FRAME1 (partially) obscures FRAME2. + +This may be thought of as an atomic action performed in two steps: The +first step removes FRAME1's window-step window from the display. The +second step reinserts FRAME1's window below (above if ABOVE is true) +that of FRAME2. Hence the position of FRAME2 in its display's Z +\(stacking) order relative to all other frames excluding FRAME1 remains +unaltered. + +The Android system refuses to restack windows, so this does not +work. */) (Lisp_Object frame1, Lisp_Object frame2, Lisp_Object frame3) { #ifdef ANDROID_STUBIFY error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); + /* This is not supported on Android because of limitations in the + platform that prevent ViewGroups from restacking + SurfaceViews. */ return Qnil; #endif } -DEFUN ("x-mouse-absolute-pixel-position", Fx_mouse_absolute_pixel_position, - Sx_mouse_absolute_pixel_position, 0, 0, 0, - doc: /* SKIP: real doc in xfns.c. */) +DEFUN ("android-mouse-absolute-pixel-position", + Fandroid_mouse_absolute_pixel_position, + Sandroid_mouse_absolute_pixel_position, 0, 0, 0, + doc: /* Return absolute position of mouse cursor in pixels. +The position is returned as a cons cell (X . Y) of the coordinates of +the mouse cursor position in pixels relative to a position (0, 0) of the +selected frame's display. This does not work on Android. */) (void) { - /* TODO: figure out how to implement this. */ + /* This cannot be implemented on Android. */ return Qnil; } -DEFUN ("x-set-mouse-absolute-pixel-position", - Fx_set_mouse_absolute_pixel_position, - Sx_set_mouse_absolute_pixel_position, 2, 2, 0, - doc: /* SKIP: real doc in xfns.c. */) +DEFUN ("android-set-mouse-absolute-pixel-position", + Fandroid_set_mouse_absolute_pixel_position, + Sandroid_set_mouse_absolute_pixel_position, 2, 2, 0, + doc: /* Move mouse pointer to a pixel position at (X, Y). The +coordinates X and Y are interpreted to start from the top-left corner +of the screen. This does not work on Android. */) (Lisp_Object x, Lisp_Object y) { - /* TODO: figure out how to implement this. */ + /* This cannot be implemented on Android. */ return Qnil; } @@ -1364,6 +1687,20 @@ DEFUN ("x-hide-tip", Fx_hide_tip, Sx_hide_tip, 0, 0, 0, #endif } +DEFUN ("android-detect-mouse", Fandroid_detect_mouse, + Sandroid_detect_mouse, 0, 0, 0, + doc: /* Figure out whether or not there is a mouse. +Return non-nil if a mouse is connected to this computer, and nil if +there is no mouse. */) + (void) +{ +#ifndef ANDROID_STUBIFY + return android_detect_mouse () ? Qt : Qnil; +#else + return Qnil; +#endif +} + #ifndef ANDROID_STUBIFY @@ -1579,7 +1916,7 @@ android_set_menu_bar_lines (struct frame *f, Lisp_Object value, y = FRAME_TOP_MARGIN_HEIGHT (f); block_input (); - android_clear_area (FRAME_ANDROID_WINDOW (f), + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, y, width, height); unblock_input (); } @@ -1590,7 +1927,7 @@ android_set_menu_bar_lines (struct frame *f, Lisp_Object value, height = nlines * FRAME_LINE_HEIGHT (f) - y; block_input (); - android_clear_area (FRAME_ANDROID_WINDOW (f), 0, y, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, y, width, height); unblock_input (); } @@ -1682,6 +2019,30 @@ android_set_alpha (struct frame *f, Lisp_Object arg, Lisp_Object oldval) } } +static void +android_set_no_focus_on_map (struct frame *f, Lisp_Object new_value, + Lisp_Object old_value) +{ + if (!EQ (new_value, old_value)) + { + android_set_dont_focus_on_map (FRAME_ANDROID_WINDOW (f), + new_value); + FRAME_NO_FOCUS_ON_MAP (f) = !NILP (new_value); + } +} + +static void +android_set_no_accept_focus (struct frame *f, Lisp_Object new_value, + Lisp_Object old_value) +{ + if (!EQ (new_value, old_value)) + { + android_set_dont_accept_focus (FRAME_ANDROID_WINDOW (f), + new_value); + FRAME_NO_ACCEPT_FOCUS (f) = !NILP (new_value); + } +} + frame_parm_handler android_frame_parm_handlers[] = { gui_set_autoraise, @@ -1724,16 +2085,16 @@ frame_parm_handler android_frame_parm_handlers[] = NULL, NULL, NULL, - NULL, /* x_set_undecorated, */ - NULL, /* x_set_parent_frame, */ - NULL, /* x_set_skip_taskbar, */ - NULL, /* x_set_no_focus_on_map, */ - NULL, /* x_set_no_accept_focus, */ - NULL, /* x_set_z_group, */ - NULL, /* x_set_override_redirect, */ + NULL, + android_set_parent_frame, + NULL, + android_set_no_focus_on_map, + android_set_no_accept_focus, + NULL, + NULL, gui_set_no_special_glyphs, - NULL, /* x_set_alpha_background, */ - NULL, /* x_set_use_frame_synchronization, */ + NULL, + NULL, }; #endif @@ -1766,14 +2127,16 @@ syms_of_androidfns (void) defsubr (&Sx_display_mm_height); defsubr (&Sx_display_backing_store); defsubr (&Sx_display_visual_class); - defsubr (&Sx_display_monitor_attributes_list); - defsubr (&Sx_frame_geometry); - defsubr (&Sx_frame_list_z_order); - defsubr (&Sx_frame_restack); - defsubr (&Sx_mouse_absolute_pixel_position); - defsubr (&Sx_set_mouse_absolute_pixel_position); + defsubr (&Sandroid_display_monitor_attributes_list); + defsubr (&Sandroid_frame_geometry); + defsubr (&Sandroid_frame_edges); + defsubr (&Sandroid_frame_list_z_order); + defsubr (&Sandroid_frame_restack); + defsubr (&Sandroid_mouse_absolute_pixel_position); + defsubr (&Sandroid_set_mouse_absolute_pixel_position); defsubr (&Sandroid_get_connection); defsubr (&Sx_display_list); defsubr (&Sx_show_tip); defsubr (&Sx_hide_tip); + defsubr (&Sandroid_detect_mouse); } diff --git a/src/androidfont.c b/src/androidfont.c index b2c83201b06..b0a9f994a85 100644 --- a/src/androidfont.c +++ b/src/androidfont.c @@ -1,4 +1,4 @@ -/* Communication module for Android terminals. +/* Android fallback font driver. Copyright (C) 2023 Free Software Foundation, Inc. @@ -17,6 +17,10 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Emacs. If not, see . */ +/* Due to the terrible nature of the Android Typeface subsystems, this + font driver is only used as a fallback when sfntfont-android.c + fails to enumerate any fonts at all. */ + #include #include "lisp.h" @@ -636,7 +640,7 @@ androidfont_draw (struct glyph_string *s, int from, int to, gcontext = android_resolve_handle (s->gc->gcontext, ANDROID_HANDLE_GCONTEXT); - drawable = android_resolve_handle (FRAME_ANDROID_WINDOW (s->f), + drawable = android_resolve_handle (FRAME_ANDROID_DRAWABLE (s->f), ANDROID_HANDLE_WINDOW); chars = (*android_java_env)->NewIntArray (android_java_env, to - from); diff --git a/src/androidgui.h b/src/androidgui.h index 7d045f7b450..422e72408c7 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -222,6 +222,12 @@ enum android_event_type ANDROID_MOTION_NOTIFY, ANDROID_BUTTON_PRESS, ANDROID_BUTTON_RELEASE, + ANDROID_TOUCH_DOWN, + ANDROID_TOUCH_UP, + ANDROID_TOUCH_MOVE, + ANDROID_WHEEL, + ANDROID_ICONIFIED, + ANDROID_DEICONIFIED, }; struct android_any_event @@ -312,6 +318,54 @@ struct android_button_event unsigned int button; }; +struct android_touch_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* Window associated with the event. */ + android_window window; + + /* X and Y coordinates of the event. */ + int x, y; + + /* Time of the event, and the pointer identifier. */ + unsigned long time; + + /* Index of the pointer being tracked. */ + unsigned int pointer_id; +}; + +struct android_wheel_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* Window associated with the event. */ + android_window window; + + /* X and Y coordinates of the event. */ + int x, y; + + /* Time of the event, and the pointer identifier. */ + unsigned long time; + + /* Modifier state at the time of the event. */ + int state; + + /* Motion alongside the X and Y axes. */ + double x_delta, y_delta; +}; + +struct android_iconify_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* Window associated with the event. */ + android_window window; +}; + union android_event { enum android_event_type type; @@ -323,8 +377,26 @@ union android_event struct android_crossing_event xcrossing; struct android_motion_event xmotion; struct android_button_event xbutton; + + /* This has no parallel in X, since the X model of having + monotonically increasing touch IDs can't work on Android. */ + struct android_touch_event touch; + + /* This has no parallel in X outside the X Input Extension, and + emulating the input extension interface would be awfully + complicated. */ + struct android_wheel_event wheel; + + /* This has no parallel in X because Android doesn't have window + properties. */ + struct android_iconify_event iconified; }; +enum + { + ANDROID_CURRENT_TIME = 0L, + }; + extern int android_pending (void); extern void android_next_event (union android_event *); @@ -396,6 +468,17 @@ extern void android_clear_area (android_window, int, int, unsigned int, extern android_pixmap android_create_bitmap_from_data (char *, unsigned int, unsigned int); +extern void android_bell (void); +extern void android_set_input_focus (android_window, unsigned long); +extern void android_raise_window (android_window); +extern void android_lower_window (android_window); +extern int android_query_tree (android_window, android_window *, + android_window *, android_window **, + unsigned int *); +extern void android_get_geometry (android_window, android_window *, + int *, int *, unsigned int *, + unsigned int *, unsigned int *); + #endif diff --git a/src/androidterm.c b/src/androidterm.c index 05fe7f01bf9..220858c0fec 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -19,6 +19,7 @@ along with GNU Emacs. If not, see . */ #include #include +#include #include "lisp.h" #include "androidterm.h" @@ -46,6 +47,11 @@ struct android_display_info *x_display_list; static bool any_help_event_p; +/* Counters for tallying up scroll wheel events if + mwheel_coalesce_scroll_events is true. */ + +static double wheel_event_x, wheel_event_y; + enum { ANDROID_EVENT_NORMAL, @@ -83,19 +89,133 @@ android_clear_frame (struct frame *f) /* Clearing the frame will erase any cursor, so mark them all as no longer visible. */ mark_window_cursors_off (XWINDOW (FRAME_ROOT_WINDOW (f))); - android_clear_window (FRAME_ANDROID_WINDOW (f)); + android_clear_window (FRAME_ANDROID_DRAWABLE (f)); +} + +static void +android_flash (struct frame *f) +{ + struct android_gc *gc; + struct android_gc_values values; + int rc; + fd_set fds; + + block_input (); + + values.function = ANDROID_GC_XOR; + values.foreground = (FRAME_FOREGROUND_PIXEL (f) + ^ FRAME_BACKGROUND_PIXEL (f)); + + gc = android_create_gc ((ANDROID_GC_FUNCTION + | ANDROID_GC_FOREGROUND), + &values); + + /* Get the height not including a menu bar widget. */ + int height = FRAME_PIXEL_HEIGHT (f); + /* Height of each line to flash. */ + int flash_height = FRAME_LINE_HEIGHT (f); + /* These will be the left and right margins of the rectangles. */ + int flash_left = FRAME_INTERNAL_BORDER_WIDTH (f); + int flash_right = FRAME_PIXEL_WIDTH (f) - FRAME_INTERNAL_BORDER_WIDTH (f); + int width = flash_right - flash_left; + + /* If window is tall, flash top and bottom line. */ + if (height > 3 * FRAME_LINE_HEIGHT (f)) + { + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, + (FRAME_INTERNAL_BORDER_WIDTH (f) + + FRAME_TOP_MARGIN_HEIGHT (f)), + width, flash_height); + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, + (height - flash_height + - FRAME_INTERNAL_BORDER_WIDTH (f)), + width, flash_height); + + } + else + /* If it is short, flash it all. */ + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, FRAME_INTERNAL_BORDER_WIDTH (f), + width, (height - 2 + * FRAME_INTERNAL_BORDER_WIDTH (f))); + + flush_frame (f); + + struct timespec delay = make_timespec (0, 150 * 1000 * 1000); + struct timespec wakeup = timespec_add (current_timespec (), delay); + + /* Keep waiting until past the time wakeup or any input gets + available. */ + while (! detect_input_pending ()) + { + struct timespec current = current_timespec (); + struct timespec timeout; + + /* Break if result would not be positive. */ + if (timespec_cmp (wakeup, current) <= 0) + break; + + /* How long `select' should wait. */ + timeout = make_timespec (0, 10 * 1000 * 1000); + + /* Wait for some input to become available on the X + connection. */ + FD_ZERO (&fds); + + /* Try to wait that long--but we might wake up sooner. */ + rc = pselect (0, &fds, NULL, NULL, &timeout, NULL); + + /* Some input is available, exit the visible bell. */ + if (rc >= 0) + break; + } + + /* If window is tall, flash top and bottom line. */ + if (height > 3 * FRAME_LINE_HEIGHT (f)) + { + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, + (FRAME_INTERNAL_BORDER_WIDTH (f) + + FRAME_TOP_MARGIN_HEIGHT (f)), + width, flash_height); + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, + (height - flash_height + - FRAME_INTERNAL_BORDER_WIDTH (f)), + width, flash_height); + } + else + /* If it is short, flash it all. */ + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, FRAME_INTERNAL_BORDER_WIDTH (f), + width, (height - 2 + * FRAME_INTERNAL_BORDER_WIDTH (f))); + + android_free_gc (gc); + flush_frame (f); + + unblock_input (); } static void android_ring_bell (struct frame *f) { - /* TODO */ + if (visible_bell) + android_flash (f); + else + { + block_input (); + android_bell (); + unblock_input (); + } } static void android_toggle_invisible_pointer (struct frame *f, bool invisible) { - /* TODO */ + } /* Start an update of frame F. This function is installed as a hook @@ -127,10 +247,17 @@ show_back_buffer (struct frame *f) { struct android_swap_info swap_info; + /* Somehow Android frames can be swapped while garbaged. */ + if (FRAME_GARBAGED_P (f)) + return; + memset (&swap_info, 0, sizeof (swap_info)); swap_info.swap_window = FRAME_ANDROID_WINDOW (f); swap_info.swap_action = ANDROID_COPIED; android_swap_buffers (&swap_info, 1); + + /* Now the back buffer no longer needs to be flipped. */ + FRAME_ANDROID_NEED_BUFFER_FLIP (f) = false; } /* Flip back buffers on F if it has undrawn content. */ @@ -142,7 +269,8 @@ android_flush_dirty_back_buffer_on (struct frame *f) || buffer_flipping_blocked_p () /* If the frame is not already up to date, do not flush buffers on input, as that will result in flicker. */ - || !FRAME_ANDROID_COMPLETE_P (f)) + || !FRAME_ANDROID_COMPLETE_P (f) + && FRAME_ANDROID_NEED_BUFFER_FLIP (f)) return; show_back_buffer (f); @@ -174,13 +302,13 @@ static void android_frame_rehighlight (struct android_display_info *); static void android_lower_frame (struct frame *f) { - /* TODO. */ + android_lower_window (FRAME_ANDROID_WINDOW (f)); } static void android_raise_frame (struct frame *f) { - /* TODO. */ + android_raise_window (FRAME_ANDROID_WINDOW (f)); } static void @@ -354,6 +482,46 @@ android_construct_mouse_click (struct input_event *result, return Qnil; } +/* Generate a TOUCHSCREEN_UPDATE_EVENT for all pressed tools in FRAME. + Return the event in IE. Do not set IE->timestamp, as that is left + to the caller. */ + +static void +android_update_tools (struct frame *f, struct input_event *ie) +{ + struct android_touch_point *touchpoint; + + ie->kind = TOUCHSCREEN_UPDATE_EVENT; + XSETFRAME (ie->frame_or_window, f); + ie->arg = Qnil; + + /* Build the list of active touches. */ + for (touchpoint = FRAME_OUTPUT_DATA (f)->touch_points; + touchpoint; touchpoint = touchpoint->next) + ie->arg = Fcons (list3i (touchpoint->x, + touchpoint->y, + touchpoint->tool_id), + ie->arg); +} + +/* Find and return an existing tool pressed against FRAME, identified + by POINTER_ID. Return NULL if no tool by that ID was found. */ + +static struct android_touch_point * +android_find_tool (struct frame *f, int pointer_id) +{ + struct android_touch_point *touchpoint; + + for (touchpoint = FRAME_OUTPUT_DATA (f)->touch_points; + touchpoint; touchpoint = touchpoint->next) + { + if (touchpoint->tool_id == pointer_id) + return touchpoint; + } + + return NULL; +} + static int handle_one_android_event (struct android_display_info *dpyinfo, union android_event *event, int *finish, @@ -364,6 +532,10 @@ handle_one_android_event (struct android_display_info *dpyinfo, Mouse_HLInfo *hlinfo; union buffered_input_event inev; int modifiers, count, do_help; + struct android_touch_point *touchpoint, **last; + Lisp_Object window; + int scroll_height; + double scroll_unit; /* It is okay for this to not resemble handle_one_xevent so much. Differences in event handling code are much less nasty than @@ -633,6 +805,28 @@ handle_one_android_event (struct android_display_info *dpyinfo, f = mouse_or_wdesc_frame (dpyinfo, event->xbutton.window); + if (f && event->xbutton.type == ANDROID_BUTTON_PRESS + && !popup_activated () + /* && !x_window_to_scroll_bar (event->xbutton.display, */ + /* event->xbutton.window, 2) */ + && !FRAME_NO_ACCEPT_FOCUS (f)) + { + /* When clicking into a child frame or when clicking + into a parent frame with the child frame selected and + `no-accept-focus' is not set, select the clicked + frame. */ + struct frame *hf = dpyinfo->highlight_frame; + + if (FRAME_PARENT_FRAME (f) || (hf && frame_ancestor_p (f, hf))) + { + android_set_input_focus (FRAME_ANDROID_WINDOW (f), + event->xbutton.time); + + if (FRAME_PARENT_FRAME (f)) + android_raise_window (FRAME_ANDROID_WINDOW (f)); + } + } + if (f) { /* Is this in the tab-bar? */ @@ -715,6 +909,223 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto OTHER; + /* Touch events. The events here don't parallel X so much. */ + case ANDROID_TOUCH_DOWN: + + if (!any) + goto OTHER; + + /* This event is sent when a tool is put on the screen. X and Y + are the location of the finger, and pointer_id identifies the + tool for as long as it is still held down. First, see if the + touch point already exists and can be reused (this shouldn't + happen, but be safe.) */ + + touchpoint = android_find_tool (any, event->touch.pointer_id); + + if (touchpoint) + { + /* Simply update the tool position and send an update. */ + touchpoint->x = event->touch.x; + touchpoint->y = event->touch.x; + android_update_tools (any, &inev.ie); + inev.ie.timestamp = event->touch.time; + + goto OTHER; + } + + /* Otherwise, link a new touchpoint onto the output's list of + pressed tools. */ + + touchpoint = xmalloc (sizeof *touchpoint); + touchpoint->tool_id = event->touch.pointer_id; + touchpoint->x = event->touch.x; + touchpoint->y = event->touch.x; + touchpoint->next = FRAME_OUTPUT_DATA (any)->touch_points; + FRAME_OUTPUT_DATA (any)->touch_points = touchpoint; + + /* Now generate the Emacs event. */ + inev.ie.kind = TOUCHSCREEN_BEGIN_EVENT; + inev.ie.timestamp = event->touch.time; + XSETFRAME (inev.ie.frame_or_window, any); + XSETINT (inev.ie.x, event->touch.x); + XSETINT (inev.ie.y, event->touch.y); + XSETINT (inev.ie.arg, event->touch.pointer_id); + + goto OTHER; + + case ANDROID_TOUCH_MOVE: + + if (!any) + goto OTHER; + + /* Look for the tool that moved. */ + + touchpoint = android_find_tool (any, event->touch.pointer_id); + + /* If it doesn't exist, skip processing this event. */ + + if (!touchpoint) + goto OTHER; + + /* Otherwise, update the position and send the update event. */ + + touchpoint->x = event->touch.x; + touchpoint->y = event->touch.y; + android_update_tools (any, &inev.ie); + inev.ie.timestamp = event->touch.time; + + goto OTHER; + + case ANDROID_TOUCH_UP: + + if (!any) + goto OTHER; + + /* Now find and unlink the tool in question. */ + + last = &FRAME_OUTPUT_DATA (any)->touch_points; + while ((touchpoint = *last)) + { + if (touchpoint->tool_id == event->touch.pointer_id) + { + *last = touchpoint->next; + + /* The tool was unlinked. Free it and generate the + appropriate Emacs event. */ + xfree (touchpoint); + inev.ie.kind = TOUCHSCREEN_END_EVENT; + inev.ie.timestamp = event->touch.time; + + XSETFRAME (inev.ie.frame_or_window, any); + XSETINT (inev.ie.x, event->touch.x); + XSETINT (inev.ie.y, event->touch.y); + XSETINT (inev.ie.arg, event->touch.pointer_id); + + /* Break out of the loop. */ + goto OTHER; + } + else + last = &touchpoint->next; + } + + /* No touch point was found. This shouldn't happen. */ + goto OTHER; + + /* Wheel motion. The events here don't parallel X because + Android doesn't have scroll valuators. */ + + case ANDROID_WHEEL: + + if (!any) + goto OTHER; + + if (fabs (event->wheel.x_delta) > 0 + || fabs (event->wheel.y_delta) > 0) + { + if (mwheel_coalesce_scroll_events) + { + if (signbit (event->wheel.x_delta) + != signbit (wheel_event_x)) + wheel_event_x = 0.0; + + if (signbit (event->wheel.y_delta) + != signbit (wheel_event_y)) + wheel_event_y = 0.0; + + /* Tally up deltas until one of them exceeds 1.0. */ + wheel_event_x += event->wheel.x_delta; + wheel_event_y += event->wheel.y_delta; + + if (fabs (wheel_event_x) < 1.0 + && fabs (wheel_event_y) < 1.0) + goto OTHER; + } + else + { + /* Use the deltas in the event. */ + wheel_event_x = event->wheel.x_delta; + wheel_event_y = event->wheel.y_delta; + } + + /* Determine what kind of event to send. */ + inev.ie.kind = ((fabs (wheel_event_y) + >= fabs (wheel_event_x)) + ? WHEEL_EVENT : HORIZ_WHEEL_EVENT); + inev.ie.timestamp = event->wheel.time; + + /* Set the event coordinates. */ + XSETINT (inev.ie.x, event->wheel.x); + XSETINT (inev.ie.y, event->wheel.y); + + /* Set the frame. */ + XSETFRAME (inev.ie.frame_or_window, any); + + /* Figure out the scroll direction. */ + inev.ie.modifiers = (signbit ((fabs (wheel_event_x) + >= fabs (wheel_event_y)) + ? wheel_event_x + : wheel_event_y) + ? down_modifier : up_modifier); + + /* Figure out how much to scale the deltas by. */ + window = window_from_coordinates (any, event->wheel.x, + event->wheel.y, NULL, + false, false); + + if (WINDOWP (window)) + scroll_height = XWINDOW (window)->pixel_height; + else + /* EVENT_X and EVENT_Y can be outside the + frame if F holds the input grab, so fall + back to the height of the frame instead. */ + scroll_height = FRAME_PIXEL_HEIGHT (any); + + scroll_unit = pow (scroll_height, 2.0 / 3.0); + + /* Add the keyboard modifiers. */ + inev.ie.modifiers + |= android_android_to_emacs_modifiers (dpyinfo, + event->wheel.state); + + /* Finally include the scroll deltas. */ + inev.ie.arg = list3 (Qnil, + make_float (wheel_event_x + * scroll_unit), + make_float (wheel_event_y + * scroll_unit)); + + wheel_event_x = 0.0; + wheel_event_y = 0.0; + } + + goto OTHER; + + /* Iconification. This is vastly simpler than on X. */ + case ANDROID_ICONIFIED: + + if (FRAME_ICONIFIED_P (any)) + goto OTHER; + + SET_FRAME_VISIBLE (any, false); + SET_FRAME_ICONIFIED (any, true); + + inev.ie.kind = ICONIFY_EVENT; + XSETFRAME (inev.ie.frame_or_window, any); + goto OTHER; + + case ANDROID_DEICONIFIED: + + if (!FRAME_ICONIFIED_P (any)) + goto OTHER; + + SET_FRAME_VISIBLE (any, true); + SET_FRAME_ICONIFIED (any, false); + + inev.ie.kind = DEICONIFY_EVENT; + XSETFRAME (inev.ie.frame_or_window, any); + goto OTHER; + default: goto OTHER; } @@ -781,8 +1192,7 @@ android_read_socket (struct terminal *terminal, now. */ if (dpyinfo->pending_autoraise_frame) { - /* android_raise_frame (dpyinfo->pending_autoraise_frame); - TODO */ + android_raise_frame (dpyinfo->pending_autoraise_frame); dpyinfo->pending_autoraise_frame = NULL; } @@ -796,7 +1206,8 @@ android_frame_up_to_date (struct frame *f) block_input (); FRAME_MOUSE_UPDATE (f); - if (!buffer_flipping_blocked_p ()) + if (!buffer_flipping_blocked_p () + && FRAME_ANDROID_NEED_BUFFER_FLIP (f)) show_back_buffer (f); /* The frame is now complete, as its contents have been drawn. */ @@ -808,7 +1219,10 @@ static void android_buffer_flipping_unblocked_hook (struct frame *f) { block_input (); - show_back_buffer (f); + + if (FRAME_ANDROID_NEED_BUFFER_FLIP (f)) + show_back_buffer (f); + unblock_input (); } @@ -935,7 +1349,25 @@ android_get_focus_frame (struct frame *f) static void android_focus_frame (struct frame *f, bool noactivate) { - /* TODO */ + /* Set the input focus to the frame's window. The system only lets + this work on child frames. */ + android_set_input_focus (FRAME_ANDROID_WINDOW (f), + ANDROID_CURRENT_TIME); +} + +/* The two procedures below only have to update the cursor on Android, + as there are no window borders there. */ + +static void +android_frame_highlight (struct frame *f) +{ + gui_update_cursor (f, true); +} + +static void +android_frame_unhighlight (struct frame *f) +{ + gui_update_cursor (f, true); } static void @@ -963,12 +1395,10 @@ android_frame_rehighlight (struct android_display_info *dpyinfo) if (dpyinfo->highlight_frame != old_highlight) { /* This is not yet required on Android. */ -#if 0 if (old_highlight) android_frame_unhighlight (old_highlight); if (dpyinfo->highlight_frame) android_frame_highlight (dpyinfo->highlight_frame); -#endif } } @@ -1027,7 +1457,8 @@ android_fullscreen_hook (struct frame *f) void android_iconify_frame (struct frame *f) { - /* TODO */ + /* This really doesn't work on Android. */ + error ("Can't notify window manager of iconification"); } static void @@ -1149,7 +1580,7 @@ android_set_offset (struct frame *f, int xoff, int yoff, static void android_set_alpha (struct frame *f) { - /* TODO */ + /* Not supported on Android. */ } static Lisp_Object @@ -1215,6 +1646,7 @@ android_free_frame_resources (struct frame *f) { struct android_display_info *dpyinfo; Mouse_HLInfo *hlinfo; + struct android_touch_point *last, *next; dpyinfo = FRAME_DISPLAY_INFO (f); hlinfo = &dpyinfo->mouse_highlight; @@ -1256,6 +1688,18 @@ android_free_frame_resources (struct frame *f) if (f == dpyinfo->last_mouse_frame) dpyinfo->last_mouse_frame = NULL; + /* Free all tool presses currently active on this frame. */ + next = FRAME_OUTPUT_DATA (f)->touch_points; + while (next) + { + last = next; + next = next->next; + xfree (last); + } + + /* Clear this in case unblock_input reads events. */ + FRAME_OUTPUT_DATA (f)->touch_points = NULL; + unblock_input (); } @@ -1316,8 +1760,8 @@ android_scroll_run (struct window *w, struct run *run) /* Cursor off. Will be switched on again in gui_update_window_end. */ gui_clear_cursor (w); - android_copy_area (FRAME_ANDROID_WINDOW (f), - FRAME_ANDROID_WINDOW (f), + android_copy_area (FRAME_ANDROID_DRAWABLE (f), + FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x, from_y, width, height, x, to_y); @@ -1337,7 +1781,9 @@ static void android_flip_and_flush (struct frame *f) { block_input (); - show_back_buffer (f); + + if (FRAME_ANDROID_NEED_BUFFER_FLIP (f)) + show_back_buffer (f); /* The frame is complete again as its contents were just flushed. */ @@ -1355,7 +1801,7 @@ android_clear_rectangle (struct frame *f, struct android_gc *gc, int x, | ANDROID_GC_FOREGROUND), &xgcv); android_set_foreground (gc, xgcv.background); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, x, y, width, height); android_set_foreground (gc, xgcv.foreground); } @@ -1405,7 +1851,7 @@ android_draw_fringe_bitmap (struct window *w, struct glyph_row *row, if (face->stipple) { android_set_fill_style (face->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), face->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), face->gc, p->bx, p->by, p->nx, p->ny); android_set_fill_style (face->gc, ANDROID_FILL_SOLID); @@ -1428,7 +1874,7 @@ android_draw_fringe_bitmap (struct window *w, struct glyph_row *row, unsigned long background, cursor_pixel; int depth; - drawable = FRAME_ANDROID_WINDOW (f); + drawable = FRAME_ANDROID_DRAWABLE (f); clipmask = ANDROID_NONE; background = face->background; cursor_pixel = f->output_data.android->cursor_pixel; @@ -1697,7 +2143,7 @@ android_draw_glyph_string_background (struct glyph_string *s, bool force_p) { /* Fill background with a stipple pattern. */ android_set_fill_style (s->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->x, s->y + box_line_width, s->background_width, s->height - 2 * box_line_width); @@ -1734,7 +2180,7 @@ android_fill_triangle (struct frame *f, struct android_gc *gc, abc[1] = point2; abc[2] = point3; - android_fill_polygon (FRAME_ANDROID_WINDOW (f), + android_fill_polygon (FRAME_ANDROID_DRAWABLE (f), gc, abc, 3, ANDROID_CONVEX, ANDROID_COORD_MODE_ORIGIN); } @@ -1776,7 +2222,7 @@ android_clear_point (struct frame *f, struct android_gc *gc, android_get_gc_values (gc, ANDROID_GC_BACKGROUND | ANDROID_GC_FOREGROUND, &xgcv); android_set_foreground (gc, xgcv.background); - android_draw_point (FRAME_ANDROID_WINDOW (f), gc, x, y); + android_draw_point (FRAME_ANDROID_DRAWABLE (f), gc, x, y); android_set_foreground (gc, xgcv.foreground); } @@ -1798,7 +2244,7 @@ android_draw_relief_rect (struct frame *f, int left_x, int top_y, int right_x, black_gc = f->output_data.android->black_relief.gc; normal_gc = f->output_data.android->normal_gc; - drawable = FRAME_ANDROID_WINDOW (f); + drawable = FRAME_ANDROID_DRAWABLE (f); android_set_clip_rectangles (white_gc, 0, 0, clip_rect, 1); android_set_clip_rectangles (black_gc, 0, 0, clip_rect, 1); @@ -1811,11 +2257,11 @@ android_draw_relief_rect (struct frame *f, int left_x, int top_y, int right_x, /* Draw lines. */ if (top_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, left_x, top_y, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, left_x, top_y, right_x - left_x + 1, hwidth); if (left_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, left_x, top_y, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, left_x, top_y, vwidth, bottom_y - top_y + 1); if (raised_p) @@ -1824,12 +2270,12 @@ android_draw_relief_rect (struct frame *f, int left_x, int top_y, int right_x, gc = white_gc; if (bot_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, left_x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, left_x, bottom_y - hwidth + 1, right_x - left_x + 1, hwidth); if (right_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, right_x - vwidth + 1, top_y, vwidth, bottom_y - top_y + 1); @@ -1853,7 +2299,7 @@ android_draw_relief_rect (struct frame *f, int left_x, int top_y, int right_x, if (top_p && left_p && bot_p && right_p && hwidth > 1 && vwidth > 1) - android_draw_rectangle (FRAME_ANDROID_WINDOW (f), + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (f), black_gc, left_x, top_y, right_x - left_x, bottom_y - top_y); else @@ -1913,22 +2359,22 @@ android_draw_box_rect (struct glyph_string *s, android_set_clip_rectangles (s->gc, 0, 0, clip_rect, 1); /* Top. */ - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, left_x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, left_x, left_x, right_x - left_x + 1, hwidth); /* Left. */ if (left_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, left_x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, left_x, top_y, vwidth, bottom_y - top_y + 1); /* Bottom. */ - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, left_x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, left_x, bottom_y - hwidth + 1, right_x - left_x + 1, hwidth); /* Right. */ if (right_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, right_x - vwidth + 1, top_y, vwidth, bottom_y - top_y + 1); @@ -2153,7 +2599,7 @@ android_draw_glyph_string_bg_rect (struct glyph_string *s, int x, int y, { /* Fill background with a stipple pattern. */ android_set_fill_style (s->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, y, w, h); android_set_fill_style (s->gc, ANDROID_FILL_SOLID); } @@ -2294,7 +2740,7 @@ android_draw_image_foreground (struct glyph_string *s) if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) android_copy_area (s->img->pixmap, - FRAME_ANDROID_WINDOW (s->f), + FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->slice.x + r.x - x, s->slice.y + r.y - y, r.width, r.height, r.x, r.y); @@ -2307,7 +2753,7 @@ android_draw_image_foreground (struct glyph_string *s) if (s->hl == DRAW_CURSOR && !s->img->mask) { int relief = eabs (s->img->relief); - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x - relief, y - relief, s->slice.width + relief*2 - 1, s->slice.height + relief*2 - 1); @@ -2317,7 +2763,7 @@ android_draw_image_foreground (struct glyph_string *s) } else /* Draw a rectangle if image could not be loaded. */ - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, x, y, + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, y, s->slice.width - 1, s->slice.height - 1); } @@ -2444,7 +2890,7 @@ android_draw_stretch_glyph_string (struct glyph_string *s) { /* Fill background with a stipple pattern. */ android_set_fill_style (gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), gc, x, y, w, h); android_set_fill_style (gc, ANDROID_FILL_SOLID); @@ -2457,7 +2903,7 @@ android_draw_stretch_glyph_string (struct glyph_string *s) | ANDROID_GC_BACKGROUND), &xgcv); android_set_foreground (gc, xgcv.background); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), gc, x, y, w, h); android_set_foreground (gc, xgcv.foreground); } @@ -2536,7 +2982,7 @@ android_draw_underwave (struct glyph_string *s, int decoration_width) while (x1 <= xmax) { - android_draw_line (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_draw_line (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x1, y1, x2, y2); x1 = x2, y1 = y2; x2 += dx, y2 = y0 + odd*dy; @@ -2567,7 +3013,7 @@ android_draw_glyph_string_foreground (struct glyph_string *s) for (i = 0; i < s->nchars; ++i) { struct glyph *g = s->first_glyph + i; - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, s->y, g->pixel_width - 1, s->height - 1); @@ -2618,7 +3064,7 @@ android_draw_composite_glyph_string_foreground (struct glyph_string *s) if (s->font_not_found_p) { if (s->cmp_from == 0) - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, s->y, s->width - 1, s->height - 1); } @@ -2754,7 +3200,7 @@ android_draw_glyphless_glyph_string_foreground (struct glyph_string *s) false); } if (glyph->u.glyphless.method != GLYPHLESS_DISPLAY_THIN_SPACE) - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, s->ybase - glyph->ascent, glyph->pixel_width - 1, glyph->ascent + glyph->descent - 1); @@ -2987,14 +3433,14 @@ android_draw_glyph_string (struct glyph_string *s) s->underline_position = position; y = s->ybase + position; if (s->face->underline_defaulted_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->x, y, decoration_width, thickness); else { struct android_gc_values xgcv; android_get_gc_values (s->gc, ANDROID_GC_FOREGROUND, &xgcv); android_set_foreground (s->gc, s->face->underline_color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->x, y, decoration_width, thickness); android_set_foreground (s->gc, xgcv.foreground); } @@ -3006,7 +3452,7 @@ android_draw_glyph_string (struct glyph_string *s) unsigned long dy = 0, h = 1; if (s->face->overline_color_defaulted_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->x, s->y + dy, decoration_width, h); else @@ -3014,8 +3460,8 @@ android_draw_glyph_string (struct glyph_string *s) struct android_gc_values xgcv; android_get_gc_values (s->gc, ANDROID_GC_FOREGROUND, &xgcv); android_set_foreground (s->gc, s->face->overline_color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, s->x, - s->y + dy, decoration_width, h); + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, + s->x, s->y + dy, decoration_width, h); android_set_foreground (s->gc, xgcv.foreground); } } @@ -3044,8 +3490,9 @@ android_draw_glyph_string (struct glyph_string *s) struct android_gc_values xgcv; android_get_gc_values (s->gc, ANDROID_GC_FOREGROUND, &xgcv); android_set_foreground (s->gc, s->face->strike_through_color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, s->x, - glyph_y + dy, decoration_width, h); + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, + s->x, glyph_y + dy, decoration_width, + h); android_set_foreground (s->gc, xgcv.foreground); } } @@ -3125,7 +3572,7 @@ static void android_clear_frame_area (struct frame *f, int x, int y, int width, int height) { - android_clear_area (FRAME_ANDROID_WINDOW (f), + android_clear_area (FRAME_ANDROID_DRAWABLE (f), x, y, width, height); } @@ -3154,25 +3601,25 @@ android_clear_under_internal_border (struct frame *f) struct android_gc *gc = f->output_data.android->normal_gc; android_set_foreground (gc, color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, 0, margin, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, 0, margin, width, border); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, 0, 0, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, 0, 0, border, height); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, width - border, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, width - border, 0, border, height); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, 0, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, 0, height - border, width, border); android_set_foreground (gc, FRAME_FOREGROUND_PIXEL (f)); } else { - android_clear_area (FRAME_ANDROID_WINDOW (f), 0, 0, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, 0, border, height); - android_clear_area (FRAME_ANDROID_WINDOW (f), 0, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, margin, width, border); - android_clear_area (FRAME_ANDROID_WINDOW (f), width - border, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), width - border, 0, border, height); - android_clear_area (FRAME_ANDROID_WINDOW (f), 0, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, height - border, width, border); } } @@ -3221,7 +3668,7 @@ android_draw_hollow_cursor (struct window *w, struct glyph_row *row) } /* Set clipping, draw the rectangle, and reset clipping again. */ android_clip_to_row (w, row, TEXT_AREA, gc); - android_draw_rectangle (FRAME_ANDROID_WINDOW (f), gc, x, y, wd, h - 1); + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, x, y, wd, h - 1); android_reset_clip_rectangles (f, gc); } @@ -3295,7 +3742,7 @@ android_draw_bar_cursor (struct window *w, struct glyph_row *row, int width, if ((cursor_glyph->resolved_level & 1) != 0) x += cursor_glyph->pixel_width - width; - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, x, WINDOW_TO_FRAME_PIXEL_Y (w, w->phys_cursor.y), width, row->height); } @@ -3318,7 +3765,7 @@ android_draw_bar_cursor (struct window *w, struct glyph_row *row, int width, if ((cursor_glyph->resolved_level & 1) != 0 && cursor_glyph->pixel_width > w->phys_cursor_width - 1) x += cursor_glyph->pixel_width - w->phys_cursor_width + 1; - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, x, cursor_start_y, w->phys_cursor_width - 1, width); } @@ -3387,7 +3834,7 @@ android_draw_vertical_window_border (struct window *w, int x, int y0, int y1) android_set_foreground (f->output_data.android->normal_gc, face->foreground); - android_draw_line (FRAME_ANDROID_WINDOW (f), + android_draw_line (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x, y0, x, y1); } @@ -3415,17 +3862,17 @@ android_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1) { android_set_foreground (f->output_data.android->normal_gc, color_first); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y0, 1, y1 - y0); android_set_foreground (f->output_data.android->normal_gc, color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0 + 1, y0, x1 - x0 - 2, y1 - y0); android_set_foreground (f->output_data.android->normal_gc, color_last); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x1 - 1, y0, 1, y1 - y0); } @@ -3435,16 +3882,16 @@ android_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1) { android_set_foreground (f->output_data.android->normal_gc, color_first); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y0, x1 - x0, 1); android_set_foreground (f->output_data.android->normal_gc, color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y0 + 1, x1 - x0, y1 - y0 - 2); android_set_foreground (f->output_data.android->normal_gc, color_last); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y1 - 1, x1 - x0, 1); } @@ -3453,7 +3900,7 @@ android_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1) /* In any other case do not draw the first and last pixels differently. */ android_set_foreground (f->output_data.android->normal_gc, color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y0, x1 - x0, y1 - y0); } diff --git a/src/androidterm.h b/src/androidterm.h index 814bcf9f08c..ebde15c40a8 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -134,6 +134,18 @@ struct android_display_info Time last_mouse_movement_time; }; +/* Structure representing a single tool (finger or stylus) pressed + onto a frame. */ + +struct android_touch_point +{ + /* The next tool on this list. */ + struct android_touch_point *next; + + /* The tool ID and the last known X and Y positions. */ + int tool_id, x, y; +}; + struct android_output { /* Graphics contexts for the default font. */ @@ -201,6 +213,10 @@ struct android_output input. */ bool_bf complete : 1; + /* True that indicates whether or not a buffer flip is required + because the frame contents have been dirtied. */ + bool_bf need_buffer_flip : 1; + /* Relief GCs, colors etc. */ struct relief { struct android_gc *gc; @@ -214,6 +230,10 @@ struct android_output /* Focus state. Only present for consistency with X; it is actually a boolean. */ int focus_state; + + /* List of all tools (either styluses or fingers) pressed onto the + frame. */ + struct android_touch_point *touch_points; }; enum @@ -240,6 +260,14 @@ enum #define FRAME_ANDROID_NEED_BUFFER_FLIP(f) \ ((f)->output_data.android->need_buffer_flip) +/* Return the drawable used for rendering to frame F and mark the + frame as needing a buffer flip later. There's no easy way to run + code after any drawing command, but code can be run whenever + someone asks for the handle necessary to draw. */ +#define FRAME_ANDROID_DRAWABLE(f) \ + (((f))->output_data.android->need_buffer_flip = true, \ + FRAME_ANDROID_WINDOW ((f))) + /* Return whether or not the frame F has been completely drawn. Used while handling async input. */ #define FRAME_ANDROID_COMPLETE_P(f) \ diff --git a/src/dired.c b/src/dired.c index 084f3225cb2..57d79b84463 100644 --- a/src/dired.c +++ b/src/dired.c @@ -44,6 +44,21 @@ along with GNU Emacs. If not, see . */ #include "msdos.h" /* for fstatat */ #endif +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) +typedef DIR emacs_dir; +#define emacs_readdir readdir +#define emacs_closedir closedir +#else + +#include "android.h" + +/* The Android emulation of dirent stuff is required to be able to + list the /assets special directory. */ +typedef struct android_dir emacs_dir; +#define emacs_readdir android_readdir +#define emacs_closedir android_closedir +#endif + #ifdef WINDOWSNT extern int is_slow_fs (const char *); #endif @@ -78,19 +93,30 @@ dirent_type (struct dirent *dp) #endif } -static DIR * +static emacs_dir * open_directory (Lisp_Object dirname, Lisp_Object encoded_dirname, int *fdp) { char *name = SSDATA (encoded_dirname); - DIR *d; + emacs_dir *d; int fd, opendir_errno; -#ifdef DOS_NT - /* Directories cannot be opened. The emulation assumes that any - file descriptor other than AT_FDCWD corresponds to the most - recently opened directory. This hack is good enough for Emacs. */ +#if defined DOS_NT || (defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + /* On DOS_NT, directories cannot be opened. The emulation assumes + that any file descriptor other than AT_FDCWD corresponds to the + most recently opened directory. This hack is good enough for + Emacs. + + This code is also used on Android for a different reason: a + special `assets' directory outside the normal file system is used + to open assets inside the Android application package, and must + be listed using the opendir-like interface provided in + android.h. */ fd = 0; +#ifndef HAVE_ANDROID d = opendir (name); +#else + d = android_opendir (name); +#endif opendir_errno = errno; #else fd = emacs_open (name, O_RDONLY | O_DIRECTORY, 0); @@ -125,7 +151,7 @@ directory_files_internal_w32_unwind (Lisp_Object arg) static void directory_files_internal_unwind (void *d) { - closedir (d); + emacs_closedir (d); } /* Return the next directory entry from DIR; DIR's name is DIRNAME. @@ -133,12 +159,12 @@ directory_files_internal_unwind (void *d) Signal any unrecoverable errors. */ static struct dirent * -read_dirent (DIR *dir, Lisp_Object dirname) +read_dirent (emacs_dir *dir, Lisp_Object dirname) { while (true) { errno = 0; - struct dirent *dp = readdir (dir); + struct dirent *dp = emacs_readdir (dir); if (dp || errno == 0) return dp; if (! (errno == EAGAIN || errno == EINTR)) @@ -190,7 +216,7 @@ directory_files_internal (Lisp_Object directory, Lisp_Object full, Lisp_Object encoded_dirfilename = ENCODE_FILE (dirfilename); int fd; - DIR *d = open_directory (dirfilename, encoded_dirfilename, &fd); + emacs_dir *d = open_directory (dirfilename, encoded_dirfilename, &fd); /* Unfortunately, we can now invoke expand-file-name and file-attributes on filenames, both of which can throw, so we must @@ -300,7 +326,7 @@ directory_files_internal (Lisp_Object directory, Lisp_Object full, list = Fcons (attrs ? Fcons (finalname, fileattrs) : finalname, list); } - closedir (d); + emacs_closedir (d); #ifdef WINDOWSNT if (attrs) Vw32_get_true_file_attributes = w32_save; @@ -514,7 +540,7 @@ file_name_completion (Lisp_Object file, Lisp_Object dirname, bool all_flag, } } int fd; - DIR *d = open_directory (dirname, encoded_dir, &fd); + emacs_dir *d = open_directory (dirname, encoded_dir, &fd); record_unwind_protect_ptr (directory_files_internal_unwind, d); /* Loop reading directory entries. */ diff --git a/src/dispnew.c b/src/dispnew.c index 12814bc0b75..f31e185a84b 100644 --- a/src/dispnew.c +++ b/src/dispnew.c @@ -3175,6 +3175,7 @@ redraw_frame (struct frame *f) its redisplay done. */ mark_window_display_accurate (FRAME_ROOT_WINDOW (f), 0); set_window_update_flags (XWINDOW (FRAME_ROOT_WINDOW (f)), true); + f->garbaged = false; } @@ -6053,7 +6054,7 @@ FILE = nil means just close any termscript file currently open. */) if (tty->termscript != 0) { block_input (); - fclose (tty->termscript); + emacs_fclose (tty->termscript); tty->termscript = 0; unblock_input (); } diff --git a/src/fileio.c b/src/fileio.c index 9d81f0ca545..6fa524b3bb4 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -277,7 +277,7 @@ void fclose_unwind (void *arg) { FILE *stream = arg; - fclose (stream); + emacs_fclose (stream); } /* Restore point, having saved it as a marker. */ @@ -2989,6 +2989,12 @@ If there is no error, returns nil. */) encoded_filename = ENCODE_FILE (absname); +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + /* FILE may be some kind of special Android file. */ + if (android_file_access_p (SSDATA (encoded_filename), R_OK)) + return Qnil; +#endif + if (faccessat (AT_FDCWD, SSDATA (encoded_filename), R_OK, AT_EACCESS) != 0) report_file_error (SSDATA (string), filename); @@ -3205,7 +3211,11 @@ file_accessible_directory_p (Lisp_Object file) There are three exceptions: "", "/", and "//". Leave "" alone, as it's invalid. Append only "." to the other two exceptions as "/" and "//" are distinct on some platforms, whereas "/", "///", - "////", etc. are all equivalent. */ + "////", etc. are all equivalent. + + Android has a special directory named "/assets". There is no "." + directory there, but appending a "/" is sufficient to check + whether or not it is a directory. */ if (! len) dir = data; else @@ -3215,11 +3225,27 @@ file_accessible_directory_p (Lisp_Object file) special cases "/" and "//", and it's a safe optimization here. After appending '.', append another '/' to work around a macOS bug (Bug#30350). */ - static char const appended[] = "/./"; - char *buf = SAFE_ALLOCA (len + sizeof appended); - memcpy (buf, data, len); - strcpy (buf + len, &appended[data[len - 1] == '/']); - dir = buf; +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + if (!strncmp ("/assets/", data, + sizeof "/assets" - 1)) + { + static char const appended[] = "/"; + char *buf = SAFE_ALLOCA (len + sizeof appended); + memcpy (buf, data, len); + strcpy (buf + len, &appended[data[len - 1] == '/']); + dir = buf; + } + else + { +#endif + static char const appended[] = "/./"; + char *buf = SAFE_ALLOCA (len + sizeof appended); + memcpy (buf, data, len); + strcpy (buf + len, &appended[data[len - 1] == '/']); + dir = buf; +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + } +#endif } ok = file_access_p (dir, F_OK); @@ -5973,7 +5999,7 @@ do_auto_save_unwind (void *arg) if (stream != NULL) { block_input (); - fclose (stream); + emacs_fclose (stream); unblock_input (); } } diff --git a/src/keyboard.c b/src/keyboard.c index daca964b981..78637ef4f15 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -4956,8 +4956,8 @@ const char *const lispy_function_keys[] = [66] = "return", [67] = "backspace", [82] = "menu", - [92] = "page-up", - [93] = "page-down", + [92] = "prior", + [93] = "next", }; #elif defined HAVE_NTGUI @@ -11219,7 +11219,7 @@ This may include sensitive information such as passwords. */) if (dribble) { block_input (); - fclose (dribble); + emacs_fclose (dribble); unblock_input (); dribble = 0; } diff --git a/src/lisp.h b/src/lisp.h index bf8ff5cdc18..f64a27ce113 100644 --- a/src/lisp.h +++ b/src/lisp.h @@ -28,6 +28,7 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include #ifdef HAVE_SYS_STAT_H #include @@ -5077,6 +5078,7 @@ extern int emacs_open (const char *, int, int); extern int emacs_open_noquit (const char *, int, int); extern int emacs_pipe (int[2]); extern int emacs_close (int); +extern int emacs_fclose (FILE *); extern ptrdiff_t emacs_read (int, void *, ptrdiff_t); extern ptrdiff_t emacs_read_quit (int, void *, ptrdiff_t); extern ptrdiff_t emacs_write (int, void const *, ptrdiff_t); diff --git a/src/lread.c b/src/lread.c index 2ac3c85ea81..d585dece392 100644 --- a/src/lread.c +++ b/src/lread.c @@ -1141,7 +1141,7 @@ close_infile_unwind (void *arg) { struct infile *prev_infile = arg; eassert (infile && infile != prev_infile); - fclose (infile->stream); + emacs_fclose (infile->stream); infile = prev_infile; } diff --git a/src/sfnt.c b/src/sfnt.c index 20cf2376d85..9b6c421212a 100644 --- a/src/sfnt.c +++ b/src/sfnt.c @@ -3072,8 +3072,8 @@ sfnt_curve_is_flat (struct sfnt_point control0, /* 2.0 is a constant describing the area covered at which point the curve is considered "flat". */ - return (abs (sfnt_mul_fixed (g.x, h.x) - - sfnt_mul_fixed (g.y, h.y)) + return (abs (sfnt_mul_fixed (g.x, h.y) + - sfnt_mul_fixed (g.y, h.x)) <= 0400000); } @@ -3261,9 +3261,11 @@ sfnt_prepare_raster (struct sfnt_raster *raster, struct sfnt_glyph_outline *outline) { raster->width - = sfnt_ceil_fixed (outline->xmax - outline->xmin) >> 16; + = (sfnt_ceil_fixed (outline->xmax) + - sfnt_floor_fixed (outline->xmin)) >> 16; raster->height - = sfnt_ceil_fixed (outline->ymax - outline->ymin) >> 16; + = (sfnt_ceil_fixed (outline->ymax) + - sfnt_floor_fixed (outline->ymin)) >> 16; raster->refcount = 0; /* Align the raster to a SFNT_POLY_ALIGNMENT byte boundary. */ @@ -3292,10 +3294,10 @@ sfnt_step_edge (struct sfnt_edge *edge) } /* Build a list of edges for each contour in OUTLINE, applying - OUTLINE->xmin and OUTLINE->ymin as the offset to each edge. Call - EDGE_PROC with DCONTEXT and the resulting edges as arguments. It - is OK to modify the edges given to EDGE_PROC. Align all edges to - the sub-pixel grid. */ + OUTLINE->xmin and floor (OUTLINE->ymin) as the offset to each edge. + Call EDGE_PROC with DCONTEXT and the resulting edges as arguments. + It is OK to modify the edges given to EDGE_PROC. Align all edges + to the sub-pixel grid. */ static void sfnt_build_outline_edges (struct sfnt_glyph_outline *outline, @@ -3303,13 +3305,18 @@ sfnt_build_outline_edges (struct sfnt_glyph_outline *outline, { struct sfnt_edge *edges; size_t i, edge, next_vertex; - sfnt_fixed dx, dy, bot, step_x; + sfnt_fixed dx, dy, bot, step_x, ymin, xmin; int inc_x; size_t top, bottom, y; edges = alloca (outline->outline_used * sizeof *edges); edge = 0; + /* ymin and xmin must be the same as the offset used to set offy and + offx in rasters. */ + ymin = sfnt_floor_fixed (outline->ymin); + xmin = sfnt_floor_fixed (outline->xmin); + for (i = 0; i < outline->outline_used; ++i) { /* Set NEXT_VERTEX to the next point (vertex) in this contour. @@ -3356,12 +3363,12 @@ sfnt_build_outline_edges (struct sfnt_glyph_outline *outline, top = next_vertex; } - bot = (outline->outline[bottom].y - outline->ymin); - edges[edge].top = (outline->outline[top].y - outline->ymin); + bot = (outline->outline[bottom].y - ymin); + edges[edge].top = (outline->outline[top].y - ymin); /* Record the edge. Rasterization happens from bottom to up, so record the X at the bottom. */ - edges[edge].x = (outline->outline[bottom].x - outline->xmin); + edges[edge].x = (outline->outline[bottom].x - xmin); dx = (outline->outline[top].x - outline->outline[bottom].x); dy = abs (outline->outline[top].y - outline->outline[bottom].y); @@ -4585,7 +4592,7 @@ main (int argc, char **argv) /* Time this important bit. */ clock_gettime (CLOCK_THREAD_CPUTIME_ID, &start); outline = sfnt_build_glyph_outline (glyph, head, - 45, + 12, sfnt_test_get_glyph, sfnt_test_free_glyph, &dcontext); @@ -4652,7 +4659,7 @@ main (int argc, char **argv) if (hmtx && head) { - if (!sfnt_lookup_glyph_metrics (code, 36, + if (!sfnt_lookup_glyph_metrics (code, 12, &metrics, hmtx, hhea, head, maxp)) diff --git a/src/sfntfont-android.c b/src/sfntfont-android.c index 47aa27dc113..01bfdbaaf58 100644 --- a/src/sfntfont-android.c +++ b/src/sfntfont-android.c @@ -42,19 +42,6 @@ static Lisp_Object font_cache; -static unsigned int -sfntfont_android_saturate32 (unsigned int a, unsigned int b) -{ - unsigned int c; - - c = a + b; - - if (c < a) - c = -1; - - return c; -} - /* Scale each of the four packed bytes in P in the low 16 bits of P by SCALE. Return the result. @@ -107,8 +94,9 @@ sfntfont_android_blend (unsigned int src, unsigned int dst) src = src & ~0x00ff00ff; src |= (src_rb >> 16 | src_rb << 16); - /* Saturating is unnecessary but helps find bugs. */ - return sfntfont_android_saturate32 (both, src); + /* This addition need not be saturating because both has already + been multiplied by 255 - a. */ + return both + src; } #define U255TO256(x) ((unsigned short) (x) + ((x) >> 7)) @@ -128,8 +116,9 @@ sfntfont_android_blendrgb (unsigned int src, unsigned int dst) both = ag_part | rb_part; - /* Saturating is unnecessary but helps find bugs. */ - return sfntfont_android_saturate32 (both, src); + /* This addition need not be saturating because both has already + been multiplied by 255 - a. */ + return both + src; } /* Composite the bitmap described by BUFFER, STRIDE and TEXT_RECTANGLE @@ -162,6 +151,10 @@ sfntfont_android_composite_bitmap (unsigned char *restrict buffer, src_y = i + (rect->y - text_rectangle->y); + if (src_y > text_rectangle->height) + /* Huh? */ + return; + src_row = (unsigned int *) ((buffer + src_y * stride)); dst_row = (unsigned int *) (dest + ((i + rect->y) * bitmap_info->stride)); @@ -343,7 +336,7 @@ sfntfont_android_put_glyphs (struct glyph_string *s, int from, } /* Lock the bitmap. It must be unlocked later. */ - bitmap_data = android_lock_bitmap (FRAME_ANDROID_WINDOW (s->f), + bitmap_data = android_lock_bitmap (FRAME_ANDROID_DRAWABLE (s->f), &bitmap_info, &bitmap); /* If locking the bitmap fails, just discard the data that was @@ -385,7 +378,7 @@ sfntfont_android_put_glyphs (struct glyph_string *s, int from, ANDROID_DELETE_LOCAL_REF (bitmap); /* Damage the window by the text rectangle. */ - android_damage_window (FRAME_ANDROID_WINDOW (s->f), + android_damage_window (FRAME_ANDROID_DRAWABLE (s->f), &text_rectangle); /* Release the temporary scanline buffer. */ @@ -495,13 +488,19 @@ init_sfntfont_android (void) version of Android the device is running. */ if (android_get_device_api_level () >= 15) Vsfnt_default_family_alist - = list2 (Fcons (build_string ("Monospace"), + = list3 (Fcons (build_string ("Monospace"), + build_string ("Droid Sans Mono")), + /* Android doesn't come with a Monospace Serif font, so + this will have to do. */ + Fcons (build_string ("Monospace Serif"), build_string ("Droid Sans Mono")), Fcons (build_string ("Sans Serif"), build_string ("Roboto"))); else Vsfnt_default_family_alist - = list2 (Fcons (build_string ("Monospace"), + = list3 (Fcons (build_string ("Monospace"), + build_string ("Droid Sans Mono")), + Fcons (build_string ("Monospace Serif"), build_string ("Droid Sans Mono")), Fcons (build_string ("Sans Serif"), build_string ("Droid Sans"))); diff --git a/src/sfntfont.c b/src/sfntfont.c index 9206fbc6629..25cea59f6a7 100644 --- a/src/sfntfont.c +++ b/src/sfntfont.c @@ -1982,6 +1982,9 @@ sfntfont_text_extents (struct font *font, const unsigned int *code, total_width = 0; + /* First clear the metrics array. */ + memset (metrics, 0, sizeof *metrics); + /* Get the metrcs one by one, then sum them up. */ for (i = 0; i < nglyphs; ++i) { @@ -2059,7 +2062,7 @@ sfntfont_draw (struct glyph_string *s, int from, int to, struct sfnt_glyph_metrics metrics; length = to - from; - font = s->face->font; + font = s->font; info = (struct sfnt_font_info *) font; rasters = alloca (length * sizeof *rasters); diff --git a/src/sysdep.c b/src/sysdep.c index dd97ae1cb38..4d89d4f25ae 100644 --- a/src/sysdep.c +++ b/src/sysdep.c @@ -2335,7 +2335,8 @@ emacs_backtrace (int backtrace_limit) } } -#ifndef HAVE_NTGUI +#if !defined HAVE_NTGUI && !(defined HAVE_ANDROID \ + && !defined ANDROID_STUBIFY) void emacs_abort (void) { @@ -2568,6 +2569,20 @@ emacs_close (int fd) } } +/* Wrapper around fclose. On Android, this calls `android_fclose' to + clear information associated with the FILE's file descriptor if + necessary. */ + +int +emacs_fclose (FILE *stream) +{ +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + return fclose (stream); +#else + return android_fclose (stream); +#endif +} + /* Maximum number of bytes to read or write in a single system call. This works around a serious bug in Linux kernels before 2.6.16; see . diff --git a/src/term.c b/src/term.c index 1a21b6f5764..05163cce533 100644 --- a/src/term.c +++ b/src/term.c @@ -2354,8 +2354,8 @@ A suspended tty may be resumed by calling `resume-tty' on it. */) #ifndef MSDOS if (f != t->display_info.tty->output) - fclose (t->display_info.tty->output); - fclose (f); + emacs_fclose (t->display_info.tty->output); + emacs_fclose (f); #endif t->display_info.tty->input = 0; @@ -4632,12 +4632,12 @@ delete_tty (struct terminal *terminal) { delete_keyboard_wait_descriptor (fileno (tty->input)); if (tty->input != stdin) - fclose (tty->input); + emacs_fclose (tty->input); } if (tty->output && tty->output != stdout && tty->output != tty->input) - fclose (tty->output); + emacs_fclose (tty->output); if (tty->termscript) - fclose (tty->termscript); + emacs_fclose (tty->termscript); xfree (tty->old_tty); xfree (tty->Wcm); diff --git a/src/verbose.mk.in b/src/verbose.mk.in index 25dff990881..f826bf18a0a 100644 --- a/src/verbose.mk.in +++ b/src/verbose.mk.in @@ -83,5 +83,5 @@ AM_V_RC = @$(info $ RC $@) # These are used for the Android port. AM_V_JAVAC = @$(info $ JAVAC $@) -AM_V_DX = @$(info $ DX $@) +AM_V_D8 = @$(info $ D8 $@) endif -- cgit v1.3 From 6e2bc91d924fbeb0ad5728e0424eabc905c0d366 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 15 Jan 2023 11:57:10 +0800 Subject: Implement toolkit menus on Android * java/org/gnu/emacs/EmacsActivity.java (onContextMenuClosed): New function. * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu): New field `itemAlreadySelected'. (onMenuItemClick): New function. (inflateMenuItems): Attach onClickListener as appropriate. (display1): Clear itemAlreadySelected. (display): Fix runnable synchronization. * java/org/gnu/emacs/EmacsNative.java (sendContextMenu): New function. * java/org/gnu/emacs/EmacsView.java (popupMenu): (cancelPopupMenu): Set popupactive correctly. * src/android.c (android_run_select_thread): Fix android_select again. (android_wait_event): New function. * src/android.h: Update prototypes. * src/androidgui.h (enum android_event_type): New `ANDROID_CONTEXT_MENU' event. (struct android_menu_event, union android_event): Add new event. * src/androidmenu.c (struct android_emacs_context_menu): New structure. (android_init_emacs_context_menu): Add `dismiss' method. (struct android_dismiss_menu_data): New structure. (android_dismiss_menu, android_process_events_for_menu): New functions. (android_menu_show): Set an actual item ID. (popup_activated): Define when stubify as well. (Fmenu_or_popup_active_p): New function. (syms_of_androidmenu): New function. * src/androidterm.c (handle_one_android_event): Handle context menu events. * src/androidterm.h (struct android_display_info): New field for menu item ID. * src/emacs.c (android_emacs_init): Call syms_of_androidmenu. * src/xdisp.c (note_mouse_highlight): Return if popup_activated on Android as well. --- java/org/gnu/emacs/EmacsActivity.java | 15 ++++ java/org/gnu/emacs/EmacsContextMenu.java | 64 +++++++++++++++-- java/org/gnu/emacs/EmacsNative.java | 3 + java/org/gnu/emacs/EmacsView.java | 2 + src/android.c | 42 +++++++++++- src/android.h | 1 + src/androidgui.h | 16 +++++ src/androidmenu.c | 114 +++++++++++++++++++++++++++++-- src/androidterm.c | 8 +++ src/androidterm.h | 5 ++ src/emacs.c | 1 + src/xdisp.c | 3 +- 12 files changed, 258 insertions(+), 16 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 4cd286d1e89..4b96a376987 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -30,6 +30,7 @@ import android.os.Bundle; import android.util.Log; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; +import android.view.Menu; public class EmacsActivity extends Activity implements EmacsWindowAttachmentManager.WindowConsumer @@ -227,4 +228,18 @@ public class EmacsActivity extends Activity EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this); super.onResume (); } + + @Override + public void + onContextMenuClosed (Menu menu) + { + Log.d (TAG, "onContextMenuClosed: " + menu); + + /* Send a context menu event given that no menu item has already + been selected. */ + if (!EmacsContextMenu.itemAlreadySelected) + EmacsNative.sendContextMenu ((short) 0, 0); + + super.onContextMenuClosed (menu); + } }; diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 8d7ae08b257..02dd1c7efa9 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -31,6 +31,8 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.util.Log; + import android.widget.PopupMenu; /* Context menu implementation. This object is built from JNI and @@ -40,12 +42,31 @@ import android.widget.PopupMenu; public class EmacsContextMenu { - private class Item + private static final String TAG = "EmacsContextMenu"; + + /* Whether or not an item was selected. */ + public static boolean itemAlreadySelected; + + private class Item implements MenuItem.OnMenuItemClickListener { public int itemID; public String itemName; public EmacsContextMenu subMenu; public boolean isEnabled; + + @Override + public boolean + onMenuItemClick (MenuItem item) + { + Log.d (TAG, "onMenuItemClick: " + itemName + " (" + itemID + ")"); + + /* Send a context menu event. */ + EmacsNative.sendContextMenu ((short) 0, itemID); + + /* Say that an item has already been selected. */ + itemAlreadySelected = true; + return true; + } }; public List menuItems; @@ -137,6 +158,7 @@ public class EmacsContextMenu else { menuItem = menu.add (item.itemName); + menuItem.setOnMenuItemClickListener (item); /* If the item ID is zero, then disable the item. */ if (item.itemID == 0 || !item.isEnabled) @@ -171,6 +193,10 @@ public class EmacsContextMenu private boolean display1 (EmacsWindow window, int xPosition, int yPosition) { + /* Set this flag to false. It is used to decide whether or not to + send 0 in response to the context menu being closed. */ + itemAlreadySelected = false; + return window.view.popupMenu (this, xPosition, yPosition); } @@ -199,15 +225,39 @@ public class EmacsContextMenu } }; - try + synchronized (runnable) { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); + EmacsService.SERVICE.runOnUiThread (runnable); + + try + { + runnable.wait (); + } + catch (InterruptedException e) + { + EmacsNative.emacsAbort (); + } } return rc.thing; } + + /* Dismiss this context menu. WINDOW is the window where the + context menu is being displayed. */ + + public void + dismiss (final EmacsWindow window) + { + Runnable runnable; + + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + window.view.cancelPopupMenu (); + itemAlreadySelected = false; + } + }); + } }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index a11e509cd7f..4a80f88edcf 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -124,6 +124,9 @@ public class EmacsNative /* Send an ANDROID_DEICONIFIED event. */ public static native void sendDeiconified (short window); + /* Send an ANDROID_CONTEXT_MENU event. */ + public static native void sendContextMenu (short window, int menuEventID); + static { System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 1391f630be0..445d8ffa023 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -453,6 +453,7 @@ public class EmacsView extends ViewGroup return false; contextMenu = menu; + popupActive = true; /* On API 21 or later, use showContextMenu (float, float). */ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) @@ -469,5 +470,6 @@ public class EmacsView extends ViewGroup + " popupActive set"); contextMenu = null; + popupActive = false; } }; diff --git a/src/android.c b/src/android.c index 5e5e28c60ca..ed162a903ba 100644 --- a/src/android.c +++ b/src/android.c @@ -236,7 +236,7 @@ static sem_t android_pselect_sem, android_pselect_start_sem; static void * android_run_select_thread (void *data) { - sigset_t signals; + sigset_t signals, sigset; int rc; sigfillset (&signals); @@ -259,7 +259,11 @@ android_run_select_thread (void *data) /* Make sure SIGUSR1 can always wake pselect up. */ if (android_pselect_sigset) - sigdelset (android_pselect_sigset, SIGUSR1); + { + sigset = *android_pselect_sigset; + sigdelset (&sigset, SIGUSR1); + android_pselect_sigset = &sigset; + } else android_pselect_sigset = &signals; @@ -356,6 +360,23 @@ android_pending (void) return i; } +/* Wait for events to become available synchronously. Return once an + event arrives. */ + +void +android_wait_event (void) +{ + pthread_mutex_lock (&event_queue.mutex); + + /* Wait for events to appear if there are none available to + read. */ + if (!event_queue.num_events) + pthread_cond_wait (&event_queue.read_var, + &event_queue.mutex); + + pthread_mutex_unlock (&event_queue.mutex); +} + void android_next_event (union android_event *event_return) { @@ -1472,6 +1493,8 @@ NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, event.iconified.type = ANDROID_ICONIFIED; event.iconified.window = window; + + android_write_event (&event); } extern JNIEXPORT void JNICALL @@ -1482,6 +1505,21 @@ NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, event.iconified.type = ANDROID_DEICONIFIED; event.iconified.window = window; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, + jshort window, jint menu_event_id) +{ + union android_event event; + + event.menu.type = ANDROID_CONTEXT_MENU; + event.menu.window = window; + event.menu.menu_event_id = menu_event_id; + + android_write_event (&event); } #pragma clang diagnostic pop diff --git a/src/android.h b/src/android.h index 98f2494e9a3..e68e0a51fbf 100644 --- a/src/android.h +++ b/src/android.h @@ -89,6 +89,7 @@ extern jstring android_build_string (Lisp_Object); extern void android_exception_check (void); extern void android_get_keysym_name (int, char *, size_t); +extern void android_wait_event (void); diff --git a/src/androidgui.h b/src/androidgui.h index 8450a1f637b..0e075fda95e 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -233,6 +233,7 @@ enum android_event_type ANDROID_WHEEL, ANDROID_ICONIFIED, ANDROID_DEICONIFIED, + ANDROID_CONTEXT_MENU, }; struct android_any_event @@ -371,6 +372,18 @@ struct android_iconify_event android_window window; }; +struct android_menu_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* Window associated with the event. Always None. */ + android_window window; + + /* Menu event ID. */ + int menu_event_id; +}; + union android_event { enum android_event_type type; @@ -395,6 +408,9 @@ union android_event /* This has no parallel in X because Android doesn't have window properties. */ struct android_iconify_event iconified; + + /* This is only used to transmit selected menu items. */ + struct android_menu_event menu; }; enum diff --git a/src/androidmenu.c b/src/androidmenu.c index 0f0c6f4ef1f..7522f9c5a52 100644 --- a/src/androidmenu.c +++ b/src/androidmenu.c @@ -54,6 +54,7 @@ struct android_emacs_context_menu jmethodID add_pane; jmethodID parent; jmethodID display; + jmethodID dismiss; }; /* Identifiers associated with the EmacsContextMenu class. */ @@ -101,6 +102,7 @@ android_init_emacs_context_menu (void) FIND_METHOD (add_pane, "addPane", "(Ljava/lang/String;)V"); FIND_METHOD (parent, "parent", "()Lorg/gnu/emacs/EmacsContextMenu;"); FIND_METHOD (display, "display", "(Lorg/gnu/emacs/EmacsWindow;II)Z"); + FIND_METHOD (dismiss, "dismiss", "(Lorg/gnu/emacs/EmacsWindow;)V"); #undef FIND_METHOD #undef FIND_METHOD_STATIC @@ -130,6 +132,62 @@ android_push_local_frame (void) record_unwind_protect_void (android_unwind_local_frame); } +/* Data for android_dismiss_menu. */ + +struct android_dismiss_menu_data +{ + /* The menu object. */ + jobject menu; + + /* The window object. */ + jobject window; +}; + +/* Cancel the context menu passed in POINTER. Also, clear + popup_activated_flag. */ + +static void +android_dismiss_menu (void *pointer) +{ + struct android_dismiss_menu_data *data; + + data = pointer; + (*android_java_env)->CallVoidMethod (android_java_env, + data->menu, + menu_class.dismiss, + data->window); + popup_activated_flag = 0; +} + +/* Recursively process events until a ANDROID_CONTEXT_MENU event + arrives. Then, return the item ID specified in the event in + *ID. */ + +static void +android_process_events_for_menu (int *id) +{ + /* Set menu_event_id to -1; handle_one_android_event will set it to + the event ID upon receiving a context menu event. This can cause + a non-local exit. */ + x_display_list->menu_event_id = -1; + + /* Now wait for the menu event ID to change. */ + while (x_display_list->menu_event_id == -1) + { + /* Wait for events to become available. */ + android_wait_event (); + + /* Process pending signals. */ + process_pending_signals (); + + /* Maybe quit. */ + maybe_quit (); + } + + /* Return the ID. */ + *id = x_display_list->menu_event_id; +} + Lisp_Object android_menu_show (struct frame *f, int x, int y, int menuflags, Lisp_Object title, const char **error_name) @@ -140,11 +198,13 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, Lisp_Object pane_name, prefix; const char *pane_string; specpdl_ref count, count1; - Lisp_Object item_name, enable, def; + Lisp_Object item_name, enable, def, tem; jmethodID method; jobject store; bool rc; jobject window; + int id, item_id; + struct android_dismiss_menu_data data; count = SPECPDL_INDEX (); @@ -266,6 +326,12 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, ; else { + /* Compute the item ID. This is the index of value. + Make sure it doesn't overflow. */ + + if (!INT_ADD_OK (0, i + MENU_ITEMS_ITEM_VALUE, &item_id)) + memory_full (i + MENU_ITEMS_ITEM_VALUE * sizeof (Lisp_Object)); + /* Add this menu item with the appropriate state. */ title_string = (!NILP (item_name) @@ -274,7 +340,7 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, (*android_java_env)->CallVoidMethod (android_java_env, current_context_menu, menu_class.add_item, - (jint) 1, + (jint) item_id, title_string, (jboolean) !NILP (enable)); android_exception_check (); @@ -295,6 +361,7 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, ANDROID_HANDLE_WINDOW); rc = (*android_java_env)->CallBooleanMethod (android_java_env, context_menu, + menu_class.display, window, (jint) x, (jint) y); android_exception_check (); @@ -303,25 +370,54 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, /* This means displaying the menu failed. */ goto finish; -#if 0 - record_unwind_protect_ptr (android_dismiss_menu, &context_menu); + /* Make sure the context menu is always dismissed. */ + data.menu = context_menu; + data.window = window; + record_unwind_protect_ptr (android_dismiss_menu, &data); - /* Otherwise, loop waiting for the menu event to arrive. */ + /* Next, process events waiting for something to be selected. */ + popup_activated_flag = 1; + unblock_input (); android_process_events_for_menu (&id); + block_input (); if (!id) /* This means no menu item was selected. */ goto finish; -#endif + /* id is an index into menu_items. Check that it remains + valid. */ + if (id >= ASIZE (menu_items)) + goto finish; + + /* Now return the menu item at that location. */ + tem = AREF (menu_items, id); + unblock_input (); + return unbind_to (count, tem); finish: unblock_input (); return unbind_to (count, Qnil); } +#else + +int +popup_activated (void) +{ + return 0; +} + #endif +DEFUN ("menu-or-popup-active-p", Fmenu_or_popup_active_p, + Smenu_or_popup_active_p, 0, 0, 0, + doc: /* SKIP: real doc in xfns.c. */) + (void) +{ + return (popup_activated ()) ? Qt : Qnil; +} + void init_androidmenu (void) { @@ -329,3 +425,9 @@ init_androidmenu (void) android_init_emacs_context_menu (); #endif } + +void +syms_of_androidmenu (void) +{ + defsubr (&Smenu_or_popup_active_p); +} diff --git a/src/androidterm.c b/src/androidterm.c index 002d39af707..4017fec60a5 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -1125,6 +1125,14 @@ handle_one_android_event (struct android_display_info *dpyinfo, XSETFRAME (inev.ie.frame_or_window, any); goto OTHER; + /* Context menu handling. */ + case ANDROID_CONTEXT_MENU: + + if (dpyinfo->menu_event_id == -1) + dpyinfo->menu_event_id = event->menu.menu_event_id; + + goto OTHER; + default: goto OTHER; } diff --git a/src/androidterm.h b/src/androidterm.h index e83e32a5854..9aa09877196 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -132,6 +132,10 @@ struct android_display_info /* The time of the last mouse movement. */ Time last_mouse_movement_time; + + /* ID of the last menu event received. -1 means Emacs is waiting + for a context menu event. */ + int menu_event_id; }; /* Structure representing a single tool (finger or stylus) pressed @@ -407,6 +411,7 @@ extern void android_finalize_font_entity (struct font_entity *); extern Lisp_Object android_menu_show (struct frame *, int, int, int, Lisp_Object, const char **); extern void init_androidmenu (void); +extern void syms_of_androidmenu (void); /* Defined in sfntfont-android.c. */ diff --git a/src/emacs.c b/src/emacs.c index 8f5be53aad9..f4973c70610 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -2397,6 +2397,7 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem #ifdef HAVE_ANDROID syms_of_androidterm (); syms_of_androidfns (); + syms_of_androidmenu (); syms_of_fontset (); #if !defined ANDROID_STUBIFY syms_of_androidfont (); diff --git a/src/xdisp.c b/src/xdisp.c index b9ee102af26..262a823f899 100644 --- a/src/xdisp.c +++ b/src/xdisp.c @@ -34959,7 +34959,8 @@ note_mouse_highlight (struct frame *f, int x, int y) struct buffer *b; /* When a menu is active, don't highlight because this looks odd. */ -#if defined (HAVE_X_WINDOWS) || defined (HAVE_NS) || defined (MSDOS) +#if defined (HAVE_X_WINDOWS) || defined (HAVE_NS) || defined (MSDOS) \ + || defined (HAVE_ANDROID) if (popup_activated ()) return; #endif -- cgit v1.3 From da9ae10636b84b88e9eb9c827b03cdaabd1611d1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 15 Jan 2023 15:45:29 +0800 Subject: Implement submenus on Android * java/org/gnu/emacs/EmacsActivity.java (onCreate): Set the default theme to Theme.DeviceDefault.NoActionBar if possible. (onContextMenuClosed): Add hack for Android bug. * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu) (onMenuItemClick): Set flag upon submenu selection. (inflateMenuItems): Set onClickListener for submenus as well. (display1): Clear new flag. * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): Fix rectangle bounds. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsService.java (onCreate): Pass cache directory. (sync): New function. * src/android.c (struct android_emacs_service): New method `sync'. (setEmacsParams, initEmacs): Handle cache directory. (android_init_emacs_service): Initialize new method `sync'. (android_sync): New function. * src/androidfns.c (Fx_show_tip): Call both functions. * src/androidgui.h: Update prototypes. * src/androidmenu.c (struct android_menu_subprefix) (android_free_subprefixes, android_menu_show): Handle submenu prefixes correctly. * src/androidterm.c (handle_one_android_event): Clear help echo on MotionNotify like on X. * src/menu.c (single_menu_item): Enable submenus on Android. --- java/org/gnu/emacs/EmacsActivity.java | 12 ++- java/org/gnu/emacs/EmacsContextMenu.java | 31 ++++++- java/org/gnu/emacs/EmacsDrawRectangle.java | 2 +- java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsService.java | 36 +++++++- src/android.c | 38 ++++++++- src/androidfns.c | 8 ++ src/androidgui.h | 1 + src/androidmenu.c | 128 ++++++++++++++++++++++++++--- src/androidterm.c | 10 +++ src/menu.c | 9 +- 11 files changed, 255 insertions(+), 24 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 4b96a376987..79c0991a5d3 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -27,6 +27,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.os.Build; import android.util.Log; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; @@ -162,7 +163,11 @@ public class EmacsActivity extends Activity FrameLayout.LayoutParams params; /* Set the theme to one without a title bar. */ - setTheme (android.R.style.Theme_NoTitleBar); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme (android.R.style.Theme_DeviceDefault_NoActionBar); + else + setTheme (android.R.style.Theme_NoTitleBar); params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); @@ -235,6 +240,11 @@ public class EmacsActivity extends Activity { Log.d (TAG, "onContextMenuClosed: " + menu); + /* See the comment inside onMenuItemClick. */ + if (EmacsContextMenu.wasSubmenuSelected + && menu.toString ().contains ("ContextMenuBuilder")) + return; + /* Send a context menu event given that no menu item has already been selected. */ if (!EmacsContextMenu.itemAlreadySelected) diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 02dd1c7efa9..00e204c9949 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -30,6 +30,7 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.SubMenu; import android.util.Log; @@ -47,6 +48,9 @@ public class EmacsContextMenu /* Whether or not an item was selected. */ public static boolean itemAlreadySelected; + /* Whether or not a submenu was selected. */ + public static boolean wasSubmenuSelected; + private class Item implements MenuItem.OnMenuItemClickListener { public int itemID; @@ -60,6 +64,20 @@ public class EmacsContextMenu { Log.d (TAG, "onMenuItemClick: " + itemName + " (" + itemID + ")"); + if (subMenu != null) + { + /* After opening a submenu within a submenu, Android will + send onContextMenuClosed for a ContextMenuBuilder. This + will normally confuse Emacs into thinking that the + context menu has been dismissed. Wrong! + + Setting this flag makes EmacsActivity to only handle + SubMenuBuilder being closed, which always means the menu + has actually been dismissed. */ + wasSubmenuSelected = true; + return false; + } + /* Send a context menu event. */ EmacsNative.sendContextMenu ((short) 0, itemID); @@ -144,7 +162,7 @@ public class EmacsContextMenu { Intent intent; MenuItem menuItem; - Menu submenu; + SubMenu submenu; for (Item item : menuItems) { @@ -153,7 +171,11 @@ public class EmacsContextMenu /* This is a submenu. Create the submenu and add the contents of the menu to it. */ submenu = menu.addSubMenu (item.itemName); - inflateMenuItems (submenu); + item.subMenu.inflateMenuItems (submenu); + + /* This is still needed to set wasSubmenuSelected. */ + menuItem = submenu.getItem (); + menuItem.setOnMenuItemClickListener (item); } else { @@ -184,7 +206,7 @@ public class EmacsContextMenu public EmacsContextMenu parent () { - return parent; + return this.parent; } /* Like display, but does the actual work and runs in the main @@ -197,6 +219,9 @@ public class EmacsContextMenu send 0 in response to the context menu being closed. */ itemAlreadySelected = false; + /* No submenu has been selected yet. */ + wasSubmenuSelected = false; + return window.view.popupMenu (this, xPosition, yPosition); } diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index 84ff498847b..b42e9556e8c 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -59,7 +59,7 @@ public class EmacsDrawRectangle } paint = gc.gcPaint; - rect = new Rect (x + 1, y + 1, x + width, y + height); + rect = new Rect (x, y, x + width, y + height); paint.setStyle (Paint.Style.STROKE); diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 4a80f88edcf..2f3a732ea7c 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -38,6 +38,9 @@ public class EmacsNative libDir must be the package's data storage location for native libraries. It is used as PATH. + cacheDir must be the package's cache directory. It is used as + the `temporary-file-directory'. + pixelDensityX and pixelDensityY are the DPI values that will be used by Emacs. @@ -45,6 +48,7 @@ public class EmacsNative public static native void setEmacsParams (AssetManager assetManager, String filesDir, String libDir, + String cacheDir, float pixelDensityX, float pixelDensityY, EmacsService emacsService); diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index f935b63fa0d..ca38f93dc98 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -108,7 +108,7 @@ public class EmacsService extends Service { AssetManager manager; Context app_context; - String filesDir, libDir; + String filesDir, libDir, cacheDir; double pixelDensityX; double pixelDensityY; @@ -126,12 +126,13 @@ public class EmacsService extends Service parameters. */ filesDir = app_context.getFilesDir ().getCanonicalPath (); libDir = getLibraryDirectory (); + cacheDir = app_context.getCacheDir ().getCanonicalPath (); Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir + " and libDir = " + libDir); EmacsNative.setEmacsParams (manager, filesDir, libDir, - (float) pixelDensityX, + cacheDir, (float) pixelDensityX, (float) pixelDensityY, this); @@ -407,4 +408,35 @@ public class EmacsService extends Service { return KeyEvent.keyCodeToString (keysym); } + + public void + sync () + { + Runnable runnable; + + runnable = new Runnable () { + public void + run () + { + synchronized (this) + { + notify (); + } + } + }; + + synchronized (runnable) + { + runOnUiThread (runnable); + + try + { + runnable.wait (); + } + catch (InterruptedException e) + { + EmacsNative.emacsAbort (); + } + } + } }; diff --git a/src/android.c b/src/android.c index ed162a903ba..3a965286460 100644 --- a/src/android.c +++ b/src/android.c @@ -88,6 +88,7 @@ struct android_emacs_service jmethodID get_screen_height; jmethodID detect_mouse; jmethodID name_keysym; + jmethodID sync; }; struct android_emacs_pixmap @@ -116,15 +117,18 @@ static AAssetManager *asset_manager; /* Whether or not Emacs has been initialized. */ static int emacs_initialized; -/* The path used to store site-lisp. */ +/* The directory used to store site-lisp. */ char *android_site_load_path; -/* The path used to store native libraries. */ +/* The directory used to store native libraries. */ char *android_lib_dir; -/* The path used to store game files. */ +/* The directory used to store game files. */ char *android_game_path; +/* The directory used to store temporary files. */ +char *android_cache_dir; + /* The display's pixel densities. */ double android_pixel_density_x, android_pixel_density_y; @@ -911,6 +915,7 @@ JNIEXPORT void JNICALL NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject local_asset_manager, jobject files_dir, jobject libs_dir, + jobject cache_dir, jfloat pixel_density_x, jfloat pixel_density_y, jobject emacs_service_object) @@ -986,6 +991,20 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, (*env)->ReleaseStringUTFChars (env, (jstring) libs_dir, java_string); + java_string = (*env)->GetStringUTFChars (env, (jstring) cache_dir, + NULL); + + if (!java_string) + emacs_abort (); + + android_cache_dir = strdup ((const char *) java_string); + + if (!android_files_dir) + emacs_abort (); + + (*env)->ReleaseStringUTFChars (env, (jstring) cache_dir, + java_string); + /* Calculate the site-lisp path. */ android_site_load_path = malloc (PATH_MAX + 1); @@ -1083,6 +1102,7 @@ android_init_emacs_service (void) FIND_METHOD (get_screen_height, "getScreenHeight", "(Z)I"); FIND_METHOD (detect_mouse, "detectMouse", "()Z"); FIND_METHOD (name_keysym, "nameKeysym", "(I)Ljava/lang/String;"); + FIND_METHOD (sync, "sync", "()V"); #undef FIND_METHOD } @@ -1216,6 +1236,9 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) /* Set HOME to the app data directory. */ setenv ("HOME", android_files_dir, 1); + /* Set TMPDIR to the temporary files directory. */ + setenv ("TMPDIR", android_cache_dir, 1); + /* Set the cwd to that directory as well. */ if (chdir (android_files_dir)) __android_log_print (ANDROID_LOG_WARN, __func__, @@ -3519,6 +3542,15 @@ android_get_keysym_name (int keysym, char *name_return, size_t size) ANDROID_DELETE_LOCAL_REF (string); } +void +android_sync (void) +{ + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.sync); + android_exception_check (); +} + #undef faccessat diff --git a/src/androidfns.c b/src/androidfns.c index ab136bc2722..bb37c415069 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -26,6 +26,7 @@ along with GNU Emacs. If not, see . */ #include "blockinput.h" #include "keyboard.h" #include "buffer.h" +#include "androidgui.h" #ifndef ANDROID_STUBIFY @@ -2282,6 +2283,13 @@ DEFUN ("x-show-tip", Fx_show_tip, Sx_show_tip, 1, 6, 0, android_map_raised (FRAME_ANDROID_WINDOW (tip_f)); unblock_input (); + /* Synchronize with the UI thread. This is required to prevent ugly + black splotches. */ + android_sync (); + + /* Garbage the tip frame too. */ + SET_FRAME_GARBAGED (tip_f); + w->must_be_updated_p = true; update_single_window (w); flush_frame (tip_f); diff --git a/src/androidgui.h b/src/androidgui.h index 0e075fda95e..9df5b073a7c 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -504,6 +504,7 @@ extern void android_move_resize_window (android_window, int, int, extern void android_map_raised (android_window); extern void android_translate_coordinates (android_window, int, int, int *, int *); +extern void android_sync (void); #endif diff --git a/src/androidmenu.c b/src/androidmenu.c index 7522f9c5a52..6fb4963174b 100644 --- a/src/androidmenu.c +++ b/src/androidmenu.c @@ -28,6 +28,8 @@ along with GNU Emacs. If not, see . */ #ifndef ANDROID_STUBIFY +#include + /* Flag indicating whether or not a popup menu has been posted and not yet popped down. */ @@ -188,6 +190,35 @@ android_process_events_for_menu (int *id) *id = x_display_list->menu_event_id; } +/* Structure describing a ``subprefix'' in the menu. */ + +struct android_menu_subprefix +{ + /* The subprefix above. */ + struct android_menu_subprefix *last; + + /* The subprefix itself. */ + Lisp_Object subprefix; +}; + +/* Free the subprefixes starting from *DATA. */ + +static void +android_free_subprefixes (void *data) +{ + struct android_menu_subprefix **head, *subprefix; + + head = data; + + while (*head) + { + subprefix = *head; + *head = subprefix->last; + + xfree (subprefix); + } +} + Lisp_Object android_menu_show (struct frame *f, int x, int y, int menuflags, Lisp_Object title, const char **error_name) @@ -198,13 +229,15 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, Lisp_Object pane_name, prefix; const char *pane_string; specpdl_ref count, count1; - Lisp_Object item_name, enable, def, tem; + Lisp_Object item_name, enable, def, tem, entry; jmethodID method; jobject store; bool rc; jobject window; - int id, item_id; + int id, item_id, submenu_depth; struct android_dismiss_menu_data data; + struct android_menu_subprefix *subprefix, *temp_subprefix; + struct android_menu_subprefix *subprefix_1; count = SPECPDL_INDEX (); @@ -232,7 +265,7 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, android_push_local_frame (); /* Iterate over the menu. */ - i = 0; + i = 0, submenu_depth = 0; while (i < menu_items_used) { @@ -241,6 +274,7 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, /* This is the start of a new submenu. However, it can be ignored here. */ i += 1; + submenu_depth += 1; } else if (EQ (AREF (menu_items, i), Qlambda)) { @@ -256,9 +290,18 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, if (store != context_menu) ANDROID_DELETE_LOCAL_REF (store); i += 1; + submenu_depth -= 1; - eassert (current_context_menu); + if (!current_context_menu || submenu_depth < 0) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "unbalanced submenu pop in menu_items"); + emacs_abort (); + } } + else if (EQ (AREF (menu_items, i), Qt) + && submenu_depth != 0) + i += MENU_ITEMS_PANE_LENGTH; else if (EQ (AREF (menu_items, i), Qquote)) i += 1; else if (EQ (AREF (menu_items, i), Qt)) @@ -300,8 +343,8 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, /* This is an actual menu item (or submenu). Add it to the menu. */ - if (i + MENU_ITEMS_ITEM_LENGTH < menu_items_used && - NILP (AREF (menu_items, i + MENU_ITEMS_ITEM_LENGTH))) + if (i + MENU_ITEMS_ITEM_LENGTH < menu_items_used + && NILP (AREF (menu_items, i + MENU_ITEMS_ITEM_LENGTH))) { /* This is a submenu. Add it. */ title_string = (!NILP (item_name) @@ -312,7 +355,7 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, = (*android_java_env)->CallObjectMethod (android_java_env, current_context_menu, menu_class.add_submenu, - title_string); + title_string, NULL); android_exception_check (); if (store != context_menu) @@ -385,13 +428,78 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, /* This means no menu item was selected. */ goto finish; - /* id is an index into menu_items. Check that it remains - valid. */ + /* This means the id is invalid. */ if (id >= ASIZE (menu_items)) goto finish; /* Now return the menu item at that location. */ - tem = AREF (menu_items, id); + tem = Qnil; + subprefix = NULL; + record_unwind_protect_ptr (android_free_subprefixes, &subprefix); + + /* Find the selected item, and its pane, to return + the proper value. */ + + prefix = entry = Qnil; + i = 0; + while (i < menu_items_used) + { + if (NILP (AREF (menu_items, i))) + { + temp_subprefix = xmalloc (sizeof *temp_subprefix); + temp_subprefix->last = subprefix; + subprefix = temp_subprefix; + subprefix->subprefix = prefix; + + prefix = entry; + i++; + } + else if (EQ (AREF (menu_items, i), Qlambda)) + { + prefix = subprefix->subprefix; + temp_subprefix = subprefix->last; + xfree (subprefix); + subprefix = temp_subprefix; + + i++; + } + else if (EQ (AREF (menu_items, i), Qt)) + { + prefix + = AREF (menu_items, i + MENU_ITEMS_PANE_PREFIX); + i += MENU_ITEMS_PANE_LENGTH; + } + /* Ignore a nil in the item list. + It's meaningful only for dialog boxes. */ + else if (EQ (AREF (menu_items, i), Qquote)) + i += 1; + else + { + entry = AREF (menu_items, i + MENU_ITEMS_ITEM_VALUE); + + if (i + MENU_ITEMS_ITEM_VALUE == id) + { + if (menuflags & MENU_KEYMAPS) + { + entry = list1 (entry); + + if (!NILP (prefix)) + entry = Fcons (prefix, entry); + + for (subprefix_1 = subprefix; subprefix_1; + subprefix_1 = subprefix_1->last) + if (!NILP (subprefix_1->subprefix)) + entry = Fcons (subprefix_1->subprefix, entry); + } + + tem = entry; + } + i += MENU_ITEMS_ITEM_LENGTH; + } + } + + Fprint (tem, Qexternal_debugging_output); + unblock_input (); return unbind_to (count, tem); diff --git a/src/androidterm.c b/src/androidterm.c index 4017fec60a5..6f452a52d85 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -689,6 +689,16 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto OTHER; case ANDROID_MOTION_NOTIFY: + + previous_help_echo_string = help_echo_string; + help_echo_string = Qnil; + + if (hlinfo->mouse_face_hidden) + { + hlinfo->mouse_face_hidden = false; + clear_mouse_face (hlinfo); + } + f = any; if (f) diff --git a/src/menu.c b/src/menu.c index 73d4215b94b..e1f899858d3 100644 --- a/src/menu.c +++ b/src/menu.c @@ -167,7 +167,7 @@ ensure_menu_items (int items) } } -#ifdef HAVE_EXT_MENU_BAR +#if defined HAVE_EXT_MENU_BAR || defined HAVE_ANDROID /* Begin a submenu. */ @@ -191,7 +191,7 @@ push_submenu_end (void) menu_items_submenu_depth--; } -#endif /* HAVE_EXT_MENU_BAR */ +#endif /* HAVE_EXT_MENU_BAR || HAVE_ANDROID */ /* Indicate boundary between left and right. */ @@ -420,8 +420,9 @@ single_menu_item (Lisp_Object key, Lisp_Object item, Lisp_Object dummy, void *sk AREF (item_properties, ITEM_PROPERTY_SELECTED), AREF (item_properties, ITEM_PROPERTY_HELP)); -#if defined (USE_X_TOOLKIT) || defined (USE_GTK) || defined (HAVE_NS) \ - || defined (HAVE_NTGUI) || defined (HAVE_HAIKU) || defined (HAVE_PGTK) +#if defined (USE_X_TOOLKIT) || defined (USE_GTK) || defined (HAVE_NS) \ + || defined (HAVE_NTGUI) || defined (HAVE_HAIKU) || defined (HAVE_PGTK) \ + || defined (HAVE_ANDROID) /* Display a submenu using the toolkit. */ if (FRAME_WINDOW_P (XFRAME (Vmenu_updating_frame)) && ! (NILP (map) || NILP (enabled))) -- cgit v1.3 From d44b60c2f001d57b010f0e9b82f798fbad9a23d6 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 20 Jan 2023 19:06:32 +0800 Subject: Update Android port * .gitignore: Don't ignore verbose.mk.android. * doc/emacs/Makefile.in (EMACSSOURCES): Add android.texi and input.texi. * doc/emacs/android.texi (Android): Document support for the on-screen keyboard. (Android Startup): Document how to start Emacs with -Q on Android. (Android Environment): Document how Emacs works around the system ``task killer''. Document changes to frame deletion behavior. * doc/emacs/emacs.texi (Top): * doc/emacs/input.texi (Other Input Devices, On-Screen Keyboards): Document how to use Emacs with virtual keyboards. * doc/lispref/commands.texi (Touchscreen Events): Document changes to `touch-screen-track-drag'. * doc/lispref/frames.texi (Frames, On-Screen Keyboards): New node. * java/AndroidManifest.xml.in: Add settings activity and appropriate OSK adjustment mode. * java/org/gnu/emacs/EmacsActivity.java (onCreate): Allow creating Emacs with -Q. (onDestroy): Don't remove if killed by the system. * java/org/gnu/emacs/EmacsContextMenu.java (inflateMenuItems): Fix context menus again. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Make all event sending functions return long. * java/org/gnu/emacs/EmacsPreferencesActivity.java (EmacsPreferencesActivity): New class. * java/org/gnu/emacs/EmacsService.java (EmacsService) (onStartCommand, onCreate, startEmacsService): Start as a foreground service if necessary to bypass system restrictions. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): * java/org/gnu/emacs/EmacsThread.java (EmacsThread, run): * java/org/gnu/emacs/EmacsView.java (EmacsView, onLayout) (onDetachedFromWindow): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, viewLayout): Implement frame resize synchronization.. * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager, removeWindowConsumer): Adjust accordingly for changes to frame deletion behavior. * lisp/frame.el (android-toggle-on-screen-keyboard) (frame-toggle-on-screen-keyboard): New function. * lisp/minibuffer.el (minibuffer-setup-on-screen-keyboard) (minibuffer-exit-on-screen-keyboard): New functions. (minibuffer-setup-hook, minibuffer-exit-hook): Add new functions to hooks. * lisp/touch-screen.el (touch-screen-relative-xy): Accept new value of window `frame'. Return frame coordinates in that case. (touch-screen-set-point-commands): New variable. (touch-screen-handle-point-up): Respect that variable. (touch-screen-track-drag): Return `no-drag' where appropriate. (touch-screen-drag-mode-line-1, touch-screen-drag-mode-line): Refactor to use `no-drag'. * src/android.c (struct android_emacs_window): New methods. Make all event sending functions return the event serial. (android_toggle_on_screen_keyboard, android_window_updated): New functions. * src/android.h: Update prototypes. * src/androidfns.c (Fandroid_toggle_on_screen_keyboard) (syms_of_androidfns): New function. * src/androidgui.h (struct android_any_event) (struct android_key_event, struct android_configure_event) (struct android_focus_event, struct android_window_action_event) (struct android_crossing_event, struct android_motion_event) (struct android_button_event, struct android_touch_event) (struct android_wheel_event, struct android_iconify_event) (struct android_menu_event): Add `serial' fields. * src/androidterm.c (handle_one_android_event) (android_frame_up_to_date): * src/androidterm.h (struct android_output): Implement frame resize synchronization. --- .gitignore | 1 + doc/emacs/Makefile.in | 2 + doc/emacs/android.texi | 70 +++++-- doc/emacs/emacs.texi | 1 + doc/emacs/input.texi | 36 ++++ doc/lispref/commands.texi | 8 +- doc/lispref/frames.texi | 21 +++ java/AndroidManifest.xml.in | 12 ++ java/org/gnu/emacs/EmacsActivity.java | 20 +- java/org/gnu/emacs/EmacsContextMenu.java | 1 + java/org/gnu/emacs/EmacsNative.java | 39 ++-- java/org/gnu/emacs/EmacsPreferencesActivity.java | 98 ++++++++++ java/org/gnu/emacs/EmacsService.java | 62 ++++++- java/org/gnu/emacs/EmacsSurfaceView.java | 127 +++++++++---- java/org/gnu/emacs/EmacsThread.java | 12 +- java/org/gnu/emacs/EmacsView.java | 55 +++++- java/org/gnu/emacs/EmacsWindow.java | 58 +++++- .../gnu/emacs/EmacsWindowAttachmentManager.java | 4 +- lisp/frame.el | 17 ++ lisp/minibuffer.el | 23 +++ lisp/touch-screen.el | 203 +++++++++++++-------- src/android.c | 147 ++++++++++++--- src/android.h | 2 + src/androidfns.c | 25 +++ src/androidgui.h | 20 ++ src/androidterm.c | 24 +++ src/androidterm.h | 5 + 27 files changed, 894 insertions(+), 199 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsPreferencesActivity.java (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/.gitignore b/.gitignore index 3bc40c67489..6494e4e8f39 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ src/emacs-module.h # Built by recursive call to `configure'. *.android !INSTALL.android +!verbose.mk.android # Built by `java'. java/install_temp/* diff --git a/doc/emacs/Makefile.in b/doc/emacs/Makefile.in index 161bdcb1c59..c7415312753 100644 --- a/doc/emacs/Makefile.in +++ b/doc/emacs/Makefile.in @@ -146,6 +146,8 @@ EMACSSOURCES= \ ${srcdir}/glossary.texi \ ${srcdir}/ack.texi \ ${srcdir}/kmacro.texi \ + ${srcdir}/android.texi \ + ${srcdir}/input.texi \ $(EMACS_XTRA) ## Disable implicit rules. diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 01a2e68a97e..ba5d2ca3e09 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -10,8 +10,8 @@ Alliance. This section describes the peculiarities of using Emacs on an Android device running Android 2.2 or later. Android devices commonly rely on user input through a touch screen -or digitizer device. For more information about using them with -Emacs, @pxref{other Input Devices}. +or digitizer device and on-screen keyboard. For more information +about using such devices with Emacs, @pxref{Other Input Devices}. @menu * What is Android?:: Preamble. @@ -82,6 +82,16 @@ command on that other system: $ adb logcat | grep -E "(android_run_debug_thread|[Ee]macs)" @end example +@cindex emacs -Q, android +Since Android has no command line, there is normally no way to specify +command-line arguments. However, Emacs can be started with the +equivalent of the @code{--quick} option (@pxref{Initial Options}) +through a special preferences screen, which can be accessed through +the Emacs ``app info'' page in the system settings application. + +Consult the manufacturer of your device for more details, as how to do +this varies by device. + @node Android File System @section What files Emacs can access under Android @cindex /assets directory, android @@ -149,7 +159,7 @@ which is the app data directory (@pxref{Android File System}.) directories, and the app data directories of other applications. In recent versions of Android, the system also prohibits, for security reasons, even Emacs itself from running executables inside the app -data directory! +data directory. Emacs comes with several binaries. While being executable files, they are packaged as libraries in the library directory, because @@ -173,9 +183,28 @@ within itself. Application processes are treated as disposable entities by the system. When all Emacs frames move to the background, Emacs is liable to be killed by the system at any time, for the purpose of saving -resources. There is currently no easy way to bypass these -restrictions, aside from keeping Emacs constantly running in the -foreground. +system resources. + + On Android 7.1 and earlier, Emacs tells the system to treat it as a +``background service''. The system will try to avoid killing Emacs +unless the device is under memory stress. + + Android 8.0 removed the ability for background services to receive +such special treatment. However, Emacs applies a workaround: the +system considers applications that create a permanent notification to +be performing active work, and will avoid killing such applications. +Thus, on those systems, Emacs displays a permanant notification for as +long as it is running. Once the notification is displayed, it can be +safely hidden through the system settings without resulting in Emacs +being killed. + + However, it is not guaranteed that the system will not kill Emacs, +even if the notification is being displayed. While the Open Handset +Alliance's sample implementation of Android behaves correctly, many +manufacturers place additional restrictions on program execution in +the background in their proprietary versions of Android. There is a +list of such troublesome manufacturers and sometimes workarounds, at +@url{https://dontkillmyapp.com/}. @section Android permissions @cindex external storage, android @@ -272,14 +301,14 @@ maximized or full-screen, and only one window can be displayed at a time. On larger devices, the system allows up to four windows to be tiled on the screen at any time. -Windows on Android do not continue to exist indefinitely after they + Windows on Android do not continue to exist indefinitely after they are created. Instead, the system may choose to terminate windows that are not on screen in order to save memory, with the assumption that the program will save its contents to disk and restore them later, when the user asks to open it again. As this is obvious not possible with Emacs, Emacs separates a frame from a system window. -Each system window created (including the initial window created + Each system window created (including the initial window created during Emacs startup) is appended to a list of windows that do not have associated frames. When a frame is created, Emacs looks up any window within that list, and displays the contents of the frame @@ -287,11 +316,28 @@ within; if there is no window at all, then one is created. Likewise, when a new window is created by the system, Emacs places the contents of any frame that is not already displayed within a window inside. When a frame is closed, the corresponding system window is also -closed. +closed. Upon startup, the system creates a window itself (within +which Emacs displays the first window system frame shortly +thereafter.) Emacs differentiates between that window and windows +created on behalf of other frames to determine what to do when the +system window associated with a frame is closed: + +@itemize @bullet +@item +When the system closes the window created during application startup +in order to save memory, Emacs retains the frame for when that window +is created later. -This strategy works as long as one window is in the foreground. -Otherwise, Emacs can only run in the background for a limited amount -of time before the process is killed completely. +@item +When the user closes the window created during application startup, +and the window was not previously closed by the system in order to +save resources, Emacs deletes any frame displayed within that window. + +@item +When the user or the system closes any window created by Emacs on +behalf of a specific frame, Emacs deletes the frame displayed within +that window. +@end itemize @cindex windowing limitations, android @cindex frame parameters, android diff --git a/doc/emacs/emacs.texi b/doc/emacs/emacs.texi index 9fd169cd5cc..0cb454e5294 100644 --- a/doc/emacs/emacs.texi +++ b/doc/emacs/emacs.texi @@ -1269,6 +1269,7 @@ Emacs and Android Emacs and unconventional input devices * Touchscreens:: Using Emacs on touchscreens. +* On-Screen Keyboards:: Using Emacs with virtual keyboards. Emacs and Microsoft Windows/MS-DOS diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index 0e029c401e3..1a58d1ca0ac 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -16,6 +16,7 @@ input devices, which is detailed here. @menu * Touchscreens:: Using Emacs on touchscreens. +* On-Screen Keyboards:: Using Emacs with virtual keyboards. @end menu @node Touchscreens @@ -58,3 +59,38 @@ were to be held down. @xref{Mouse Commands}. By default, Emacs considers a tool as having been left on the display for a while after 0.7 seconds, but this can be changed by customizing the variable @code{touch-screen-delay}. + +@node On-Screen Keyboards +@section Using Emacs with virtual keyboards +@cindex virtual keyboards +@cindex on-screen keyboards + + When there is no physical keyboard attached to a system, the +windowing system typically provides an on-screen keyboard, more often +known as a ``virtual keyboard'', containing rows of clickable buttons +that send keyboard input to the application, much like a real keyboard +would. This virtual keyboard is hidden by default, as it uses up +valuable on-screen real estate, and must be opened once the program +being used is ready to accept keyboard input. + + Under the X Window System, the client that provides the on-screen +keyboard typically detects when the application is ready to accept +keyboard input through a set of complex heuristics, and automatically +displays the keyboard when necessary. + + On other systems such as Android, Emacs must tell the system when it +is ready to accept keyboard input. Typically, this is done in +response to a touchscreen ``tap'' gesture (@pxref{Touchscreens}), or +once to the minibuffer becomes in use (@pxref{Minibuffer}.) + +@vindex touch-screen-set-point-commands + When a ``tap'' gesture results in a command being executed, Emacs +checks to see whether or not the command is supposed to set the point +by looking for it in the list @code{touch-screen-set-point-commands}. +If it is, then Emacs looks up whether or not the text under the point +is read-only; if not, it activates the on-screen keyboard, assuming +that the user is about to enter text in to the current buffer. + + Emacs also provides a set of functions to show or hide the on-screen +keyboard. For more details, @pxref{On-Screen Keyboards,,, elisp, The +Emacs Lisp Reference Manual}. diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 59367a2cecc..484c7dc2a06 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2055,9 +2055,11 @@ The caller should not perform any action in that case. @defun touch-screen-track-drag event update &optional data This function is used to track a single ``drag'' gesture originating -from the @code{touchscreen-begin} event @code{event}. Currently, it -behaves identically to @code{touch-screen-track-tap}, but differences -are anticipated in the future. +from the @code{touchscreen-begin} event @code{event}. + +It behaves like @code{touch-screen-track-tap}, except that it returns +@code{no-drag} if the touchpoint in @code{event} did not move far +enough to qualify as an actual drag. @end defun @node Focus Events diff --git a/doc/lispref/frames.texi b/doc/lispref/frames.texi index 2ceafab7a6b..497715bdb19 100644 --- a/doc/lispref/frames.texi +++ b/doc/lispref/frames.texi @@ -104,6 +104,7 @@ window of another Emacs frame. @xref{Child Frames}. * Mouse Tracking:: Getting events that say when the mouse moves. * Mouse Position:: Asking where the mouse is, or moving it. * Pop-Up Menus:: Displaying a menu for the user to select from. +* On-Screen Keyboards:: Displaying the virtual keyboard. * Dialog Boxes:: Displaying a box to ask yes or no. * Pointer Shape:: Specifying the shape of the mouse pointer. * Window System Selections:: Transferring text to and from other X clients. @@ -3812,6 +3813,26 @@ keymap. It won't be called if @code{x-popup-menu} returns for some other reason without displaying a pop-up menu. @end defvar +@node On-Screen Keyboards +@section On-Screen Keyboards + + An on-screen keyboard is a special kind of pop up provided by the +system, with rows of clickable buttons that act as a real keyboard. + + On certain systems (@pxref{On-Screen Keyboards,,,emacs, The Emacs +Manual}), Emacs is supposed to display and hide the on screen keyboard +depending on whether or not the user is about to type something. + +@defun frame-toggle-on-screen-keyboard frame hide +This function displays or hides the on-screen keyboard on behalf of +the frame @var{frame}. If @var{hide} is non-@code{nil}, then the +on-screen keyboard is hidden; otherwise, it is displayed. + +This has no effect if the system automatically detects when to display +the on-screen keyboard, or when it does not provide any on-screen +keyboard. +@end defun + @node Dialog Boxes @section Dialog Boxes @cindex dialog boxes diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index b680137a9d0..74f69d2a8e5 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -62,8 +62,10 @@ along with GNU Emacs. If not, see . --> android:theme="@android:style/Theme" android:debuggable="true" android:extractNativeLibs="true"> + @@ -73,8 +75,18 @@ along with GNU Emacs. If not, see . --> + + + + + + + . */ + +package org.gnu.emacs; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.LinearLayout; +import android.widget.TextView; + +import android.R; + +/* This module provides a ``preferences'' display for Emacs. It is + supposed to be launched from inside the Settings application to + perform various actions, such as starting Emacs with the ``-Q'' + option, which would not be possible otherwise, as there is no + command line on Android. */ + +public class EmacsPreferencesActivity extends Activity +{ + /* The linear layout associated with the activity. */ + private LinearLayout layout; + + /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now, and + tell the system to EmacsActivity with some parameters later. */ + + private void + startEmacsQ () + { + Intent intent; + + intent = new Intent (this, EmacsActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra ("org.gnu.emacs.START_DASH_Q", true); + startActivity (intent); + System.exit (0); + } + + @Override + public void + onCreate (Bundle savedInstanceState) + { + LinearLayout layout; + TextView textView; + LinearLayout.LayoutParams params; + int resid; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + setTheme (R.style.Theme_DeviceDefault_Settings); + else if (Build.VERSION.SDK_INT + >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme (R.style.Theme_DeviceDefault); + + layout = new LinearLayout (this); + layout.setOrientation (LinearLayout.VERTICAL); + setContentView (layout); + + textView = new TextView (this); + textView.setPadding (8, 20, 20, 8); + + params = new LinearLayout.LayoutParams (LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + textView.setLayoutParams (params); + textView.setText ("(Re)start Emacs with -Q"); + textView.setOnClickListener (new View.OnClickListener () { + @Override + public void + onClick (View view) + { + startEmacsQ (); + } + }); + layout.addView (textView); + + super.onCreate (savedInstanceState); + } +}; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index bcf8d9ff6e8..95f21b211a3 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -32,7 +32,13 @@ import android.view.InputDevice; import android.view.KeyEvent; import android.annotation.TargetApi; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.NotificationChannel; +import android.app.PendingIntent; import android.app.Service; + import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; @@ -63,6 +69,7 @@ public class EmacsService extends Service public static final String TAG = "EmacsService"; public static final int MAX_PENDING_REQUESTS = 256; public static volatile EmacsService SERVICE; + public static boolean needDashQ; private EmacsThread thread; private Handler handler; @@ -74,6 +81,31 @@ public class EmacsService extends Service public int onStartCommand (Intent intent, int flags, int startId) { + Notification notification; + NotificationManager manager; + NotificationChannel channel; + String infoBlurb; + Object tem; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + tem = getSystemService (Context.NOTIFICATION_SERVICE); + manager = (NotificationManager) tem; + infoBlurb = ("See (emacs)Android Environment for more" + + " details about this notification."); + channel + = new NotificationChannel ("emacs", "Emacs persistent notification", + NotificationManager.IMPORTANCE_DEFAULT); + manager.createNotificationChannel (channel); + notification = (new Notification.Builder (this, "emacs") + .setContentTitle ("Emacs") + .setContentText (infoBlurb) + .setSmallIcon (android.R.drawable.sym_def_app_icon) + .build ()); + manager.notify (1, notification); + startForeground (1, notification); + } + return START_NOT_STICKY; } @@ -137,7 +169,7 @@ public class EmacsService extends Service this); /* Start the thread that runs Emacs. */ - thread = new EmacsThread (this); + thread = new EmacsThread (this, needDashQ); thread.start (); } catch (IOException exception) @@ -444,4 +476,32 @@ public class EmacsService extends Service } } } + + + + /* Start the Emacs service if necessary. On Android 26 and up, + start Emacs as a foreground service with a notification, to avoid + it being killed by the system. + + On older systems, simply start it as a normal background + service. */ + + public static void + startEmacsService (Context context) + { + PendingIntent intent; + + if (EmacsService.SERVICE == null) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + /* Start the Emacs service now. */ + context.startService (new Intent (context, + EmacsService.class)); + else + /* Display the permanant notification and start Emacs as a + foreground service. */ + context.startForegroundService (new Intent (context, + EmacsService.class)); + } + } }; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index f713818d4bc..2fe9e103b2b 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -32,53 +32,106 @@ import android.util.Log; public class EmacsSurfaceView extends SurfaceView { private static final String TAG = "EmacsSurfaceView"; - public Object surfaceChangeLock; private boolean created; + private EmacsView view; - public - EmacsSurfaceView (final EmacsView view) - { - super (view.getContext ()); - - surfaceChangeLock = new Object (); + /* This is the callback used on Android 8 to 25. */ - getHolder ().addCallback (new SurfaceHolder.Callback () { - @Override - public void - surfaceChanged (SurfaceHolder holder, int format, - int width, int height) + private class Callback implements SurfaceHolder.Callback + { + @Override + public void + surfaceChanged (SurfaceHolder holder, int format, + int width, int height) + { + Log.d (TAG, "surfaceChanged: " + view + ", " + view.pendingConfigure); + + /* Make sure not to swap buffers if there is pending + configuration, because otherwise the redraw callback will not + run correctly. */ + + if (view.pendingConfigure == 0) + view.swapBuffers (); + } + + @Override + public void + surfaceCreated (SurfaceHolder holder) + { + synchronized (surfaceChangeLock) { - Log.d (TAG, "surfaceChanged: " + view); - view.swapBuffers (); + Log.d (TAG, "surfaceCreated: " + view); + created = true; } - @Override - public void - surfaceCreated (SurfaceHolder holder) - { - synchronized (surfaceChangeLock) - { - Log.d (TAG, "surfaceCreated: " + view); - created = true; - } - - /* Drop the lock when doing this, or a deadlock can - result. */ - view.swapBuffers (); - } + /* Drop the lock when doing this, or a deadlock can + result. */ + view.swapBuffers (); + } - @Override - public void - surfaceDestroyed (SurfaceHolder holder) + @Override + public void + surfaceDestroyed (SurfaceHolder holder) + { + synchronized (surfaceChangeLock) { - synchronized (surfaceChangeLock) - { - Log.d (TAG, "surfaceDestroyed: " + view); - created = false; - } + Log.d (TAG, "surfaceDestroyed: " + view); + created = false; } - }); + } + } + + /* And this is the callback used on Android 26 and later. It is + used because it can tell the system when drawing completes. */ + + private class Callback2 extends Callback implements SurfaceHolder.Callback2 + { + @Override + public void + surfaceRedrawNeeded (SurfaceHolder holder) + { + /* This version is not supported. */ + return; + } + + @Override + public void + surfaceRedrawNeededAsync (SurfaceHolder holder, + Runnable drawingFinished) + { + Runnable old; + + Log.d (TAG, "surfaceRedrawNeededAsync: " + view.pendingConfigure); + + /* The system calls this function when it wants to know whether + or not Emacs is still configuring itself in response to a + resize. + + If the view did not send an outstanding ConfigureNotify + event, then call drawingFinish immediately. Else, give it to + the view to execute after drawing completes. */ + + if (view.pendingConfigure == 0) + drawingFinished.run (); + else + /* And set this runnable to run once drawing completes. */ + view.drawingFinished = drawingFinished; + } + } + + public + EmacsSurfaceView (final EmacsView view) + { + super (view.getContext ()); + + this.surfaceChangeLock = new Object (); + this.view = view; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + getHolder ().addCallback (new Callback ()); + else + getHolder ().addCallback (new Callback2 ()); } public boolean diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 0882753747f..f9bc132f354 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -23,12 +23,13 @@ import java.lang.Thread; public class EmacsThread extends Thread { - EmacsService context; + /* Whether or not Emacs should be started -Q. */ + private boolean startDashQ; public - EmacsThread (EmacsService service) + EmacsThread (EmacsService service, boolean startDashQ) { - context = service; + this.startDashQ = startDashQ; } public void @@ -36,7 +37,10 @@ public class EmacsThread extends Thread { String args[]; - args = new String[] { "libandroid-emacs.so", }; + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", }; + else + args = new String[] { "libandroid-emacs.so", "-Q", }; /* Run the native code now. */ EmacsNative.initEmacs (args); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 82f44acaebe..74bbb7b3ecc 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -19,6 +19,7 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import android.content.Context; import android.content.res.ColorStateList; import android.view.ContextMenu; @@ -27,6 +28,8 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; + import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; @@ -86,11 +89,23 @@ public class EmacsView extends ViewGroup /* The serial of the last clip rectangle change. */ private long lastClipSerial; + /* The InputMethodManager for this view's context. */ + private InputMethodManager imManager; + + /* Runnable that will run once drawing completes. */ + public Runnable drawingFinished; + + /* Serial of the last ConfigureNotify event sent that Emacs has not + yet responded to. 0 if there is no such outstanding event. */ + public long pendingConfigure; + public EmacsView (EmacsWindow window) { super (EmacsService.SERVICE); + Object tem; + this.window = window; this.damageRegion = new Region (); this.paint = new Paint (); @@ -111,6 +126,10 @@ public class EmacsView extends ViewGroup /* Get rid of the default focus highlight. */ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) setDefaultFocusHighlightEnabled (false); + + /* Obtain the input method manager. */ + tem = getContext ().getSystemService (Context.INPUT_METHOD_SERVICE); + imManager = (InputMethodManager) tem; } private void @@ -259,7 +278,8 @@ public class EmacsView extends ViewGroup if (changed || mustReportLayout) { mustReportLayout = false; - window.viewLayout (left, top, right, bottom); + pendingConfigure + = window.viewLayout (left, top, right, bottom); } measuredWidth = right - left; @@ -538,4 +558,37 @@ public class EmacsView extends ViewGroup Runtime.getRuntime ().gc (); } } + + public void + showOnScreenKeyboard () + { + /* Specifying no flags at all tells the system the user asked for + the input method to be displayed. */ + imManager.showSoftInput (this, 0); + } + + public void + hideOnScreenKeyboard () + { + imManager.hideSoftInputFromWindow (this.getWindowToken (), + 0); + } + + public void + windowUpdated (long serial) + { + Log.d (TAG, "windowUpdated: serial is " + serial); + + if (pendingConfigure <= serial + /* Detect wraparound. */ + || pendingConfigure - serial >= 0x7fffffff) + { + pendingConfigure = 0; + + if (drawingFinished != null) + drawingFinished.run (); + + drawingFinished = null; + } + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index c5b1522086c..8511af9193e 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -233,7 +233,7 @@ public class EmacsWindow extends EmacsHandleObject return attached; } - public void + public long viewLayout (int left, int top, int right, int bottom) { int rectWidth, rectHeight; @@ -249,10 +249,10 @@ public class EmacsWindow extends EmacsHandleObject rectWidth = right - left; rectHeight = bottom - top; - EmacsNative.sendConfigureNotify (this.handle, - System.currentTimeMillis (), - left, top, rectWidth, - rectHeight); + return EmacsNative.sendConfigureNotify (this.handle, + System.currentTimeMillis (), + left, top, rectWidth, + rectHeight); } public void @@ -589,11 +589,20 @@ public class EmacsWindow extends EmacsHandleObject EmacsActivity.invalidateFocus (); } + /* Notice that the activity has been detached or destroyed. + + ISFINISHING is set if the activity is not the main activity, or + if the activity was not destroyed in response to explicit user + action. */ + public void - onActivityDetached () + onActivityDetached (boolean isFinishing) { - /* Destroy the associated frame when the activity is detached. */ - EmacsNative.sendWindowAction (this.handle, 0); + /* Destroy the associated frame when the activity is detached in + response to explicit user action. */ + + if (isFinishing) + EmacsNative.sendWindowAction (this.handle, 0); } /* Look through the button state to determine what button EVENT was @@ -1064,4 +1073,37 @@ public class EmacsWindow extends EmacsHandleObject /* Return the resulting coordinates. */ return array; } + + public void + toggleOnScreenKeyboard (final boolean on) + { + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + if (on) + view.showOnScreenKeyboard (); + else + view.hideOnScreenKeyboard (); + } + }); + } + + /* Notice that outstanding configure events have been processed. + SERIAL is checked in the UI thread to verify that no new + configure events have been generated in the mean time. */ + + public void + windowUpdated (final long serial) + { + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.windowUpdated (serial); + } + }); + } }; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 15eb3bb65c2..510300571b8 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -134,7 +134,7 @@ public class EmacsWindowAttachmentManager } public void - removeWindowConsumer (WindowConsumer consumer) + removeWindowConsumer (WindowConsumer consumer, boolean isFinishing) { EmacsWindow window; @@ -147,7 +147,7 @@ public class EmacsWindowAttachmentManager Log.d (TAG, "removeWindowConsumer: detaching " + window); consumer.detachWindow (); - window.onActivityDetached (); + window.onActivityDetached (isFinishing); } Log.d (TAG, "removeWindowConsumer: removing " + consumer); diff --git a/lisp/frame.el b/lisp/frame.el index f21c0c369bd..f8ef17325ec 100644 --- a/lisp/frame.el +++ b/lisp/frame.el @@ -2553,6 +2553,23 @@ symbols." ((string= name "Virtual core keyboard") 'core-keyboard)))))) + +;;;; On-screen keyboard management. + +(declare-function android-toggle-on-screen-keyboard "androidfns.c") + +(defun frame-toggle-on-screen-keyboard (frame hide) + "Display or hide the on-screen keyboard. +On systems with an on-screen keyboard, display the on screen +keyboard on behalf of the frame FRAME if HIDE is nil. Else, hide +the on screen keyboard. + +FRAME must already have the input focus for this to work + reliably." + (let ((frame-type (framep-on-display frame))) + (cond ((eq frame-type 'android) + (android-toggle-on-screen-keyboard frame hide))))) + ;;;; Frame geometry values diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el index 21d4607e7cf..3c42f29cd1f 100644 --- a/lisp/minibuffer.el +++ b/lisp/minibuffer.el @@ -4576,6 +4576,29 @@ is included in the return value." default))) ": ")) + +;;; On screen keyboard support. +;; Try to display the on screen keyboard whenever entering the +;; mini-buffer, and hide it whenever leaving. + +(defun minibuffer-setup-on-screen-keyboard () + "Maybe display the on-screen keyboard in the current frame. +Display the on-screen keyboard in the current frame if the +last device to have sent an input event is not a keyboard. +This is run upon minibuffer setup." + (when (not (memq (device-class last-event-frame + last-event-device) + '(keyboard core-keyboard))) + (frame-toggle-on-screen-keyboard (selected-frame) nil))) + +(defun minibuffer-exit-on-screen-keyboard () + "Hide the on-screen keyboard if it was displayed. +This is run upon minibuffer exit." + (frame-toggle-on-screen-keyboard (selected-frame) t)) + +(add-hook 'minibuffer-setup-hook #'minibuffer-setup-on-screen-keyboard) +(add-hook 'minibuffer-exit-hook #'minibuffer-exit-on-screen-keyboard) + (provide 'minibuffer) ;;; minibuffer.el ends here diff --git a/lisp/touch-screen.el b/lisp/touch-screen.el index bcc4f5e9be3..a1c9e0b4afd 100644 --- a/lisp/touch-screen.el +++ b/lisp/touch-screen.el @@ -38,6 +38,12 @@ touch point, and the initial position of the touchpoint. See `touch-screen-handle-point-update' for the meanings of the fourth element.") +(defvar touch-screen-set-point-commands '(mouse-set-point) + "List of commands known to set the point. +This is used to determine whether or not to display the on-screen +keyboard after a mouse command is executed in response to a +`touchscreen-end' event.") + (defvar touch-screen-current-timer nil "Timer used to track long-presses. This is always cleared upon any significant state change.") @@ -54,20 +60,28 @@ However, return the coordinates relative to WINDOW. If (posn-window posn) is the same as window, simply return the coordinates in POSN. Otherwise, convert them to the frame, and -then back again." - (if (eq (posn-window posn) window) +then back again. + +If WINDOW is the symbol `frame', simply convert the coordinates +to the frame that they belong in." + (if (or (eq (posn-window posn) window) + (and (eq window 'frame) + (framep (posn-window posn)))) (posn-x-y posn) (let ((xy (posn-x-y posn)) - (edges (window-inside-pixel-edges window))) + (edges (and (windowp window) + (window-inside-pixel-edges window)))) ;; Make the X and Y positions frame relative. (when (windowp (posn-window posn)) (let ((edges (window-inside-pixel-edges (posn-window posn)))) (setq xy (cons (+ (car xy) (car edges)) (+ (cdr xy) (cadr edges)))))) - ;; Make the X and Y positions window relative again. - (cons (- (car xy) (car edges)) - (- (cdr xy) (cadr edges)))))) + (if (eq window 'frame) + xy + ;; Make the X and Y positions window relative again. + (cons (- (car xy) (car edges)) + (- (cdr xy) (cadr edges))))))) (defun touch-screen-handle-scroll (dx dy) "Scroll the display assuming that a touch point has moved by DX and DY." @@ -252,7 +266,12 @@ If the fourth argument of `touch-screen-current-tool' is nil, move point to the position of POINT, selecting the window under POINT as well, and deactivate the mark; if there is a button or link at POINT, call the command bound to `mouse-2' there. -Otherwise, call the command bound to `mouse-1'." +Otherwise, call the command bound to `mouse-1'. + +If the command being executed is listed in +`touch-screen-set-point-commands' also display the on-screen +keyboard if the current buffer and the character at the new point +is not read-only." (let ((what (nth 3 touch-screen-current-tool))) (cond ((null what) (when (windowp (posn-window (cdr point))) @@ -283,9 +302,17 @@ Otherwise, call the command bound to `mouse-1'." (deactivate-mark) ;; This is necessary for following links. (goto-char (posn-point (cdr point))) + ;; Figure out if the on screen keyboard needs to be + ;; displayed. (when command (call-interactively command nil - (vector event))))))))) + (vector event)) + (when (memq command touch-screen-set-point-commands) + (if (not (or buffer-read-only + (get-text-property (point) 'read-only))) + (frame-toggle-on-screen-keyboard (selected-frame) nil) + ;; Otherwise, hide the on screen keyboard now. + (frame-toggle-on-screen-keyboard (selected-frame) t)))))))))) (defun touch-screen-handle-touch (event) "Handle a single touch EVENT, and perform associated actions. @@ -379,22 +406,34 @@ touch point with the same ID as in EVENT, call UPDATE with the touch point in event and DATA. Return nil immediately if any other kind of event is received; -otherwise, return t once the `touchscreen-end' event arrives." - (catch 'finish - (while t - (let ((new-event (read-event))) - (cond - ((eq (car-safe new-event) 'touchscreen-update) - (let ((tool (assq (caadr event) (nth 1 new-event)))) - (when (and update tool) - (funcall update new-event data)))) - ((eq (car-safe new-event) 'touchscreen-end) - (throw 'finish - ;; Now determine whether or not the `touchscreen-end' - ;; event has the same ID as EVENT. If it doesn't, - ;; then this is another touch, so return nil. - (eq (caadr event) (caadr new-event)))) - (t (throw 'finish nil))))))) +otherwise, return either t or `no-drag' once the +`touchscreen-end' event arrives; return `no-drag' returned if the +touch point in EVENT did not move significantly, and t otherwise." + (let ((return-value 'no-drag) + (start-xy (touch-screen-relative-xy (cdadr event) + 'frame))) + (catch 'finish + (while t + (let ((new-event (read-event))) + (cond + ((eq (car-safe new-event) 'touchscreen-update) + (when-let* ((tool (assq (caadr event) (nth 1 new-event))) + (xy (touch-screen-relative-xy (cdr tool) 'frame))) + (when (or (> (- (car xy) (car start-xy)) 5) + (< (- (car xy) (car start-xy)) -5) + (> (- (cdr xy) (cdr start-xy)) 5) + (< (- (cdr xy) (cdr start-xy)) -5)) + (setq return-value t)) + (when (and update tool) + (funcall update new-event data)))) + ((eq (car-safe new-event) 'touchscreen-end) + (throw 'finish + ;; Now determine whether or not the `touchscreen-end' + ;; event has the same ID as EVENT. If it doesn't, + ;; then this is another touch, so return nil. + (and (eq (caadr event) (caadr new-event)) + return-value))) + (t (throw 'finish nil)))))))) @@ -402,45 +441,8 @@ otherwise, return t once the `touchscreen-end' event arrives." (defun touch-screen-drag-mode-line-1 (event) "Internal helper for `touch-screen-drag-mode-line'. -This is called when that function determines it need not execute -any keymaps on the mode line at that particular spot." - ;; Find the window that should be dragged and the starting position. - (let* ((window (posn-window (cdadr event))) - (relative-xy (touch-screen-relative-xy - (cdadr event) window)) - (last-position (cdr relative-xy))) - (when (window-resizable window 0) - (touch-screen-track-drag - event (lambda (new-event &optional _data) - ;; Find the position of the touchpoint in NEW-EVENT. - (let* ((touchpoint (assq (caadr event) (cadr new-event))) - (new-relative-xy - (touch-screen-relative-xy (cdr touchpoint) - window)) - (position (cdr new-relative-xy)) - growth) - ;; Now set the new height of the window. - ;; If new-relative-y is above relative-xy, then - ;; make the window that much shorter. Otherwise, - ;; make it bigger. - (unless (or (zerop (setq growth (- position last-position))) - (and (> growth 0) - (< position (+ (window-pixel-top window) - (window-pixel-height window)))) - (and (< growth 0) - (> position (+ (window-pixel-top window) - (window-pixel-height window))))) - (adjust-window-trailing-edge window growth nil t)) - (setq last-position position))))))) - -(defun touch-screen-drag-mode-line (event) - "Begin dragging the mode line in response to a touch EVENT. -If EVENT lies on top of text with a mouse command bound, run -that command instead. - -Change the height of the window based on where the touch point -in EVENT moves." - (interactive "e") +This is called when that function determines that no drag really +happened. EVENT is the same as in `touch-screen-drag-mode-line'." ;; If there is an object at EVENT, then look either a keymap bound ;; to [down-mouse-1] or a command bound to [mouse-1]. Then, if a ;; keymap was found, pop it up as a menu. Otherwise, wait for a tap @@ -457,21 +459,66 @@ in EVENT moves." (keymap (lookup-key object-keymap [mode-line down-mouse-1])) (command (or (lookup-key object-keymap [mode-line mouse-1]) keymap))) - (if (or (keymapp keymap) command) - (if (keymapp keymap) - (when (touch-screen-track-tap event) - (when-let* ((command (x-popup-menu event keymap)) - (tem (lookup-key keymap - (if (consp command) - (apply #'vector command) - (vector command)) - t))) - (call-interactively tem))) - (when (and (commandp command) - (touch-screen-track-tap event)) - (call-interactively command nil - (vector (list 'mouse-1 (cdadr event)))))) - (touch-screen-drag-mode-line-1 event)))) + (when (or (keymapp keymap) command) + (if (keymapp keymap) + (when-let* ((command (x-popup-menu event keymap)) + (tem (lookup-key keymap + (if (consp command) + (apply #'vector command) + (vector command)) + t))) + (call-interactively tem)) + (when (commandp command) + (call-interactively command nil + (vector (list 'mouse-1 (cdadr event))))))))) + +(defun touch-screen-drag-mode-line (event) + "Begin dragging the mode line in response to a touch EVENT. +Change the height of the window based on where the touch point in +EVENT moves. + +If it does not actually move anywhere and the touch point is +removed, and EVENT lies on top of text with a mouse command +bound, run that command instead." + (interactive "e") + ;; Find the window that should be dragged and the starting position. + (let* ((window (posn-window (cdadr event))) + (relative-xy (touch-screen-relative-xy + (cdadr event) window)) + (last-position (cdr relative-xy))) + (when (window-resizable window 0) + (when (eq + (touch-screen-track-drag + event (lambda (new-event &optional _data) + ;; Find the position of the touchpoint in + ;; NEW-EVENT. + (let* ((touchpoint (assq (caadr event) + (cadr new-event))) + (new-relative-xy + (touch-screen-relative-xy (cdr touchpoint) + window)) + (position (cdr new-relative-xy)) + growth) + ;; Now set the new height of the window. If + ;; new-relative-y is above relative-xy, then + ;; make the window that much shorter. + ;; Otherwise, make it bigger. + (unless (or (zerop (setq growth + (- position last-position))) + (and (> growth 0) + (< position + (+ (window-pixel-top window) + (window-pixel-height window)))) + (and (< growth 0) + (> position + (+ (window-pixel-top window) + (window-pixel-height window))))) + (adjust-window-trailing-edge window growth nil t)) + (setq last-position position)))) + 'no-drag) + ;; Dragging did not actually happen, so try to run any command + ;; necessary. + (touch-screen-drag-mode-line-1 event))))) (global-set-key [mode-line touchscreen-begin] #'touch-screen-drag-mode-line) diff --git a/src/android.c b/src/android.c index eb9c404f1a3..43ac3b3f754 100644 --- a/src/android.c +++ b/src/android.c @@ -121,6 +121,8 @@ struct android_emacs_window { jclass class; jmethodID swap_buffers; + jmethodID toggle_on_screen_keyboard; + jmethodID window_updated; }; /* The asset manager being used. */ @@ -193,6 +195,10 @@ static struct android_emacs_drawable drawable_class; /* Various methods associated with the EmacsWindow class. */ static struct android_emacs_window window_class; +/* The last event serial used. This is a 32 bit value, but it is + stored in unsigned long to be consistent with X. */ +static unsigned int event_serial; + /* Event handling functions. Events are stored on a (circular) queue @@ -1435,6 +1441,9 @@ android_init_emacs_window (void) assert (window_class.c_name); FIND_METHOD (swap_buffers, "swapBuffers", "()V"); + FIND_METHOD (toggle_on_screen_keyboard, + "toggleOnScreenKeyboard", "(Z)V"); + FIND_METHOD (window_updated, "windowUpdated", "(J)V"); #undef FIND_METHOD } @@ -1497,7 +1506,7 @@ NATIVE_NAME (emacsAbort) (JNIEnv *env, jobject object) emacs_abort (); } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, jshort window, jlong time, jint x, jint y, jint width, @@ -1506,6 +1515,7 @@ NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, union android_event event; event.xconfigure.type = ANDROID_CONFIGURE_NOTIFY; + event.xconfigure.serial = ++event_serial; event.xconfigure.window = window; event.xconfigure.time = time; event.xconfigure.x = x; @@ -1514,9 +1524,10 @@ NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, event.xconfigure.height = height; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -1525,6 +1536,7 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, union android_event event; event.xkey.type = ANDROID_KEY_PRESS; + event.xkey.serial = ++event_serial; event.xkey.window = window; event.xkey.time = time; event.xkey.state = state; @@ -1532,9 +1544,10 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, event.xkey.unicode_char = unicode_char; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -1543,6 +1556,7 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, union android_event event; event.xkey.type = ANDROID_KEY_RELEASE; + event.xkey.serial = ++event_serial; event.xkey.window = window; event.xkey.time = time; event.xkey.state = state; @@ -1550,48 +1564,55 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, event.xkey.unicode_char = unicode_char; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, jshort window, jlong time) { union android_event event; - event.xkey.type = ANDROID_FOCUS_IN; - event.xkey.window = window; - event.xkey.time = time; + event.xfocus.type = ANDROID_FOCUS_IN; + event.xfocus.serial = ++event_serial; + event.xfocus.window = window; + event.xfocus.time = time; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, jshort window, jlong time) { union android_event event; - event.xkey.type = ANDROID_FOCUS_OUT; - event.xkey.window = window; - event.xkey.time = time; + event.xfocus.type = ANDROID_FOCUS_OUT; + event.xfocus.serial = ++event_serial; + event.xfocus.window = window; + event.xfocus.time = time; android_write_event (&event); + return ++event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, jshort window, jint action) { union android_event event; event.xaction.type = ANDROID_WINDOW_ACTION; + event.xaction.serial = ++event_serial; event.xaction.window = window; event.xaction.action = action; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -1599,15 +1620,17 @@ NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, union android_event event; event.xcrossing.type = ANDROID_ENTER_NOTIFY; + event.xcrossing.serial = ++event_serial; event.xcrossing.window = window; event.xcrossing.x = x; event.xcrossing.y = y; event.xcrossing.time = time; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -1615,15 +1638,17 @@ NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, union android_event event; event.xcrossing.type = ANDROID_LEAVE_NOTIFY; + event.xcrossing.serial = ++event_serial; event.xcrossing.window = window; event.xcrossing.x = x; event.xcrossing.y = y; event.xcrossing.time = time; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -1631,15 +1656,17 @@ NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, union android_event event; event.xmotion.type = ANDROID_MOTION_NOTIFY; + event.xmotion.serial = ++event_serial; event.xmotion.window = window; event.xmotion.x = x; event.xmotion.y = y; event.xmotion.time = time; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -1648,6 +1675,7 @@ NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, union android_event event; event.xbutton.type = ANDROID_BUTTON_PRESS; + event.xbutton.serial = ++event_serial; event.xbutton.window = window; event.xbutton.x = x; event.xbutton.y = y; @@ -1656,9 +1684,10 @@ NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, event.xbutton.button = button; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -1667,6 +1696,7 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, union android_event event; event.xbutton.type = ANDROID_BUTTON_RELEASE; + event.xbutton.serial = ++event_serial; event.xbutton.window = window; event.xbutton.x = x; event.xbutton.y = y; @@ -1675,9 +1705,10 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, event.xbutton.button = button; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -1685,6 +1716,7 @@ NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, union android_event event; event.touch.type = ANDROID_TOUCH_DOWN; + event.touch.serial = ++event_serial; event.touch.window = window; event.touch.x = x; event.touch.y = y; @@ -1692,9 +1724,10 @@ NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, event.touch.pointer_id = pointer_id; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -1702,6 +1735,7 @@ NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, union android_event event; event.touch.type = ANDROID_TOUCH_UP; + event.touch.serial = ++event_serial; event.touch.window = window; event.touch.x = x; event.touch.y = y; @@ -1709,9 +1743,10 @@ NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, event.touch.pointer_id = pointer_id; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -1719,6 +1754,7 @@ NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, union android_event event; event.touch.type = ANDROID_TOUCH_MOVE; + event.touch.serial = ++event_serial; event.touch.window = window; event.touch.x = x; event.touch.y = y; @@ -1726,9 +1762,10 @@ NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, event.touch.pointer_id = pointer_id; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -1737,6 +1774,7 @@ NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, union android_event event; event.wheel.type = ANDROID_WHEEL; + event.wheel.serial = ++event_serial; event.wheel.window = window; event.wheel.x = x; event.wheel.y = y; @@ -1746,43 +1784,50 @@ NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, event.wheel.y_delta = y_delta; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, jshort window) { union android_event event; event.iconified.type = ANDROID_ICONIFIED; + event.iconified.serial = ++event_serial; event.iconified.window = window; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, jshort window) { union android_event event; event.iconified.type = ANDROID_DEICONIFIED; + event.iconified.serial = ++event_serial; event.iconified.window = window; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, jshort window, jint menu_event_id) { union android_event event; event.menu.type = ANDROID_CONTEXT_MENU; + event.menu.serial = ++event_serial; event.menu.window = window; event.menu.menu_event_id = menu_event_id; android_write_event (&event); + return event_serial; } #ifdef __clang__ @@ -3599,6 +3644,15 @@ android_translate_coordinates (android_window src, int x, ANDROID_DELETE_LOCAL_REF (coordinates); } +void +android_sync (void) +{ + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.sync); + android_exception_check (); +} + /* Low level drawing primitives. */ @@ -3795,12 +3849,45 @@ android_get_keysym_name (int keysym, char *name_return, size_t size) ANDROID_DELETE_LOCAL_REF (string); } +/* Display the on screen keyboard on window WINDOW, or hide it if SHOW + is false. Ask the system to bring up or hide the on-screen + keyboard on behalf of WINDOW. The request may be rejected by the + system, especially when the window does not have the input + focus. */ + void -android_sync (void) +android_toggle_on_screen_keyboard (android_window window, bool show) { - (*android_java_env)->CallVoidMethod (android_java_env, - emacs_service, - service_class.sync); + jobject object; + jmethodID method; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + method = window_class.toggle_on_screen_keyboard; + + /* Now display the on screen keyboard. */ + (*android_java_env)->CallVoidMethod (android_java_env, object, + method, (jboolean) show); + + /* Check for out of memory errors. */ + android_exception_check (); +} + +/* Tell the window system that all configure events sent to WINDOW + have been fully processed, and that it is now okay to display its + new contents. SERIAL is the serial of the last configure event + processed. */ + +void +android_window_updated (android_window window, unsigned long serial) +{ + jobject object; + jmethodID method; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + method = window_class.window_updated; + + (*android_java_env)->CallVoidMethod (android_java_env, object, + method, (jlong) serial); android_exception_check (); } @@ -3811,7 +3898,7 @@ android_sync (void) #undef faccessat /* Replace the system faccessat with one which understands AT_EACCESS. - Android's faccessat simply fails upon using AT_EACCESS, so repalce + Android's faccessat simply fails upon using AT_EACCESS, so replace it with zero here. This isn't caught during configuration. This replacement is only done when building for Android 17 or @@ -3829,7 +3916,7 @@ faccessat (int dirfd, const char *pathname, int mode, int flags) return real_faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); } -#endif /* __ANDROID_API__ < 16 */ +#endif /* __ANDROID_API__ >= 17 */ diff --git a/src/android.h b/src/android.h index 240bc90d831..97818ab4911 100644 --- a/src/android.h +++ b/src/android.h @@ -91,6 +91,8 @@ extern void android_exception_check (void); extern void android_get_keysym_name (int, char *, size_t); extern void android_wait_event (void); +extern void android_toggle_on_screen_keyboard (android_window, bool); +extern void android_window_updated (android_window, unsigned long); diff --git a/src/androidfns.c b/src/androidfns.c index bb37c415069..77ee2e8de44 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -2332,6 +2332,30 @@ there is no mouse. */) #endif } +DEFUN ("android-toggle-on-screen-keyboard", + Fandroid_toggle_on_screen_keyboard, + Sandroid_toggle_on_screen_keyboard, 2, 2, 0, + doc: /* Display or hide the on-screen keyboard. +If HIDE is non-nil, hide the on screen keyboard if it is currently +being displayed. Else, request that the system display it on behalf +of FRAME. This request may be rejected if FRAME does not have the +input focus. */) + (Lisp_Object frame, Lisp_Object hide) +{ +#ifndef ANDROID_STUBIFY + struct frame *f; + + f = decode_window_system_frame (frame); + + block_input (); + android_toggle_on_screen_keyboard (FRAME_ANDROID_WINDOW (f), + NILP (hide)); + unblock_input (); +#endif + + return Qnil; +} + #ifndef ANDROID_STUBIFY @@ -2781,6 +2805,7 @@ syms_of_androidfns (void) defsubr (&Sx_show_tip); defsubr (&Sx_hide_tip); defsubr (&Sandroid_detect_mouse); + defsubr (&Sandroid_toggle_on_screen_keyboard); #ifndef ANDROID_STUBIFY tip_timer = Qnil; diff --git a/src/androidgui.h b/src/androidgui.h index 1f28c18ff34..3b9a74dc0b0 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -239,6 +239,7 @@ enum android_event_type struct android_any_event { enum android_event_type type; + unsigned long serial; android_window window; }; @@ -252,6 +253,7 @@ enum android_modifier_mask struct android_key_event { enum android_event_type type; + unsigned long serial; android_window window; android_time time; unsigned int state; @@ -271,6 +273,7 @@ struct android_key_event struct android_configure_event { enum android_event_type type; + unsigned long serial; android_window window; android_time time; int x, y; @@ -280,6 +283,7 @@ struct android_configure_event struct android_focus_event { enum android_event_type type; + unsigned long serial; android_window window; android_time time; }; @@ -287,6 +291,7 @@ struct android_focus_event struct android_window_action_event { enum android_event_type type; + unsigned long serial; /* The window handle. This can be ANDROID_NONE. */ android_window window; @@ -301,6 +306,7 @@ struct android_window_action_event struct android_crossing_event { enum android_event_type type; + unsigned long serial; android_window window; int x, y; unsigned long time; @@ -309,6 +315,7 @@ struct android_crossing_event struct android_motion_event { enum android_event_type type; + unsigned long serial; android_window window; int x, y; unsigned long time; @@ -317,6 +324,7 @@ struct android_motion_event struct android_button_event { enum android_event_type type; + unsigned long serial; android_window window; int x, y; unsigned long time; @@ -329,6 +337,9 @@ struct android_touch_event /* Type of the event. */ enum android_event_type type; + /* Serial identifying the event. */ + unsigned long serial; + /* Window associated with the event. */ android_window window; @@ -347,6 +358,9 @@ struct android_wheel_event /* Type of the event. */ enum android_event_type type; + /* Serial identifying the event. */ + unsigned long serial; + /* Window associated with the event. */ android_window window; @@ -368,6 +382,9 @@ struct android_iconify_event /* Type of the event. */ enum android_event_type type; + /* Serial identifying the event. */ + unsigned long serial; + /* Window associated with the event. */ android_window window; }; @@ -377,6 +394,9 @@ struct android_menu_event /* Type of the event. */ enum android_event_type type; + /* Serial identifying the event. */ + unsigned long serial; + /* Window associated with the event. Always None. */ android_window window; diff --git a/src/androidterm.c b/src/androidterm.c index 3c16b542d91..3024890d789 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -591,7 +591,17 @@ handle_one_android_event (struct android_display_info *dpyinfo, android_clear_under_internal_border (f); SET_FRAME_GARBAGED (f); cancel_mouse_face (f); + + /* Now stash the serial of this configure event somewhere, + and call android_window_updated with it once the redraw + completes. */ + FRAME_OUTPUT_DATA (f)->last_configure_serial + = configureEvent.xconfigure.serial; } + else + /* Reply to this ConfigureNotify event immediately. */ + android_window_updated (FRAME_ANDROID_WINDOW (f), + configureEvent.xconfigure.serial); goto OTHER; @@ -1194,6 +1204,9 @@ handle_one_android_event (struct android_display_info *dpyinfo, /* Iconification. This is vastly simpler than on X. */ case ANDROID_ICONIFIED: + if (!any) + goto OTHER; + if (FRAME_ICONIFIED_P (any)) goto OTHER; @@ -1206,6 +1219,9 @@ handle_one_android_event (struct android_display_info *dpyinfo, case ANDROID_DEICONIFIED: + if (!any) + goto OTHER; + if (!FRAME_ICONIFIED_P (any)) goto OTHER; @@ -1311,6 +1327,14 @@ android_frame_up_to_date (struct frame *f) /* The frame is now complete, as its contents have been drawn. */ FRAME_ANDROID_COMPLETE_P (f) = true; + /* If there was an outstanding configure event, then tell system + that the update has finished and the new contents can now be + displayed. */ + if (FRAME_OUTPUT_DATA (f)->last_configure_serial) + android_window_updated (FRAME_ANDROID_WINDOW (f), + FRAME_OUTPUT_DATA (f)->last_configure_serial); + FRAME_OUTPUT_DATA (f)->last_configure_serial = 0; + /* Shrink the scanline buffer used by the font backend. */ sfntfont_android_shrink_scanline_buffer (); unblock_input (); diff --git a/src/androidterm.h b/src/androidterm.h index c0f862e35fb..11b24d40b73 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -241,6 +241,11 @@ struct android_output /* List of all tools (either styluses or fingers) pressed onto the frame. */ struct android_touch_point *touch_points; + + /* Event serial of the last ConfigureNotify event received that has + not yet been drawn. This is used to synchronize resize with the + window system. 0 if no such outstanding event exists. */ + unsigned long last_configure_serial; }; enum -- cgit v1.3 From 4de6b187933479ce93b6079f42a485e5868f01a5 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 24 Jan 2023 10:34:40 +0800 Subject: Update Android port * .gitignore: Update with new files. Do not ignore std*.in.h. * INSTALL.android: Explain how to build Emacs with external dependencies. * Makefile.in (xcompile, cross): Rename to `cross'. (clean_dirs): Clean cross, not xcompile. * README: Document new directories. * build-aux/ndk-build-helper-1.mk (build_kind, NDK_SO_NAMES): * build-aux/ndk-build-helper-2.mk (build_kind, NDK_SO_NAMES): * build-aux/ndk-build-helper-3.mk (build_kind): * build-aux/ndk-build-helper-4.mk: * build-aux/ndk-build-helper.mk (NDK_BUILD_DIR, my-dir): * build-aux/ndk-module-extract.awk: New files. * configure.ac: Set up libgif, libwebp, and libpng for ndk-build. * cross/ndk-build/Makefile.in (srcdir, NDK_BUILD_ANDROID_MK): * cross/ndk-build/ndk-build-executable.mk: * cross/ndk-build/ndk-build-shared-library.mk (eq, objname): * cross/ndk-build/ndk-build-static-library.mk (eq, objname): * cross/ndk-build/ndk-build.in (NDK_BUILD_MODULES): * cross/ndk-build/ndk-build.mk.in (NDK_BUILD_MODULES) (NDK_BUILD_SHARED): * cross/ndk-build/ndk-clear-vars.mk: * cross/ndk-build/ndk-prebuilt-shared-library.mk: * cross/ndk-build/ndk-prebuilt-static-library.mk: New files. * doc/emacs/android.texi (Android, Android Environment): Document clipboard support on Android. * doc/emacs/emacs.texi (Top): Update menus. * etc/MACHINES: Document Android. * java/AndroidManifest.xml.in: Respect new `--with-android-debug' option. * java/Makefile.in (CROSS_BINS, CROSS_LIBS): Adjust for rename. Include ndk-build.mk.:(emacs.apk-in): Depend on shared libraries. Then, package shared libraries. * java/org/gnu/emacs/EmacsClipboard.java (EmacsClipboard): New class. * java/org/gnu/emacs/EmacsFontDriver.java: Update comment to say this is unused. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function `sendExpose'. * java/org/gnu/emacs/EmacsSdk11Clipboard.java (EmacsSdk11Clipboard): * java/org/gnu/emacs/EmacsSdk8Clipboard.java (EmacsSdk8Clipboard): New classes. * java/org/gnu/emacs/EmacsView.java (EmacsView, handleDirtyBitmap) (onDetachedFromWindow): When window is reattached, expose the frame. * lib/Makefile.in (VPATH): (ALL_CFLAGS): Adjust for rename. * lisp/term/android-win.el (android-clipboard-exists-p) (android-get-clipboard, android-set-clipboard) (android-clipboard-owner-p, android-primary-selection) (android-get-clipboard-1, android-get-primary) (android-selection-bounds, android-encode-select-string) (gui-backend-get-selection, gui-backend-selection-exists-p) (gui-backend-selection-owner-p, gui-backend-set-selection): New functions. * m4/ndk-build.m4: New file. * src/Makefile.in (GIF_CFLAGS, ANDROID_LDFLAGS): New variables. (EMACS_CFLAGS): Add GIF_CFLAGS. Include ndk-build.mk. (libemacs.so): Depend on and link with required libraries. * src/android.c (android_check_compressed_file): New function. (android_open): Work around Android platform bug. (sendExpose): New function. (android_readdir): Set d_type if this is a directory. * src/androidgui.h (enum android_event_type) (struct android_expose_event, union android_event): Add expose events. * src/androidselect.c (struct android_emacs_clipboard) (android_init_emacs_clipboard, Fandroid_clipboard_owner_p) (Fandroid_set_clipboard, Fandroid_get_clipboard) (Fandroid_clipboard_exists_p, init_androidselect) (syms_of_androidselect): New file. * src/androidterm.c (handle_one_android_event): Handle exposures. * src/androidterm.h: Update prototypes. * src/emacs.c (android_emacs_init): Initialize androidselect. --- .gitignore | 64 ++--- INSTALL.android | 334 ++++++++++++++++++++++++- Makefile.in | 4 +- README | 3 + build-aux/ndk-build-helper-1.mk | 92 +++++++ build-aux/ndk-build-helper-2.mk | 87 +++++++ build-aux/ndk-build-helper-3.mk | 28 +++ build-aux/ndk-build-helper-4.mk | 38 +++ build-aux/ndk-build-helper.mk | 52 ++++ build-aux/ndk-module-extract.awk | 64 +++++ configure.ac | 90 +++++-- cross/ndk-build/Makefile.in | 115 +++++++++ cross/ndk-build/ndk-build-executable.mk | 22 ++ cross/ndk-build/ndk-build-shared-library.mk | 83 ++++++ cross/ndk-build/ndk-build-static-library.mk | 83 ++++++ cross/ndk-build/ndk-build.in | 44 ++++ cross/ndk-build/ndk-build.mk.in | 43 ++++ cross/ndk-build/ndk-clear-vars.mk | 47 ++++ cross/ndk-build/ndk-prebuilt-shared-library.mk | 24 ++ cross/ndk-build/ndk-prebuilt-static-library.mk | 24 ++ doc/emacs/android.texi | 29 ++- doc/emacs/emacs.texi | 1 + etc/MACHINES | 8 + java/AndroidManifest.xml.in | 5 +- java/Makefile.in | 30 ++- java/org/gnu/emacs/EmacsClipboard.java | 44 ++++ java/org/gnu/emacs/EmacsFontDriver.java | 3 + java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsSdk11Clipboard.java | 156 ++++++++++++ java/org/gnu/emacs/EmacsSdk8Clipboard.java | 116 +++++++++ java/org/gnu/emacs/EmacsView.java | 23 ++ lib/Makefile.in | 4 +- lisp/term/android-win.el | 106 ++++++++ m4/ndk-build.m4 | 213 ++++++++++++++++ src/Makefile.in | 19 +- src/android.c | 82 +++++- src/androidgui.h | 11 + src/androidselect.c | 249 ++++++++++++++++++ src/androidterm.c | 22 ++ src/androidterm.h | 9 + src/emacs.c | 2 + 41 files changed, 2406 insertions(+), 71 deletions(-) create mode 100644 build-aux/ndk-build-helper-1.mk create mode 100644 build-aux/ndk-build-helper-2.mk create mode 100644 build-aux/ndk-build-helper-3.mk create mode 100644 build-aux/ndk-build-helper-4.mk create mode 100644 build-aux/ndk-build-helper.mk create mode 100644 build-aux/ndk-module-extract.awk create mode 100644 cross/ndk-build/Makefile.in create mode 100644 cross/ndk-build/ndk-build-executable.mk create mode 100644 cross/ndk-build/ndk-build-shared-library.mk create mode 100644 cross/ndk-build/ndk-build-static-library.mk create mode 100644 cross/ndk-build/ndk-build.in create mode 100644 cross/ndk-build/ndk-build.mk.in create mode 100644 cross/ndk-build/ndk-clear-vars.mk create mode 100644 cross/ndk-build/ndk-prebuilt-shared-library.mk create mode 100644 cross/ndk-build/ndk-prebuilt-static-library.mk create mode 100644 java/org/gnu/emacs/EmacsClipboard.java create mode 100644 java/org/gnu/emacs/EmacsSdk11Clipboard.java create mode 100644 java/org/gnu/emacs/EmacsSdk8Clipboard.java create mode 100644 m4/ndk-build.m4 create mode 100644 src/androidselect.c (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/.gitignore b/.gitignore index 6494e4e8f39..6b624767c66 100644 --- a/.gitignore +++ b/.gitignore @@ -96,36 +96,40 @@ src/lisp.mk src/verbose.mk # Stuff built during cross compilation -xcompile/lib/alloca.h -xcompile/lib/assert.h -xcompile/lib/byteswap.h -xcompile/lib/dirent.h -xcompile/lib/errno.h -xcompile/lib/execinfo.h -xcompile/lib/fcntl.h -xcompile/lib/getopt.h -xcompile/lib/getopt-cdefs.h -xcompile/lib/gmp.h -xcompile/lib/ieee754.h -xcompile/lib/inttypes.h -xcompile/lib/libgnu.a -xcompile/lib/limits.h -xcompile/lib/malloc/*.gl.h -xcompile/lib/signal.h -xcompile/lib/std*.h -xcompile/!lib/std*.in.h -xcompile/!lib/stdio-impl.h -xcompile/lib/string.h -xcompile/lib/sys/ -xcompile/lib/time.h -xcompile/lib/unistd.h -xcompile/lib/config.h -xcompile/lib/gnulib.mk -xcompile/src/* -xcompile/lib-src/* -xcompile/sys/* -xcompile/config.status -xcompile/*.bak +cross/lib/alloca.h +cross/lib/assert.h +cross/lib/byteswap.h +cross/lib/dirent.h +cross/lib/errno.h +cross/lib/execinfo.h +cross/lib/fcntl.h +cross/lib/getopt.h +cross/lib/getopt-cdefs.h +cross/lib/gmp.h +cross/lib/ieee754.h +cross/lib/inttypes.h +cross/lib/libgnu.a +cross/lib/limits.h +cross/lib/malloc/*.gl.h +cross/lib/signal.h +cross/lib/std*.h +!cross/lib/std*.in.h +!cross/lib/stdio-impl.h +cross/lib/string.h +cross/lib/sys/ +cross/lib/time.h +cross/lib/unistd.h +cross/lib/config.h +cross/lib/gnulib.mk +cross/src/* +cross/lib-src/* +cross/sys/* +cross/config.status +cross/*.bak + +cross/ndk-build/Makefile +cross/ndk-build/ndk-build.mk +cross/ndk-build/*.o # Lisp-level sources built by 'make'. *cus-load.el diff --git a/INSTALL.android b/INSTALL.android index 63c856fe56c..9abea74a55d 100644 --- a/INSTALL.android +++ b/INSTALL.android @@ -62,7 +62,7 @@ medium) and installed on-device. BUILDING WITH OLD NDK VERSIONS Building Emacs with an old version of the Android NDK requires special -setup. This is because there is no separate C compiler binary for +setup. This is because there is no separate C compiler binary for each version of Android in those versions of the NDK. Before running `configure', you must identify three variables: @@ -94,7 +94,339 @@ Emacs is known to build for Android 2.2 (API version 8) or later, and run on Android 2.3 or later. It is supposed to run on Android 2.2 as well. +DEBUG AND RELEASE BUILDS + +Android makes a distinction between ``debug'' and ``release'' builds +of applications. With ``release'' builds, the system will apply +stronger optimizations to the application at the cost of being unable +to debug them with the steps in etc/DEBUG. + +Emacs is built as a debuggable package by default, but: + + ./configure --without-android-debug + +will create a release build of Emacs instead. This may be useful when +running Emacs on resource constrained machines. + +If you are building an Emacs package for redistribution, we urge you +to provide both debug and release versions. + +BUILDING WITH THIRD PARTY LIBRARIES + +The Android NDK does not support the usual ways of locating third +party libraries, especially not via `pkg-config'. Instead, it uses +its own system called `ndk-build'. The one exception to this rule is +zlib, which is considered a part of the Android OS itself and is +available on all devices running Android. + +Android also requires that each application include its own +dependencies, as the system makes no guarantee about the existence of +any particular library. + +Emacs is not built with the `ndk-build' system. Instead, it is built +with Autoconf and Make. + +However, it supports building and including dependencies which use the +similarly Make-based `ndk-build' system. + +To use dependencies built through `ndk-build', you must specify a list +of directories within which Emacs will search for ``Android.mk'' +files, like so: + + ./configure "--with-ndk-path=directory1 directory2" + +Emacs will then read the ``Android.mk'' file in each directory, and +automatically build and use those modules. + +Google, Inc. has adapted many common Emacs dependencies to use the +`ndk-build' system. Here is a non-exhaustive list of what is known to +work: + + libpng - https://android.googlesource.com/platform/external/libpng + libwebp - https://android.googlesource.com/platform/external/webp + giflib - https://android.googlesource.com/platform/external/giflib + (You must add LOCAL_EXPORT_CFLAGS := -I$(LOCAL_PATH) before + its Android.mk includes $(BUILD_STATIC_LIBRARY)) + +We anticipate that most untested non-trivial ndk-build dependencies +will need adjustments in Emacs to work, as the Emacs build system +which emulates ndk-build is in an extremely early state. + +NDK BUILD SYSTEM IMPLEMENTATION + +Emacs implements ndk-build itself, because the version that comes with +the Android NDK is not easy to use from another Makefile, and keeps +accumulating incompatible changes. + +The Emacs implementation of ndk-build consists of one m4 file: + + m4/ndk-build.m4 + +four Makefiles in build-aux, run during configure: + + build-aux/ndk-build-helper-1.mk + build-aux/ndk-build-helper-2.mk + build-aux/ndk-build-helper-3.mk + build-aux/ndk-build-helper.mk + +one awk script in build-awx, run during configure: + + build-aux/ndk-module-extract.awk + +six Makefiles in cross/ndk-build, + + cross/ndk-build/ndk-build-shared-library.mk + cross/ndk-build/ndk-build-static-library.mk + cross/ndk-build/ndk-build-executable.mk + cross/ndk-build/ndk-clear-vars.mk + cross/ndk-build/ndk-prebuilt-shared-library.mk + cross/ndk-build/ndk-prebuilt-static-library.mk + +and finally, two more Makefiles in cross/ndk-build, generated by +configure: + + cross/ndk-build/Makefile (generated from cross/ndk-build/Makefile.in) + cross/ndk-build/ndk-build.mk (generated from cross/ndk-build/ndk-build.mk.in) + +m4/ndk-build.m4 is a collection of macros which are used by the +configure script to set up the ndk-build system, look for modules, add +the appropriate options to LIBS and CFLAGS, and generate the Makefiles +necessary to build the rest of Emacs. + +Immediately after determining the list of directories in which to look +for ``Android.mk'' files, the version and type of Android system being +built for, configure calls: + + ndk_INIT([$android_abi], [$ANDROID_SDK], [cross/ndk-build]) + +This expands to a sequence of shell script that enumerates all of the +Android.mk files specified in "$with_ndk_path", sets up some shell +functions used by the rest of the ndk-build code run by the configure +script, and teaches the ndk-build system that the Makefiles to be +generated are found in the directory "cross/ndk-build/Makefile". + +When configure is cross-compiling for Android, the macro +EMACS_CHECK_MODULES will expand to the macro ndk_CHECK_MODULES, +instead of pkg-config.m4's PKG_CHECK_MODULES. Thus, the following +code: + + EMACS_CHECK_MODULES([PNG], [libpng >= 1.0.0]) + +will actually expand to: + + ndk_CHECK_MODULES([PNG], [libpng >= 1.0.0], [HAVE_PNG=yes], + [HAVE_PNG=no]) + +which in turn expands to a sequence shell script that first invokes: + + make -f build-aux/ndk-build-helper.mk + +for each ``Android.mk'' file found by ndk_INIT, with the following +variables given to Make: + + EMACS_SRCDIR=. # the source directory (in which configure is running) + EMACS_ABI=$ndk_ABI # this is the $android_abi given to ndk_INIT + ANDROID_MAKEFILE="/opt/android/libpng/Android.mk" + ANDROID_MODULE_DIRECTORY="/opt/android/libpng" + NDK_BUILD_DIR="$ndk_DIR" # this is the directory given as to ndk_INIT + +build-aux/ndk-build-helper.mk will then evaluate the contents +$(ANDROID_MAKEFILE), the ``Android.mk'' file, for the first time. The +purpose of this evaluation is to establish a list of packages (or +modules) provided by the ``Android.mk'' file, and the corresponding +Makefile targets and compiler and linker flags required to build and +link to those tagets. + +Before doing so, build-aux/ndk-build-helper.mk will define several +variables and functions required by all ``Android.mk'' files. The +most important of these are: + + my-dir # the directory containing the Android.mk file. + BUILD_SHARED_LIBRARY # build-aux/ndk-build-helper-1.mk + BUILD_STATIC_LIBRARY # build-aux/ndk-build-helper-2.mk + BUILD_EXECUTABLE # build-aux/ndk-build-helper-3.mk + CLEAR_VARS # build-aux/ndk-build-helper-4.mk + +Then, ``Android.mk'' will include $(CLEAN_VARS) (to clear variables +previously set), set several variables describing each module to the +ndk-build system, and include one of $(BUILD_SHARED_LIBRARY), +$(BUILD_STATIC_LIBRARY) and $(BUILD_EXECUTABLE). + +Each one of those three scripts will then read from the variables set +by ``Android.mk'', resolve dependencies, and print out some text +describing the module to Emacs. For example, the shared library +module "libpng" results in the following text being printed: + +Building shared +libpng +/opt/android/libpng/png.c /opt/android/libpng/pngerror.c /opt/android/libpng/pngget.c /opt/android/libpng/pngmem.c /opt/android/libpng/pngpread.c /opt/android/libpng/pngread.c /opt/android/libpng/pngrio.c /opt/android/libpng/pngrtran.c /opt/android/libpng/pngrutil.c /opt/android/libpng/pngset.c /opt/android/libpng/pngtrans.c /opt/android/libpng/pngwio.c /opt/android/libpng/pngwrite.c /opt/android/libpng/pngwtran.c /opt/android/libpng/pngwutil.c +-I/opt/android/libpng + + -L/opt/emacs/cross/ndk-build -l:libpng_emacs.so +libpng_emacs.so +End + +The output is arranged as follows: + + - The first line consists of the word ``Building'', followed by + either ``shared'', ``static'', or ``executable'', depending on + what type of module being built. + + - The second line consists of the name of the module currently being + built. + + - The third line consists of all of the source code files comprising + the module. + + - The fourth line consists of the text that has to be added to + CFLAGS in order to find the includes associated with the module. + + - The fifth line consists of the text that has to be added to LIBS + in order to link with this module and all of its dependencies. + + - The sixth line consists of the Make targets (more on this later) + that will build the final shared object or library archive of this + module, along with all of its dependencies. + +The output from Make is given to an awk script, +build-aux/ndk-module-extract.awk. This is responsible for parsing the +that output and filtering out modules other than what is being built: + + awk -f build-aux/ndk-module-extract.awk MODULE=libpng + +eventually generating this section of shell script: + +module_name=libpng +module_kind=shared +module_src="/opt/android/libpng/png.c /opt/android/libpng/pngerror.c /opt/android/libpng/pngget.c /opt/android/libpng/pngmem.c /opt/android/libpng/pngpread.c /opt/android/libpng/pngread.c /opt/android/libpng/pngrio.c /opt/android/libpng/pngrtran.c /opt/android/libpng/pngrutil.c /opt/android/libpng/pngset.c /opt/android/libpng/pngtrans.c /opt/android/libpng/pngwio.c /opt/android/libpng/pngwrite.c /opt/android/libpng/pngwtran.c /opt/android/libpng/pngwutil.c" +module_includes="-I/opt/android/libpng" +module_cflags="" +module_ldflags=" -L/opt/emacs/cross/ndk-build -l:libpng_emacs.so" +module_target="libpng_emacs.so" + +which is then evaluated by `configure'. Once the variable +`module_name' is set, configure apends the remaining +$(module_includes), $(module_cflags) and $(module_ldflags) to the +module's CFLAGS and LIBS variables, and appends the list of Makefile +targets specified to the variable NDK_BUILD_MODULES. + +Finally, immediately before generating src/Makefile.android, configure +expands: + + ndk_CONFIG_FILES + +to generate $ndk_DIR/Makefile and $ndk_DIR/ndk-build.mk. + +Now, the $ndk_DIR directory is set up to build all modules upon which +depends, and $ndk_DIR/ndk-build.mk includes a list of files required +to link Emacs, along with the rules to chdir into $ndk_DIR in order to +build them. + +$ndk_DIR/ndk-build.mk is included by cross/src/Makefile +(Makefile.android) and java/Makefile. It defines three different +variables: + + NDK_BUILD_MODULES the file names of all modules to be built. + NDK_BUILD_STATIC absolute names of all library archives + to be built. + NDK_BUILD_SHARED absolute names of all shared libraries to + be built. + +and then proceeds to define rules to build each of the modules in +$(NDK_BUILD_MODULES). + +cross/src/Makefile arranges to have all dependencies of Emacs not +already built built before linking ``libemacs.so'' with them. + +java/Makefile additionally arranges to have all shared object +dependencies built before the application package is built, which is +normally redundant because they should have already been built before +linking ``libemacs.so''. + +Building the modules is performed through $ndk_DIR/Makefile, which +contains the actual implementation of the ``ndk-build'' build system. +First, it defines certain variables constant within the ``ndk-build'' +build system, such as the files included by ``Android.mk'' to build +shared or static libraries, and CLEAR_VARS. The most important of +these are: + + CLEAR_VARS cross/ndk-build/ndk-clear-vars.mk + BUILD_EXECUTABLE cross/ndk-build/ndk-build-executable.mk + BUILD_SHARED_LIBRARY cross/ndk-build/ndk-build-shared-library.mk + BUILD_STATIC_LIBRARY cross/ndk-build/ndk-build-static-library.mk + PREBUILT_SHARED_LIBRARY cross/ndk-build/ndk-prebuilt-shared-library.mk + PREBUILT_STATIC_LIBRARY cross/ndk-build/ndk-prebuilt-static-library.mk + +Then, it loads each Emacs dependency's ``Android.mk'' file. For each +module defined there, ``Android.mk'' includes $(CLEAR_VARS) to unset +all variables specific to each module, and then includes +$(BUILD_SHARED_LIBRARY) or $(BUILD_STATIC_LIBRARY) for each shared or +static library module. + +This results in cross/ndk-build/ndk-build-shared-library.mk or +cross/ndk-build/ndk-build-static-library being included, just like the +Makefiles in build-aux were inside the configure script. + +Each one of those two scripts then defines rules to build all of the +object files associated with the module, and then link or archive +them. The name under which the module is linked is the same as the +Make target found on the sixth line of output from +build-aux/ndk-build-helper.mk. + +However, none of the Makefiles in +cross/ndk-build/ndk-build-shared-library.mk perform any kind of +dependency resolution! Instead, they only define rules to build +individual modules, leaving dependency resolution up to the Makefiles +in the `build-aux' directory. + +libpng is a very simple module, providing only a single shared object +module. This module is named libpng_emacs.so and is eventually built +and packaged into the library directory of the Emacs application +package. Now, let us look at a more complex module, libwebp: + + +When built with libwebp, Emacs depends on a single library, +libwebpdemux. This library is named ``libwebpdemux'' on Unix systems, +and that is the name by which it is found with pkg-config. + +However, the library's module is only named ``webpdemux'' on Android. +When ndk_CHECK_MODULES begins to look for a module, it first tries to +see if its name is found in the variable `ndk_package_map', which was +set inside ndk_INIT. In this case, it finds the following word: + + libwebpdemux:webpdemux + +and immediately replaces ``libwebpdemux'' with ``webpdemux''. + +Then, it locates the ``Android.mk'' file containing a static library +module named webpdemux and gives the output from +build-aux/ndk-build-helper.mk to the awk script, resulting in: + +module_name=webpdemux +module_kind=static +module_src="/opt/android/webp/src/demux/anim_decode.c /opt/android/webp/src/demux/demux.c" +module_includes="-I/opt/android/webp/src" +module_cflags="" +module_ldflags=" cross/ndk-build/libwebpdemux.a cross/ndk-build/libwebp.a cross/ndk-build/libwebpdecoder_static.a " +module_target="libwebpdemux.a libwebp.a libwebpdecoder_static.a" + +The attentive reader will notice that in addition to the +``libwebpdemux.a'' archive associated with the ``webpdemux'' library, +Emacs has been made to link with two additional libraries. This is +because the ``webpdemux'' module specifies a dependency on the +``webp'' module (defined in the same Android.mk). +build-aux/ndk-build-helper.mk resolved that dependency, noticing that +it in turn specified another dependency on ``webpdecoder_static'', +which in turn was added to the linker command line and list of targets +to build. + +As a result, all three dependencies will be built and linked to Emacs, +instead of just the single ``webpdemux'' dependency that was +specified. + + + This file is part of GNU Emacs. GNU Emacs is free software: you can redistribute it and/or modify diff --git a/Makefile.in b/Makefile.in index 0090b858228..c4b346128dc 100644 --- a/Makefile.in +++ b/Makefile.in @@ -539,7 +539,7 @@ lib lib-src lisp nt: Makefile java: lisp info $(MAKE) -C $@ all -xcompile: src +cross: src $(MAKE) -C $@ all trampolines: src lisp @@ -1012,7 +1012,7 @@ mostlyclean: $(mostlyclean_dirs:=_mostlyclean) ### with them. ### ### Delete '.dvi' files here if they are not part of the distribution. -clean_dirs = $(mostlyclean_dirs) java xcompile nextstep admin/charsets \ +clean_dirs = $(mostlyclean_dirs) java cross nextstep admin/charsets \ admin/unidata $(foreach dir,$(clean_dirs),$(eval $(call submake_template,$(dir),clean))) diff --git a/README b/README index 19d5c96e348..c8a3d485c28 100644 --- a/README +++ b/README @@ -95,6 +95,9 @@ There are several subdirectories: 'admin' holds files used by Emacs developers, and Unicode data files. 'build-aux' holds auxiliary files used during the build. 'm4' holds Autoconf macros used for generating the configure script. +'java' holds the Java code for the Emacs port to Android. +'cross' holds Makefiles and an additional copy of gnulib used to build + Emacs for Android devices. Building Emacs on non-Posix platforms requires tools that aren't part of the standard distribution of the OS. The platform-specific README diff --git a/build-aux/ndk-build-helper-1.mk b/build-aux/ndk-build-helper-1.mk new file mode 100644 index 00000000000..fb8e4da2f0a --- /dev/null +++ b/build-aux/ndk-build-helper-1.mk @@ -0,0 +1,92 @@ +# ndk-build-helper-1.mk -- Helper for ndk-build.m4. +# Copyright (C) 2023 Free Software Foundation, Inc. +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# Print out information now defined. Important details include: +# - list of source files to compile. +# - module export include directories. +# - module export CFLAGS. +# - module export LDFLAGS. +# - module name. + +build_kind = shared +NDK_SO_NAMES = +NDK_A_NAMES = + +# Record this module's dependencies. This information is used later +# on to recurse over libraries. +NDK_$(LOCAL_MODULE)_STATIC_LIBRARIES := $(LOCAL_STATIC_LIBRARIES) $(LOCAL_WHOLE_STATIC_LIBRARIES) +NDK_$(LOCAL_MODULE)_SHARED_LIBRARIES := $(LOCAL_SHARED_LIBRARIES) + +$(info Building $(build_kind)) +$(info $(LOCAL_MODULE)) +$(info $(addprefix $(ANDROID_MODULE_DIRECTORY)?,$(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES$(EMACS_ABI)))) + +$(info $(foreach dir,$(LOCAL_EXPORT_C_INCLUDE_DIRS) $(LOCAL_EXPORT_C_INCLUDES),-I$(dir))) +$(info $(LOCAL_EXPORT_CFLAGS)) +ifeq ($(LOCAL_MODULE_FILENAME),) +ifeq ($(findstring lib,$(LOCAL_MODULE)),lib) +NDK_SO_NAMES = $(LOCAL_MODULE)_emacs.so +else +NDK_SO_NAMES = lib$(LOCAL_MODULE)_emacs.so +endif +else +NDK_SO_NAMES = $(LOCAL_MODULE_FILENAME).so +endif + +define add-a-name +ifeq ($(findstring lib,$(1)),lib) +NDK_A_NAME = $(1).a +else +NDK_A_NAME = lib$(1).a +endif + +ifeq ($$(NDK_A_NAMES:$$(NDK_A_NAME)=),$$(NDK_A_NAMES)) +NDK_A_NAMES := $$(NDK_A_NAMES) $$(NDK_A_NAME) + +# Now recurse over this module's dependencies. +$$(foreach module,$$(filter-out $$(SYSTEM_LIBRARIES), $$(NDK_$(1)_STATIC_LIBRARIES)),$$(eval $$(call add-a-name,$$(module)))) +$$(foreach module,$$(filter-out $$(SYSTEM_LIBRARIES), $$(NDK_$(1)_SHARED_LIBRARIES)),$$(eval $$(call add-so-name,$$(module)))) +endif +endef + +define add-so-name +ifeq ($(findstring lib,$(1)),lib) +NDK_SO_NAME = $(1)_emacs.so +else +NDK_SO_NAME = lib$(1)_emacs.so +endif + +ifeq ($$(NDK_SO_NAMES:$$(NDK_SO_NAME)=),$$(NDK_SO_NAMES)) +NDK_SO_NAMES := $$(NDK_SO_NAMES) $$(NDK_SO_NAME) + +# Now recurse over this module's dependencies. +$$(foreach module,$$(filter-out $$(SYSTEM_LIBRARIES), $$(NDK_$(1)_STATIC_LIBRARIES)),$$(eval $$(call add-a-name,$$(module)))) +$$(foreach module,$$(filter-out $$(SYSTEM_LIBRARIES), $$(NDK_$(1)_SHARED_LIBRARIES)),$$(eval $$(call add-so-name,$$(module)))) +endif +endef + +# Resolve additional dependencies based on LOCAL_STATIC_LIBRARIES and +# LOCAL_SHARED_LIBRARIES. + +SYSTEM_LIBRARIES = z libz libc c + +$(foreach module,$(filter-out $(SYSTEM_LIBRARIES), $(LOCAL_STATIC_LIBRARIES)),$(eval $(call add-a-name,$(module)))) +$(foreach module,$(filter-out $(SYSTEM_LIBRARIES), $(LOCAL_SHARED_LIBRARIES)),$(eval $(call add-so-name,$(module)))) + +$(info $(LOCAL_EXPORT_LDFLAGS) $(abspath $(addprefix $(NDK_BUILD_DIR)/,$(NDK_A_NAMES))) -L$(abspath $(NDK_BUILD_DIR)) $(foreach soname,$(NDK_SO_NAMES),-l:$(soname))) +$(info $(NDK_SO_NAMES)) +$(info End) diff --git a/build-aux/ndk-build-helper-2.mk b/build-aux/ndk-build-helper-2.mk new file mode 100644 index 00000000000..82c1d53c7d0 --- /dev/null +++ b/build-aux/ndk-build-helper-2.mk @@ -0,0 +1,87 @@ +# ndk-build-helper-2.mk -- Helper for ndk-build.m4. +# Copyright (C) 2023 Free Software Foundation, Inc. +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# Say a static library is being built +build_kind = static +NDK_SO_NAMES = +NDK_A_NAMES = + +# Record this module's dependencies. This information is used later +# on to recurse over libraries. +NDK_$(LOCAL_MODULE)_STATIC_LIBRARIES := $(LOCAL_STATIC_LIBRARIES) $(LOCAL_WHOLE_STATIC_LIBRARIES) +NDK_$(LOCAL_MODULE)_SHARED_LIBRARIES := $(LOCAL_SHARED_LIBRARIES) + +$(info Building $(build_kind)) +$(info $(LOCAL_MODULE)) +$(info $(addprefix $(ANDROID_MODULE_DIRECTORY)/,$(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES$(EMACS_ABI)))) + +$(info $(foreach dir,$(LOCAL_EXPORT_C_INCLUDE_DIRS) $(LOCAL_EXPORT_C_INCLUDES),-I$(dir))) +$(info $(LOCAL_EXPORT_CFLAGS)) +ifeq ($(LOCAL_MODULE_FILENAME),) + +ifeq ($(findstring lib,$(LOCAL_MODULE)),lib) +NDK_A_NAMES = $(LOCAL_MODULE).a +else +NDK_A_NAMES = lib$(LOCAL_MODULE).a +endif +else +NDK_A_NAMES = $(LOCAL_MODULE_FILENAME).a +endif + +define add-a-name +ifeq ($(findstring lib,$(1)),lib) +NDK_A_NAME = $(1).a +else +NDK_A_NAME = lib$(1).a +endif + +ifeq ($$(NDK_A_NAMES:$$(NDK_A_NAME)=),$$(NDK_A_NAMES)) +NDK_A_NAMES := $$(NDK_A_NAMES) $$(NDK_A_NAME) + +# Now recurse over this module's dependencies. +$$(foreach module,$$(filter-out $$(SYSTEM_LIBRARIES), $$(NDK_$(1)_STATIC_LIBRARIES)),$$(eval $$(call add-a-name,$$(module)))) +$$(foreach module,$$(filter-out $$(SYSTEM_LIBRARIES), $$(NDK_$(1)_SHARED_LIBRARIES)),$$(eval $$(call add-so-name,$$(module)))) +endif +endef + +define add-so-name +ifeq ($(findstring lib,$(1)),lib) +NDK_SO_NAME = $(1)_emacs.so +else +NDK_SO_NAME = lib$(1)_emacs.so +endif + +ifeq ($$(NDK_SO_NAMES:$$(NDK_SO_NAME)=),$$(NDK_SO_NAMES)) +NDK_SO_NAMES := $$(NDK_SO_NAMES) $$(NDK_SO_NAME) + +# Now recurse over this module's dependencies. +$$(foreach module,$$(filter-out $$(SYSTEM_LIBRARIES), $$(NDK_$(1)_STATIC_LIBRARIES)),$$(eval $$(call add-a-name,$$(module)))) +$$(foreach module,$$(filter-out $$(SYSTEM_LIBRARIES), $$(NDK_$(1)_SHARED_LIBRARIES)),$$(eval $$(call add-so-name,$$(module)))) +endif +endef + +# Resolve additional dependencies based on LOCAL_STATIC_LIBRARIES and +# LOCAL_SHARED_LIBRARIES. + +SYSTEM_LIBRARIES = z libz + +$(foreach module,$(filter-out $(SYSTEM_LIBRARIES), $(LOCAL_STATIC_LIBRARIES)),$(eval $(call add-a-name,$(module)))) +$(foreach module,$(filter-out $(SYSTEM_LIBRARIES), $(LOCAL_SHARED_LIBRARIES)),$(eval $(call add-so-name,$(module)))) + +$(info $(LOCAL_EXPORT_LDFLAGS) $(abspath $(addprefix $(NDK_BUILD_DIR)/,$(NDK_A_NAMES))) $(and $(NDK_SO_NAMES), -L$(abspath $(NDK_BUILD_DIR)) $(foreach soname,$(NDK_SO_NAMES),-l:$(soname)))) +$(info $(NDK_A_NAMES)) +$(info End) diff --git a/build-aux/ndk-build-helper-3.mk b/build-aux/ndk-build-helper-3.mk new file mode 100644 index 00000000000..4d0358d4f77 --- /dev/null +++ b/build-aux/ndk-build-helper-3.mk @@ -0,0 +1,28 @@ +# ndk-build-helper-3.mk -- Helper for ndk-build.m4. +# Copyright (C) 2023 Free Software Foundation, Inc. +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# Say a static library is being built +build_kind = executable + +$(info Building $(build_kind)) +$(info $(LOCAL_MODULE)) +$(info $(addprefix $(ANDROID_MODULE_DIRECTORY),$(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES$(EMACS_ABI)))) + +$(info $(foreach dir,$(LOCAL_EXPORT_C_INCLUDE_DIRS) $(LOCAL_EXPORT_C_INCLUDES),-I$(dir))) +$(info $(LOCAL_EXPORT_CFLAGS)) +$(info $(LOCAL_EXPORT_LDFLAGS)) +$(info End) diff --git a/build-aux/ndk-build-helper-4.mk b/build-aux/ndk-build-helper-4.mk new file mode 100644 index 00000000000..e1024f0a243 --- /dev/null +++ b/build-aux/ndk-build-helper-4.mk @@ -0,0 +1,38 @@ +# Copyright (C) 2023 Free Software Foundation, Inc. +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +undefine LOCAL_MODULE +undefine LOCAL_MODULE_FILENAME +undefine LOCAL_SRC_FILES +undefine LOCAL_CPP_EXTENSION +undefine LOCAL_CPP_FEATURES +undefine LOCAL_C_INCLUDES +undefine LOCAL_CFLAGS +undefine LOCAL_CPPFLAGS +undefine LOCAL_STATIC_LIBRARIES +undefine LOCAL_SHARED_LIBRARIES +undefine LOCAL_WHOLE_STATIC_LIBRARIES +undefine LOCAL_LDLIBS +undefine LOCAL_LDFLAGS +undefine LOCAL_ALLOW_UNDEFINED_SYMBOLS +undefine LOCAL_ARM_MODE +undefine LOCAL_ARM_NEON +undefine LOCAL_DISABLE_FORMAT_STRING_CHECKS +undefine LOCAL_EXPORT_CFLAGS +undefine LOCAL_EXPORT_CPPFLAGS +undefine LOCAL_EXPORT_C_INCLUDES +undefine LOCAL_EXPORT_LDFLAGS +undefine LOCAL_EXPORT_LDLIBS diff --git a/build-aux/ndk-build-helper.mk b/build-aux/ndk-build-helper.mk new file mode 100644 index 00000000000..beb32cae3f1 --- /dev/null +++ b/build-aux/ndk-build-helper.mk @@ -0,0 +1,52 @@ +# ndk-build-helper.mk -- Helper for ndk-build.m4. +# Copyright (C) 2023 Free Software Foundation, Inc. +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# This Makefile sets up enough to parse an Android-style Android.mk +# file and return useful information about its contents. + +# See the text under ``NDK BUILD SYSTEM IMPLEMENTATION'' in +# INSTALL.android for more details. + +# Make NDK_BUILD_DIR absolute. +NDK_BUILD_DIR := $(absname $(NDK_BUILD_DIR)) + +# my-dir is a function that returns the Android module directory. +my-dir = $(ANDROID_MODULE_DIRECTORY) + +# all-subdir-makefiles is a function which returns all Android.mk +# files within this directory. +all-subdir-makefiles = $(shell find . -name "Android.mk") + +# These functions are not implemented. +parent-makefile = +grand-parent-makefile = +import-module = + +# Print out module information every time BUILD_SHARED_LIBRARY is +# called. + +BUILD_SHARED_LIBRARY=$(EMACS_SRCDIR)/build-aux/ndk-build-helper-1.mk +BUILD_STATIC_LIBRARY=$(EMACS_SRCDIR)/build-aux/ndk-build-helper-2.mk +BUILD_EXECUTABLE=$(EMACS_SRCDIR)/build-aux/ndk-build-helper-3.mk +CLEAR_VARS=$(EMACS_SRCDIR)/build-aux/ndk-build-helper-4.mk + +# Now include Android.mk. + +include $(ANDROID_MAKEFILE) + +# Dummy target. +all: diff --git a/build-aux/ndk-module-extract.awk b/build-aux/ndk-module-extract.awk new file mode 100644 index 00000000000..eaea3e96dd8 --- /dev/null +++ b/build-aux/ndk-module-extract.awk @@ -0,0 +1,64 @@ +/^Building.+$/ { + kind = $2 +} + +// { + if (!match ($0, /^End$/) && !match ($0, /^Building.+$/)) + { + if (kind) + { + if (ldflags_found) + target = $0 + else if (cflags_found) + { + ldflags = $0 + ldflags_found = 1 + } + else if (includes_found) + { + cflags = $0 + cflags_found = 1 + } + else if (src_found) + { + includes = $0 + includes_found = 1 + } + else if (name_found) + { + src = $0 + src_found = 1; + } + else + { + name = $0 + name_found = 1 + } + } + } +} + +/^End$/ { + if (name == MODULE && (kind == "shared" || kind == "static")) + { + printf "module_name=%s\n", name + printf "module_kind=%s\n", kind + printf "module_src=\"%s\"\n", src + printf "module_includes=\"%s\"\n", includes + printf "module_cflags=\"%s\"\n", cflags + printf "module_ldflags=\"%s\"\n", ldflags + printf "module_target=\"%s\"\n", target + } + + src = "" + name = "" + kind = "" + includes = "" + cflags = "" + ldflags = "" + name_found = "" + src_found = "" + includes_found = "" + cflags_found = "" + ldflags_found = "" +} diff --git a/configure.ac b/configure.ac index bdcce8d8694..9df4a6a073f 100644 --- a/configure.ac +++ b/configure.ac @@ -166,6 +166,13 @@ fi AC_CANONICAL_HOST AC_CANONICAL_BUILD +if test "$XCONFIGURE" = "android"; then + # Initialize the Android NDK build system. Make sure to use the + # passed through NDK path. + with_ndk_path="$android_ndk_path" + ndk_INIT([$android_abi], [$ANDROID_SDK], [cross/ndk-build]) +fi + case $host in *-mingw*) @@ -544,6 +551,7 @@ OPTION_DEFAULT_OFF([cygwin32-native-compilation],[use native compilation on 32-b OPTION_DEFAULT_ON([xinput2],[don't use version 2 of the X Input Extension for input]) OPTION_DEFAULT_OFF([small-ja-dic],[generate a smaller-size Japanese dictionary]) OPTION_DEFAULT_OFF([android],[cross-compile Android application package]) +OPTION_DEFAULT_ON([android-debug],[don't build Emacs as a debug package on Android]) AC_ARG_WITH([file-notification],[AS_HELP_STRING([--with-file-notification=LIB], [use a file notification library (LIB one of: yes, inotify, kqueue, gfile, w32, no)])], @@ -1033,8 +1041,14 @@ package will likely install on older systems but crash on startup.]) mv -f confdefs.h _confdefs.h mv -f config.log _config.log + # Figure out what --with-FOO options to pass through. + passthrough="$passthrough --with-png=$with_png" + passthrough="$passthrough --with-webp=$with_webp" + passthrough="$passthrough --with-gif=$with_gif" + AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" \ - ANDROID_SDK="$android_sdk" $0], [], + ANDROID_SDK="$android_sdk" android_abi=$android_abi \ + android_ndk_path="$with_ndk_path" $0 $passthrough], [], [AC_MSG_ERROR([Failed to cross-configure Emacs for android.])]) # Now set ANDROID to yes. @@ -1048,6 +1062,14 @@ package will likely install on older systems but crash on startup.]) AC_MSG_NOTICE([Generating src/config.h.android]) mv -f src/config.h src/config.h.android + # Tell AndroidManifest.xml whether or not Emacs should be built + # debug. + ANDROID_DEBUGGABLE=false + if test "$with_android_debug" = "yes"; then + ANDROID_DEBUGGABLE=true + fi + AC_SUBST([ANDROID_DEBUGGABLE]) + # Move confdefs.h back now that the recursive call to configure is # complete. mv -f _confdefs.h confdefs.h @@ -1083,15 +1105,21 @@ if test "$ANDROID" = "yes"; then with_xpm=no with_jpeg=no with_tiff=no - with_gif=no - with_png=no + + # Some of these dependencies are now supported within Android, so + # they can be enabled. + if test "$XCONFIGURE" != "android"; then + with_png=no + with_webp=no + with_gif=no + fi + + with_xml2=no with_rsvg=no - with_webp=no with_sqlite3=no with_lcms2=no with_libsystemd=no with_cairo=no - with_xml2=no with_imagemagick=no with_json=no with_tree_sitter=no @@ -2218,10 +2246,13 @@ dnl EMACS_CHECK_MODULES accepts optional 3rd and 4th arguments that dnl can take the place of the default HAVE_GSTUFF=yes and HAVE_GSTUFF=no dnl actions. AC_DEFUN([EMACS_CHECK_MODULES], - [PKG_CHECK_MODULES([$1], [$2], - [$1_CFLAGS=`AS_ECHO(["$$1_CFLAGS"]) | sed -e "$edit_cflags"` - m4_default([$3], [HAVE_$1=yes])], - [m4_default([$4], [HAVE_$1=no])])]) + [AS_IF([test -n "$ndk_INITIALIZED"], + [ndk_CHECK_MODULES([$1], [$2], m4_default([$3], [HAVE_$1=yes]), + m4_default([$4],[HAVE_$1=no]))], + [PKG_CHECK_MODULES([$1], [$2], + [$1_CFLAGS=`AS_ECHO(["$$1_CFLAGS"]) | sed -e "$edit_cflags"` + m4_default([$3], [HAVE_$1=yes])], + [m4_default([$4], [HAVE_$1=no])])])]) HAVE_SOUND=no if test "${with_sound}" != "no"; then @@ -2394,6 +2425,7 @@ window_system=none ANDROID_OBJ= ANDROID_LIBS= ANDROID_CFLAGS= +REALLY_ANDROID= CM_OBJ="cm.o" if test "${ANDROID}" = "yes"; then @@ -2423,18 +2455,31 @@ for Android, but all API calls need to be stubbed out]) # Link with libraries required for Android support. ANDROID_LIBS="-landroid -llog -ljnigraphics" + # This is required to make the system load emacs.apk's libpng + # (among others) instead of the system's own. But it doesn't work + # on all Android versions yet, so for now just suffix shared + # libraries with _emacs. + # ANDROID_LDFLAGS="-Wl,-rpath,'\$\$ORIGIN'" + # Link with the sfnt font library and sfntfont.o, along with # sfntfont-android.o. ANDROID_OBJ="$ANDROID_OBJ sfnt.o sfntfont.o sfntfont-android.o" + # Build androidselect.o. + ANDROID_OBJ="$ANDROID_OBJ androidselect.o" + # Check for some functions not always present in the NDK. AC_CHECK_DECLS([android_get_device_api_level]) + + # Say this build is really for Android. + REALLY_ANDROID=yes fi fi AC_SUBST(ANDROID) AC_SUBST(ANDROID_OBJ) AC_SUBST(ANDROID_LIBS) +AC_SUBST(ANDROID_LDFLAGS) AC_SUBST(ANDROID_CFLAGS) if test "${with_pgtk}" = "yes"; then @@ -3265,7 +3310,8 @@ HAVE_WEBP=no if test "${with_webp}" != "no"; then if test "${HAVE_X11}" = "yes" || test "${opsys}" = "mingw32" \ || test "${HAVE_W32}" = "yes" || test "${HAVE_NS}" = "yes" \ - || test "${HAVE_BE_APP}" = "yes" || test "${HAVE_PGTK}" = "yes"; then + || test "${HAVE_BE_APP}" = "yes" || test "${HAVE_PGTK}" = "yes" \ + || test "${REALLY_ANDROID}" = "yes"; then WEBP_REQUIRED=0.6.0 WEBP_MODULE="libwebpdemux >= $WEBP_REQUIRED" @@ -4506,7 +4552,8 @@ HAVE_JPEG=no LIBJPEG= if test "${HAVE_X11}" = "yes" || test "${HAVE_W32}" = "yes" \ || test "${HAVE_NS}" = "yes" || test "${HAVE_BE_APP}" = "yes" \ - || test "$window_system" = "pgtk"; then + || test "$window_system" = "pgtk" \ + || test "${REALLY_ANDROID}" = "yes"; then if test "${with_jpeg}" != "no"; then AC_CACHE_CHECK([for jpeglib 6b or later], [emacs_cv_jpeglib], @@ -4852,7 +4899,8 @@ if test "${with_png}" != no; then AC_CHECK_HEADER([png.h], [HAVE_PNG=yes]) elif test "${HAVE_X11}" = "yes" || test "${HAVE_W32}" = "yes" \ || test "${HAVE_NS}" = "yes" || test "${HAVE_BE_APP}" = "yes" \ - || test "$window_system" = "pgtk"; then + || test "$window_system" = "pgtk" \ + || test "${REALLY_ANDROID}" = "yes"; then EMACS_CHECK_MODULES([PNG], [libpng >= 1.0.0]) if test $HAVE_PNG = yes; then LIBPNG=$PNG_LIBS @@ -4951,6 +4999,7 @@ AC_SUBST([LIBTIFF]) ### Use -lgif or -lungif if available, unless '--with-gif=no'. ### mingw32 doesn't use -lgif/-lungif, since it loads the library dynamically. HAVE_GIF=no +GIF_CFLAGS= LIBGIF= if test "${opsys}" = "mingw32"; then if test "${with_gif}" != "no"; then @@ -4963,6 +5012,7 @@ if test "${opsys}" = "mingw32"; then elif test "${HAVE_X11}" = "yes" && test "${with_gif}" != "no" \ || test "${HAVE_W32}" = "yes" || test "${HAVE_NS}" = "yes" \ || test "${HAVE_BE_APP}" = "yes" || test "$window_system" = "pgtk" \ + || test "${REALLY_ANDROID}" = "yes" \ && test "${with_gif}" != "no"; then AC_CHECK_HEADER([gif_lib.h], # EGifPutExtensionLast only exists from version libungif-4.1.0b1. @@ -4982,12 +5032,20 @@ elif test "${HAVE_X11}" = "yes" && test "${with_gif}" != "no" \ test "$HAVE_GIF" = yes && LIBGIF=-lungif fi +# Finally, try ndk-build on Android. + if test "$REALLY_ANDROID" = "yes"; then + ndk_SEARCH_MODULE([libgif], [GIF], [HAVE_GIF=yes], + [HAVE_GIF=no]) + test "$HAVE_GIF" = yes && LIBGIF="$GIF_LIBS" + fi + if test "${HAVE_GIF}" = "yes"; then AC_DEFINE([HAVE_GIF], [1], [Define to 1 if you have a gif (or ungif) library.]) fi fi AC_SUBST([LIBGIF]) +AC_SUBST([GIF_CFLAGS]) dnl Check for required libraries. MISSING= @@ -7291,7 +7349,7 @@ if test "$ANDROID" = "yes"; then fi if test "$XCOMPILE" = "yes"; then - SUBDIR_MAKEFILES="$SUBDIR_MAKEFILES xcompile/Makefile" + SUBDIR_MAKEFILES="$SUBDIR_MAKEFILES cross/Makefile" fi dnl The admin/ directory used to be excluded from tarfiles. @@ -7377,11 +7435,15 @@ fi # Make java/Makefile ARCH_INDEPENDENT_CONFIG_FILES([java/Makefile]) -ARCH_INDEPENDENT_CONFIG_FILES([xcompile/Makefile]) +ARCH_INDEPENDENT_CONFIG_FILES([cross/Makefile]) # Make java/AndroidManifest.xml ARCH_INDEPENDENT_CONFIG_FILES([java/AndroidManifest.xml]) +# Make ndk-build Makefiles. This is only done inside the recursive +# configure. +ndk_CONFIG_FILES + AC_OUTPUT if test ! "$with_mailutils"; then diff --git a/cross/ndk-build/Makefile.in b/cross/ndk-build/Makefile.in new file mode 100644 index 00000000000..ec8e6205680 --- /dev/null +++ b/cross/ndk-build/Makefile.in @@ -0,0 +1,115 @@ +### @configure_input@ + +# Copyright 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# ndk-build works by including a bunch of Makefiles which set +# variables, and then having those Makefiles include another makefile +# which actually builds targets. + + srcdir = @srcdir@ + +# This is a list of Android.mk files which provide targets. +NDK_BUILD_ANDROID_MK = @NDK_BUILD_ANDROID_MK@ + NDK_BUILD_ARCH = @NDK_BUILD_ARCH@ + NDK_BUILD_ABI = @NDK_BUILD_ABI@ + NDK_BUILD_SDK = @NDK_BUILD_SDK@ + NDK_BUILD_CC = @NDK_BUILD_CC@ + NDK_BUILD_AR = @NDK_BUILD_AR@ + +# This is a list of targets to build. + NDK_BUILD_MODULES = @NDK_BUILD_MODULES@ + +# This is set by the Android in tree build system and is used by some +# libraries to look for the NDK. Its value is unimportant. + NDK_ROOT = /tmp/ + +# Finally, here are rules common to Emacs. +.PHONY: all +all: $(NDK_BUILD_MODULES) + +define uniqify +$(if $1,$(firstword $1) $(call uniqify,$(filter-out $(firstword $1),$1))) +endef + +# Remove duplicate files. +NDK_BUILD_ANDROID_MK := $(call uniqify,$(NDK_BUILD_ANDROID_MK)) + +define subr-1 + +# Define ndk-build functions. + +define my-dir +$(dir $(abspath $(1))) +endef + +# NDK-defined include variables. + +CLEAR_VARS = $(srcdir)/ndk-clear-vars.mk +BUILD_EXECUTABLE = $(srcdir)/ndk-build-executable.mk +BUILD_SHARED_LIBRARY = $(srcdir)/ndk-build-shared-library.mk +BUILD_STATIC_LIBRARY = $(srcdir)/ndk-build-static-library.mk +PREBUILT_SHARED_LIBRARY = $(srcdir)/ndk-prebuilt-shared-library.mk +PREBUILT_STATIC_LIBRARY = $(srcdir)/ndk-prebuilt-static-library.mk + +# Target information variables. + +TARGET_ARCH = $(NDK_BUILD_ARCH) +TARGET_PLATFORM = android-$(NDK_BUILD_SDK) +TARGET_ARCH_ABI = $(NDK_BUILD_ABI) +TARGET_ABI = $(TARGET_PLATFORM)-$(TARGET_ABI) + +# Module description variables. These are defined by Android.mk. +LOCAL_PATH := +LOCAL_MODULE := +LOCAL_MODULE_FILENAME := +LOCAL_SRC_FILES := +LOCAL_CPP_EXTENSION := +LOCAL_CPP_FEATURES := +LOCAL_C_INCLUDES := +LOCAL_CFLAGS := +LOCAL_CPPFLAGS := +LOCAL_STATIC_LIBRARIES := +LOCAL_SHARED_LIBRARIES := +LOCAL_WHOLE_STATIC_LIBRARIES := +LOCAL_LDLIBS := +LOCAL_LDFLAGS := +LOCAL_ALLOW_UNDEFINED_SYMBOLS := +LOCAL_ARM_MODE := +LOCAL_ARM_NEON := +LOCAL_DISABLE_FORMAT_STRING_CHECKS := +LOCAL_EXPORT_CFLAGS := +LOCAL_EXPORT_CPPFLAGS := +LOCAL_EXPORT_C_INCLUDES := +LOCAL_EXPORT_LDFLAGS := +LOCAL_EXPORT_LDLIBS := + +# Now load Android.mk. +include $(1) + +endef + +# Now define rules for each Android.mk file. +$(foreach android_mk,$(NDK_BUILD_ANDROID_MK),$(eval $(call subr-1,$(android_mk)))) + +.PHONY: clean mostlyclean +clean mostlyclean: + rm -rf *.o *.so *.a + +.PHONY: extraclean dist-clean maintainer-clean +extraclean dist-clean maintainer-clean: + rm -rf Makefile diff --git a/cross/ndk-build/ndk-build-executable.mk b/cross/ndk-build/ndk-build-executable.mk new file mode 100644 index 00000000000..9591c862b18 --- /dev/null +++ b/cross/ndk-build/ndk-build-executable.mk @@ -0,0 +1,22 @@ +# Copyright 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# ndk-build works by including a bunch of Makefiles which set +# variables, and then having those Makefiles include another makefile +# which actually builds targets. + +# Building executables is not supported diff --git a/cross/ndk-build/ndk-build-shared-library.mk b/cross/ndk-build/ndk-build-shared-library.mk new file mode 100644 index 00000000000..0d90d89e55d --- /dev/null +++ b/cross/ndk-build/ndk-build-shared-library.mk @@ -0,0 +1,83 @@ +# Copyright 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# ndk-build works by including a bunch of Makefiles which set +# variables, and then having those Makefiles include another makefile +# which actually builds targets. + +eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) +objname = $(1)-$(subst /,_,$(2).o) + +define single-object-target + +ifeq (x$(suffix $(1)),x.c) + +$(call objname,$(LOCAL_MODULE),$(basename $(1))): $(LOCAL_PATH)/$(1) + $(NDK_BUILD_CC) -c $$< -o $$@ $(NDK_CFLAGS_$(LOCAL_MODULE)) + +else +ifneq ($(or $(call eq,x$(suffix $(1)),x.s),$(call eq,x$(suffix $(1)),x.S)),) + +$(call objname,$(LOCAL_MODULE),$(basename $(1))): $(LOCAL_PATH)/$(1) + $(NDK_BUILD_CC) -c $$< -o $$@ $(NDK_ASFLAGS_$(LOCAL_MODULE)) + +else +$$(error Unsupported suffix: $(suffix $(1))) +endif +endif + +ALL_OBJECT_FILES$(LOCAL_MODULE) += $(call objname,$(LOCAL_MODULE),$(basename $(1))) + +endef + +NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(addprefix $(LOCAL_PATH),$(LOCAL_C_INCLUDES))) +NDK_CFLAGS_$(LOCAL_MODULE) ::= -fPIC -iquote $(LOCAL_EXPORT_CFLAGS) $(LOCAL_PATH) $(LOCAL_CFLAGS) +NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) +NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDFLAGS) +ALL_OBJECT_FILES_$(LOCAL_MODULE) := + +ifeq ($(NDK_BUILD_ARCH)$(NDK_ARM_MODE),armarm) +NDK_CFLAGS ::= -marm +else +ifeq ($(NDK_BUILD_ARCH),arm) +NDK_CFLAGS ::= -mthumb +endif +endif + +LOCAL_MODULE_FILENAME := $(strip $(LOCAL_MODULE_FILENAME)) + +ifndef LOCAL_MODULE_FILENAME +ifeq ($(findstring lib,$(LOCAL_MODULE)),lib) +LOCAL_MODULE_FILENAME := $(LOCAL_MODULE)_emacs +else +LOCAL_MODULE_FILENAME := lib$(LOCAL_MODULE)_emacs +endif +endif + +# Since a shared library is being built, suffix the library with +# _emacs. Otherwise, libraries already on the system will be found +# first, with potentially nasty consequences. + +LOCAL_MODULE_FILENAME := $(LOCAL_MODULE_FILENAME).so + +# Then define rules to build all objects. +ALL_SOURCE_FILES = $(LOCAL_SRC_FILES) +$(foreach source,$(ALL_SOURCE_FILES),$(eval $(call single-object-target,$(source)))) + +# Now define the rule to build the shared library. +$(LOCAL_MODULE_FILENAME): $(ALL_OBJECT_FILES$(LOCAL_MODULE)) + $(NDK_BUILD_CC) $^ -o $@ -shared $(NDK_LDFLAGS$(LOCAL_MODULE)) diff --git a/cross/ndk-build/ndk-build-static-library.mk b/cross/ndk-build/ndk-build-static-library.mk new file mode 100644 index 00000000000..1423cf243cf --- /dev/null +++ b/cross/ndk-build/ndk-build-static-library.mk @@ -0,0 +1,83 @@ +# Copyright 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# ndk-build works by including a bunch of Makefiles which set +# variables, and then having those Makefiles include another makefile +# which actually builds targets. + +eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) +objname = $(1)-$(subst /,_,$(2).o) + +define single-object-target + +ifeq (x$(suffix $(1)),x.c) + +$(call objname,$(LOCAL_MODULE),$(basename $(1))): $(LOCAL_PATH)/$(1) + $(NDK_BUILD_CC) -c $$< -o $$@ $(NDK_CFLAGS_$(LOCAL_MODULE)) + +else +ifneq ($(or $(call eq,x$(suffix $(1)),x.s),$(call eq,x$(suffix $(1)),x.S)),) + +$(call objname,$(LOCAL_MODULE),$(basename $(1))): $(LOCAL_PATH)/$(1) + $(NDK_BUILD_CC) -c $$< -o $$@ $(NDK_ASFLAGS_$(LOCAL_MODULE)) + +else +$$(error Unsupported suffix: $(suffix $(1))) +endif +endif + +ALL_OBJECT_FILES$(LOCAL_MODULE) += $(call objname,$(LOCAL_MODULE),$(basename $(1))) +endef + +NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(addprefix $(LOCAL_PATH),$(LOCAL_C_INCLUDES))) +NDK_CFLAGS_$(LOCAL_MODULE) ::= -iquote $(LOCAL_PATH) $(LOCAL_EXPORT_CFLAGS) $(LOCAL_CFLAGS) $(LOCAL_CFLAGS_$(NDK_BUILD_ARCH)) +NDK_ASFLAGS_$(LOCAL_MODULE) ::= $(LOCAL_ASFLAGS) $(LOCAL_ASFLAGS_$(NDK_BUILD_ARCH)) +NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) +NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDFLAGS) +ALL_OBJECT_FILES$(LOCAL_MODULE) := + +ifeq ($(NDK_BUILD_ARCH)$(NDK_ARM_MODE),armarm) +NDK_CFLAGS ::= -marm +else +ifeq ($(NDK_BUILD_ARCH),arm) +NDK_CFLAGS ::= -mthumb +endif +endif + +LOCAL_MODULE_FILENAME := $(strip $(LOCAL_MODULE_FILENAME)) + +ifndef LOCAL_MODULE_FILENAME +ifeq ($(findstring lib,$(LOCAL_MODULE)),lib) +LOCAL_MODULE_FILENAME := $(LOCAL_MODULE) +else +LOCAL_MODULE_FILENAME := lib$(LOCAL_MODULE) +endif +endif + +LOCAL_MODULE_FILENAME := $(LOCAL_MODULE_FILENAME).a + +# Then define rules to build all objects. +ALL_SOURCE_FILES = $(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES_$(NDK_BUILD_ARCH)) + +# This defines all dependencies. +ALL_OBJECT_FILES$(LOCAL_MODULE) = + +$(foreach source,$(ALL_SOURCE_FILES),$(eval $(call single-object-target,$(source)))) + +# Now define the rule to build the library. +$(LOCAL_MODULE_FILENAME): $(ALL_OBJECT_FILES$(LOCAL_MODULE)) + $(NDK_BUILD_AR) r $@ $^ diff --git a/cross/ndk-build/ndk-build.in b/cross/ndk-build/ndk-build.in new file mode 100644 index 00000000000..8586b90d840 --- /dev/null +++ b/cross/ndk-build/ndk-build.in @@ -0,0 +1,44 @@ +### @configure_input@ + +# Copyright (C) 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# This file is included all over the place to build prerequisites. + +NDK_BUILD_MODULES = @NDK_BUILD_MODULES@ +NDK_BUILD_SHARED = +NDK_BUILD_STATIC = + +define subr-1 + +.PHONY $(top_builddir)/cross/ndk-build/$(0) +$(top_builddir)/cross/ndk-build/$(0): + $(MAKE) -C $(top_builddir)/cross/ndk-build $(0) + +ifeq ($(suffix $(0)),.so) +NDK_BUILD_SHARED += $(top_builddir)/cross/ndk-build/$(0) +else +ifeq ($(suffix $(0)),.a) +NDK_BUILD_STATIC += $(top_builddir)/cross/ndk-build/$(0) +endif +endif + +endef + +# Generate rules for each module. + +$(foreach module,$(NDK_BUILD_MODULES),$(eval $(call subr-1,$(module)))) diff --git a/cross/ndk-build/ndk-build.mk.in b/cross/ndk-build/ndk-build.mk.in new file mode 100644 index 00000000000..6ad577ccc35 --- /dev/null +++ b/cross/ndk-build/ndk-build.mk.in @@ -0,0 +1,43 @@ +### @configure_input@ + +# Copyright (C) 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# This file is included all over the place to build prerequisites. + +NDK_BUILD_MODULES = @NDK_BUILD_MODULES@ +NDK_BUILD_SHARED = +NDK_BUILD_STATIC = + +define subr-1 + +$(top_builddir)/cross/ndk-build/$(1): + $(MAKE) -C $(top_builddir)/cross/ndk-build $(1) + +ifeq ($(suffix $(1)),.so) +NDK_BUILD_SHARED += $(top_builddir)/cross/ndk-build/$(1) +else +ifeq ($(suffix $(1)),.a) +NDK_BUILD_STATIC += $(top_builddir)/cross/ndk-build/$(1) +endif +endif + +endef + +# Generate rules for each module. + +$(foreach module,$(NDK_BUILD_MODULES),$(eval $(call subr-1,$(module)))) diff --git a/cross/ndk-build/ndk-clear-vars.mk b/cross/ndk-build/ndk-clear-vars.mk new file mode 100644 index 00000000000..df2bf95fd5e --- /dev/null +++ b/cross/ndk-build/ndk-clear-vars.mk @@ -0,0 +1,47 @@ +# Copyright 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# ndk-build works by including a bunch of Makefiles which set +# variables, and then having those Makefiles include another makefile +# which actually builds targets. + +undefine LOCAL_MODULE +undefine LOCAL_MODULE_FILENAME +undefine LOCAL_SRC_FILES +undefine LOCAL_CPP_EXTENSION +undefine LOCAL_CPP_FEATURES +undefine LOCAL_C_INCLUDES +undefine LOCAL_CFLAGS +undefine LOCAL_CPPFLAGS +undefine LOCAL_STATIC_LIBRARIES +undefine LOCAL_SHARED_LIBRARIES +undefine LOCAL_WHOLE_STATIC_LIBRARIES +undefine LOCAL_LDLIBS +undefine LOCAL_LDFLAGS +undefine LOCAL_ALLOW_UNDEFINED_SYMBOLS +undefine LOCAL_ARM_MODE +undefine LOCAL_ARM_NEON +undefine LOCAL_DISABLE_FORMAT_STRING_CHECKS +undefine LOCAL_EXPORT_CFLAGS +undefine LOCAL_EXPORT_CPPFLAGS +undefine LOCAL_EXPORT_C_INCLUDES +undefine LOCAL_EXPORT_LDFLAGS +undefine LOCAL_EXPORT_LDLIBS + +undefine LOCAL_SRC_FILES_$(NDK_BUILD_ARCH) +undefine LOCAL_ASFLAGS_$(NDK_BUILD_ARCH) +undefine LOCAL_CFLAGS_$(NDK_BUILD_ARCH) diff --git a/cross/ndk-build/ndk-prebuilt-shared-library.mk b/cross/ndk-build/ndk-prebuilt-shared-library.mk new file mode 100644 index 00000000000..2a8260f9851 --- /dev/null +++ b/cross/ndk-build/ndk-prebuilt-shared-library.mk @@ -0,0 +1,24 @@ +### @configure_input@ + +# Copyright 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# ndk-build works by including a bunch of Makefiles which set +# variables, and then having those Makefiles include another makefile +# which actually builds targets. + +$(warn Prebuilt shared libraries are not supported) diff --git a/cross/ndk-build/ndk-prebuilt-static-library.mk b/cross/ndk-build/ndk-prebuilt-static-library.mk new file mode 100644 index 00000000000..9230f690bb1 --- /dev/null +++ b/cross/ndk-build/ndk-prebuilt-static-library.mk @@ -0,0 +1,24 @@ +### @configure_input@ + +# Copyright 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# ndk-build works by including a bunch of Makefiles which set +# variables, and then having those Makefiles include another makefile +# which actually builds targets. + +$(warn Prebuilt static libraries are not supported) diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 223e9dbf8ef..e4498da401d 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -18,6 +18,7 @@ about using such devices with Emacs, @pxref{Other Input Devices}. * Android Startup:: Starting up Emacs on Android. * Android File System:: The Android file system. * Android Environment:: Running Emacs under Android. +* Android Windowing:: The Android window system. * Android Fonts:: Font selection under Android. @end menu @@ -299,7 +300,8 @@ settings application. Consult the manufacturer of your device for more details, as how to do this varies by device. @end itemize -@section Android windowing +@node Android Windowing +@section The Android window system Android has an unusual window system; there, all windows are maximized or full-screen, and only one window can be displayed at a @@ -372,6 +374,31 @@ The @code{fullscreen} frame parameter is always @code{maximized} for top-level frames. @end itemize +@cindex selections, android +@cindex android clipboard + Emacs does not implement all selection related features supported +under the X Window System on Android. For example, only the +@code{CLIPBOARD} and @code{PRIMARY} selections (@pxref{Cut and Paste}) +are supported, and plain text is the only supported data type. + + In addition, the Android system itself places certain restrictions +on what selection data Emacs can access: + +@itemize @bullet +@item +On Android 2.3 and earlier, the function @code{gui-selection-owner-p} +always returns @code{nil} for the clipboard selection. + +@item +On Android 3.0 and later, Emacs can only access clipboard data when +one of its frames has the input focus. +@end itemize + + Since the Android system itself has no concept of a primary +selection, Emacs provides an emulation instead. This means there is +no way to transfer the contents of the primary selection to another +application via cut-and-paste. + @node Android Fonts @section Font backends and selection under Android @cindex fonts, android diff --git a/doc/emacs/emacs.texi b/doc/emacs/emacs.texi index 0cb454e5294..30758efb43a 100644 --- a/doc/emacs/emacs.texi +++ b/doc/emacs/emacs.texi @@ -1264,6 +1264,7 @@ Emacs and Android * Android Startup:: Starting up Emacs on Android. * Android File System:: The Android file system. * Android Environment:: Running Emacs under Android. +* Android Windowing:: The Android window system. * Android Fonts:: Font selection under Android. Emacs and unconventional input devices diff --git a/etc/MACHINES b/etc/MACHINES index 8c6f3f48ce7..8ba0b1faa77 100644 --- a/etc/MACHINES +++ b/etc/MACHINES @@ -131,6 +131,14 @@ the list at the end of this file. The earliest release of Haiku that will successfully compile Emacs is R1/Beta2. For windowing support, R1/Beta3 or later is required. +** Android + + Emacs is known to run on all Android versions from 2.3 onwards. + It should work with Android 2.2 as well, but only the build + has been tested, and actually running the built Emacs has not. + + See the file INSTALL.android for detailed installation instructions. + * Obsolete platforms diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index 74f69d2a8e5..c4a9d1f5177 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -60,12 +60,13 @@ along with GNU Emacs. If not, see . --> android:hardwareAccelerated="true" android:supportsRtl="true" android:theme="@android:style/Theme" - android:debuggable="true" + android:debuggable="@ANDROID_DEBUGGABLE@" android:extractNativeLibs="true"> @@ -76,10 +77,12 @@ along with GNU Emacs. If not, see . --> diff --git a/java/Makefile.in b/java/Makefile.in index 22c912fdce5..d27775ea3db 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -81,21 +81,24 @@ APK_NAME = emacs-$(version)-$(ANDROID_MIN_SDK)-$(ANDROID_ABI).apk all: $(APK_NAME) # Binaries to cross-compile. -CROSS_BINS = ../xcompile/src/android-emacs ../xcompile/lib-src/ctags \ - ../xcompile/lib-src/hexl ../xcompile/lib-src/movemail \ - ../xcompile/lib-src/ebrowse ../xcompile/lib-src/emacsclient +CROSS_BINS = ../cross/src/android-emacs ../cross/lib-src/ctags \ + ../cross/lib-src/hexl ../cross/lib-src/movemail \ + ../cross/lib-src/ebrowse ../cross/lib-src/emacsclient # Libraries to cross-compile. -CROSS_LIBS = ../xcompile/src/libemacs.so +CROSS_LIBS = ../cross/src/libemacs.so + +# Third party libraries to compile. +include $(top_builddir)/cross/ndk-build/ndk-build.mk .PHONY: $(CROSS_BINS) $(CROSS_LIBS) -../xcompile/src/android-emacs ../xcompile/src/libemacs.so: - make -C ../xcompile src/$(notdir $@) +../cross/src/android-emacs ../cross/src/libemacs.so: + make -C ../cross src/$(notdir $@) -../xcompile/lib-src/hexl ../xcompile/lib-src/movemail \ -../xcompile/lib-src/ctags ../xcompile/lib-src/ebrowse &: - make -C ../xcompile lib-src/$(notdir $@) +../cross/lib-src/hexl ../cross/lib-src/movemail \ +../cross/lib-src/ctags ../cross/lib-src/ebrowse &: + make -C ../cross lib-src/$(notdir $@) # This is needed to generate the ``.directory-tree'' file used by the # Android emulations of readdir and faccessat. @@ -104,7 +107,7 @@ $(libsrc)/asset-directory-tool: $(MAKE) -C $(libsrc) $(notdir $@) emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) $(libsrc)/asset-directory-tool \ - AndroidManifest.xml + AndroidManifest.xml $(NDK_BUILD_SHARED) # Make the working directory for this stuff rm -rf install_temp mkdir -p install_temp/lib/$(ANDROID_ABI) @@ -145,11 +148,13 @@ emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) $(libsrc)/asset-directory-tool \ cp -f $$file install_temp/lib/$(ANDROID_ABI); \ fi \ done + $(foreach module,$(NDK_BUILD_SHARED), \ + cp -f $(module) install_temp/lib/$(ANDROID_ABI)) # Package everything. Specifying the assets on this command line is # necessary for AAssetManager_getNextFileName to work on old versions # of Android. - $(AAPT) package -I "$(ANDROID_JAR)" -F $@ -f -M AndroidManifest.xml \ - -A install_temp/assets + $(AAPT) package -I "$(ANDROID_JAR)" -F $@ -f \ + -M AndroidManifest.xml -A install_temp/assets pushd install_temp; $(AAPT) add ../$@ `find lib -type f`; popd rm -rf install_temp @@ -196,3 +201,4 @@ clean: find . -name '*.class' -delete maintainer-clean distclean bootstrap-clean: clean + rm -f Makefile ndk-build.mk diff --git a/java/org/gnu/emacs/EmacsClipboard.java b/java/org/gnu/emacs/EmacsClipboard.java new file mode 100644 index 00000000000..cd6bcebfe0e --- /dev/null +++ b/java/org/gnu/emacs/EmacsClipboard.java @@ -0,0 +1,44 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.os.Build; + +/* This class provides helper code for accessing the clipboard, + abstracting between the different interfaces on API 8 and 11. */ + +public abstract class EmacsClipboard +{ + public abstract void setClipboard (byte[] bytes); + public abstract int ownsClipboard (); + public abstract boolean clipboardExists (); + public abstract byte[] getClipboard (); + + /* Create the correct kind of clipboard for this system. */ + + public static EmacsClipboard + makeClipboard () + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) + return new EmacsSdk11Clipboard (); + else + return new EmacsSdk8Clipboard (); + } +}; diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java index 1d1e6f7b33f..39bda5a456d 100644 --- a/java/org/gnu/emacs/EmacsFontDriver.java +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -23,6 +23,9 @@ import java.util.List; import android.os.Build; +/* This code is mostly unused. See sfntfont-android.c for the code + that is actually used. */ + public abstract class EmacsFontDriver { /* Font weights. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 3efdc0cff9a..962538bef7b 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -132,6 +132,10 @@ public class EmacsNative /* Send an ANDROID_CONTEXT_MENU event. */ public static native long sendContextMenu (short window, int menuEventID); + /* Send an ANDROID_EXPOSE event. */ + public static native long sendExpose (short window, int x, int y, + int width, int height); + static { System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java new file mode 100644 index 00000000000..0a725200723 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java @@ -0,0 +1,156 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.content.ClipboardManager; +import android.content.Context; +import android.content.ClipData; + +import android.util.Log; + +import java.io.UnsupportedEncodingException; + +/* This class implements EmacsClipboard for Android 3.0 and later + systems. */ + +public class EmacsSdk11Clipboard extends EmacsClipboard + implements ClipboardManager.OnPrimaryClipChangedListener +{ + private static final String TAG = "EmacsSdk11Clipboard"; + private ClipboardManager manager; + private boolean ownsClipboard; + private int clipboardChangedCount; + private int monitoredClipboardChangedCount; + + public + EmacsSdk11Clipboard () + { + String what; + Context context; + + what = Context.CLIPBOARD_SERVICE; + context = EmacsService.SERVICE; + manager + = (ClipboardManager) context.getSystemService (what); + manager.addPrimaryClipChangedListener (this); + } + + @Override + public synchronized void + onPrimaryClipChanged () + { + Log.d (TAG, ("onPrimaryClipChanged: " + + monitoredClipboardChangedCount + + " " + clipboardChangedCount)); + + /* Increment monitoredClipboardChangeCount. If it is now greater + than clipboardChangedCount, then Emacs no longer owns the + clipboard. */ + monitoredClipboardChangedCount++; + + if (monitoredClipboardChangedCount > clipboardChangedCount) + { + ownsClipboard = false; + + /* Reset both values back to 0. */ + monitoredClipboardChangedCount = 0; + clipboardChangedCount = 0; + } + } + + /* Set the clipboard text to CLIPBOARD, a string in UTF-8 + encoding. */ + + @Override + public synchronized void + setClipboard (byte[] bytes) + { + ClipData data; + String string; + + try + { + string = new String (bytes, "UTF-8"); + data = ClipData.newPlainText ("Emacs", string); + manager.setPrimaryClip (data); + ownsClipboard = true; + + /* onPrimaryClipChanged will be called again. Use this + variable to keep track of how many times the clipboard has + been changed. */ + ++clipboardChangedCount; + } + catch (UnsupportedEncodingException exception) + { + Log.w (TAG, "setClipboard: " + exception); + } + } + + /* Return whether or not Emacs owns the clipboard. Value is 1 if + Emacs does, 0 if Emacs does not, and -1 if that information is + unavailable. */ + + @Override + public synchronized int + ownsClipboard () + { + return ownsClipboard ? 1 : 0; + } + + /* Return whether or not clipboard content currently exists. */ + + @Override + public boolean + clipboardExists () + { + return manager.hasPrimaryClip (); + } + + /* Return the current content of the clipboard, as plain text, or + NULL if no content is available. */ + + @Override + public byte[] + getClipboard () + { + ClipData clip; + CharSequence text; + Context context; + + clip = manager.getPrimaryClip (); + + if (clip == null || clip.getItemCount () < 1) + return null; + + context = EmacsService.SERVICE; + + try + { + text = clip.getItemAt (0).coerceToText (context); + return text.toString ().getBytes ("UTF-8"); + } + catch (UnsupportedEncodingException exception) + { + Log.w (TAG, "getClipboard: " + exception); + } + + return null; + } +}; diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java new file mode 100644 index 00000000000..34e66912562 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java @@ -0,0 +1,116 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +/* Importing the entire package avoids the deprecation warning. */ +import android.text.*; + +import android.content.Context; +import android.util.Log; + +import java.io.UnsupportedEncodingException; + +/* This class implements EmacsClipboard for Android 2.2 and other + similarly old systems. */ + +@SuppressWarnings ("deprecation") +public class EmacsSdk8Clipboard extends EmacsClipboard +{ + private static final String TAG = "EmacsSdk8Clipboard"; + private ClipboardManager manager; + + public + EmacsSdk8Clipboard () + { + String what; + Context context; + + what = Context.CLIPBOARD_SERVICE; + context = EmacsService.SERVICE; + manager + = (ClipboardManager) context.getSystemService (what); + } + + /* Set the clipboard text to CLIPBOARD, a string in UTF-8 + encoding. */ + + @Override + public void + setClipboard (byte[] bytes) + { + try + { + manager.setText (new String (bytes, "UTF-8")); + } + catch (UnsupportedEncodingException exception) + { + Log.w (TAG, "setClipboard: " + exception); + } + } + + /* Return whether or not Emacs owns the clipboard. Value is 1 if + Emacs does, 0 if Emacs does not, and -1 if that information is + unavailable. */ + + @Override + public int + ownsClipboard () + { + return -1; + } + + /* Return whether or not clipboard content currently exists. */ + + @Override + public boolean + clipboardExists () + { + return manager.hasText (); + } + + /* Return the current content of the clipboard, as plain text, or + NULL if no content is available. */ + + @Override + public byte[] + getClipboard () + { + String string; + CharSequence text; + + text = manager.getText (); + + if (text == null) + return null; + + string = text.toString (); + + try + { + return string.getBytes ("UTF-8"); + } + catch (UnsupportedEncodingException exception) + { + Log.w (TAG, "getClipboard: " + exception); + } + + return null; + } +}; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 74bbb7b3ecc..881cbc363ba 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -99,6 +99,9 @@ public class EmacsView extends ViewGroup yet responded to. 0 if there is no such outstanding event. */ public long pendingConfigure; + /* Whether or not this view is attached to a window. */ + public boolean isAttachedToWindow; + public EmacsView (EmacsWindow window) { @@ -140,6 +143,9 @@ public class EmacsView extends ViewGroup if (measuredWidth == 0 || measuredHeight == 0) return; + if (!isAttachedToWindow) + return; + /* If bitmap is the same width and height as the measured width and height, there is no need to do anything. Avoid allocating the extra bitmap. */ @@ -547,6 +553,8 @@ public class EmacsView extends ViewGroup public synchronized void onDetachedFromWindow () { + isAttachedToWindow = false; + synchronized (this) { /* Recycle the bitmap and call GC. */ @@ -559,6 +567,21 @@ public class EmacsView extends ViewGroup } } + @Override + public synchronized void + onAttachedToWindow () + { + isAttachedToWindow = true; + + /* Dirty the bitmap, as it was destroyed when onDetachedFromWindow + was called. */ + bitmapDirty = true; + + /* Now expose the view contents again. */ + EmacsNative.sendExpose (this.window.handle, 0, 0, + measuredWidth, measuredHeight); + } + public void showOnScreenKeyboard () { diff --git a/lib/Makefile.in b/lib/Makefile.in index fd730b554e3..b84a1bf9741 100644 --- a/lib/Makefile.in +++ b/lib/Makefile.in @@ -21,7 +21,7 @@ srcdir = @srcdir@ VPATH = @srcdir@ # This is not empty if this is a Makefile that will be copied to -# xcompile/lib. +# cross/lib. XCONFIGURE = @XCONFIGURE@ # This is required to make sure symbol visibility is correct and @@ -64,7 +64,7 @@ endif ifeq ($(XCONFIGURE),android) # The next line is necessary to override -I$(srcdir), which will end # up pulling in lots of headers from the host. -ALL_CFLAGS += -I$(top_srcdir)/xcompile -I. +ALL_CFLAGS += -I$(top_srcdir)/cross -I. endif DEPDIR = deps diff --git a/lisp/term/android-win.el b/lisp/term/android-win.el index 43b75d39f50..6ab469dc0b0 100644 --- a/lisp/term/android-win.el +++ b/lisp/term/android-win.el @@ -59,5 +59,111 @@ DISPLAY is ignored on Android." ;; arguments on. (ignore)) + +;;; Selection support. + +(declare-function android-clipboard-exists-p "androidselect.c") +(declare-function android-get-clipboard "androidselect.c") +(declare-function android-set-clipboard "androidselect.c") +(declare-function android-clipboard-owner-p "androidselect.c") + +(defvar android-primary-selection nil + "The last string placed in the primary selection. +Nil if there was no such string. + +Android does not have a primary selection of its own, so Emacs +emulates one inside Lisp.") + +(defun android-get-clipboard-1 (data-type) + "Return the clipboard data. +DATA-TYPE is a selection conversion target; only STRING and +TARGETS are supported." + (or (and (eq data-type 'STRING) + (android-get-clipboard)) + (and (eq data-type 'TARGETS) + (android-clipboard-exists-p) + [TARGETS STRING]))) + +(defun android-get-primary (data-type) + "Return the last string placed in the primary selection, or nil. +Return nil if DATA-TYPE is anything other than STRING or TARGETS." + (when android-primary-selection + (or (and (eq data-type 'STRING) + android-primary-selection) + (and (eq data-type 'TARGETS) + [TARGETS])))) + +(defun android-selection-bounds (value) + "Return bounds of selection value VALUE. +The return value is a list (BEG END BUF) if VALUE is a cons of +two markers or an overlay. Otherwise, it is nil." + (cond ((bufferp value) + (with-current-buffer value + (when (mark t) + (list (mark t) (point) value)))) + ((and (consp value) + (markerp (car value)) + (markerp (cdr value))) + (when (and (marker-buffer (car value)) + (buffer-name (marker-buffer (car value))) + (eq (marker-buffer (car value)) + (marker-buffer (cdr value)))) + (list (marker-position (car value)) + (marker-position (cdr value)) + (marker-buffer (car value))))) + ((overlayp value) + (when (overlay-buffer value) + (list (overlay-start value) + (overlay-end value) + (overlay-buffer value)))))) + +(defun android-encode-select-string (value) + "Turn VALUE into a string suitable for placing in the clipboard. +VALUE should be something suitable for passing to +`gui-set-selection'." + (unless (stringp value) + (when-let ((bounds (android-selection-bounds value))) + (setq value (ignore-errors + (with-current-buffer (nth 2 bounds) + (buffer-substring (nth 0 bounds) + (nth 1 bounds))))))) + value) + +(cl-defmethod gui-backend-get-selection (type data-type + &context (window-system android)) + (cond ((eq type 'CLIPBOARD) + (android-get-clipboard-1 data-type)) + ((eq type 'PRIMARY) + (android-get-primary data-type)))) + +(cl-defmethod gui-backend-selection-exists-p (selection + &context (window-system android)) + (cond ((eq selection 'CLIPBOARD) + (android-clipboard-exists-p)) + ((eq selection 'PRIMARY) + (not (null android-primary-selection))))) + +(cl-defmethod gui-backend-selection-owner-p (selection + &context (window-system android)) + (cond ((eq selection 'CLIPBOARD) + (let ((ownership (android-clipboard-owner-p))) + ;; If ownership is `lambda', then Emacs couldn't determine + ;; whether or not it owns the clipboard. + (and (not (eq ownership 'lambda)) ownership))) + ((eq selection 'PRIMARY) + ;; Emacs always owns its own primary selection as long as it + ;; exists. + (not (null android-primary-selection))))) + +(cl-defmethod gui-backend-set-selection (type value + &context (window-system android)) + ;; First, try to turn value into a string. + ;; Don't set anything if that did not work. + (when-let ((string (android-encode-select-string value))) + (cond ((eq type 'CLIPBOARD) + (android-set-clipboard string)) + ((eq type 'PRIMARY) + (setq android-primary-selection string))))) + (provide 'android-win) ;; android-win.el ends here. diff --git a/m4/ndk-build.m4 b/m4/ndk-build.m4 new file mode 100644 index 00000000000..a35900884d9 --- /dev/null +++ b/m4/ndk-build.m4 @@ -0,0 +1,213 @@ +dnl Copyright (C) 2023 Free Software Foundation, Inc. +dnl This file is part of GNU Emacs. + +dnl GNU Emacs is free software: you can redistribute it and/or modify +dnl it under the terms of the GNU General Public License as published by +dnl the Free Software Foundation, either version 3 of the License, or +dnl (at your option) any later version. + +dnl GNU Emacs is distributed in the hope that it will be useful, +dnl but WITHOUT ANY WARRANTY; without even the implied warranty of +dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +dnl GNU General Public License for more details. + +dnl You should have received a copy of the GNU General Public License +dnl along with GNU Emacs. If not, see . + +# Support for building Emacs with dependencies using the Android NDK +# build system. + +AC_ARG_WITH([ndk_path], + [AS_HELP_STRING([--with-ndk-path], + [find Android libraries in these directories])]) + +# ndk_INIT(ABI, API, DIR) +# -------- +# Initialize the Android NDK. ABI is the ABI being built for. +# API is the API version being built for. +# As a side effect, set the variable ndk_INITIALIZED to true. +# DIR should be a directory containing the Makefile.in actually +# implementing the Android NDK build system. + +AC_DEFUN_ONCE([ndk_INIT], +[ +# Look for Android.mk files. +ndk_module_files= +for file in $with_ndk_path; do + if test -f $file/Android.mk; then + ndk_module_files="$ndk_module_files$file/Android.mk " + fi +done + +ndk_ABI=$1 +ndk_MODULES= +ndk_MAKEFILES= +ndk_INITIALIZED=yes +ndk_API=$2 +ndk_DIR=$3 + +case "$ndk_ABI" in + *arm64* ) + ndk_ARCH=arm64 + ;; + *arm* ) + ndk_ARCH=arm + ;; + *x86_64* ) + ndk_ARCH=x86_64 + ;; + *x86* ) + ndk_ARCH=x86 + ;; + * ) + AC_MSG_ERROR([Failed to determine Android device architecture]) + ;; +esac + +# This is a map between pkg-config style package names and Android +# ones. + +ndk_package_map="libwebpdemux:webpdemux libxml-2.0:libxml2" + +# Replace ndk_module with the appropriate Android module name if it is +# found in ndk_package_map. + +ndk_replace_pkg_config_package () { + for ndk_stuff in $ndk_package_map; do + ndk_key=${ndk_stuff%%:*} + ndk_value=${ndk_stuff#*:} + + if test "$ndk_key" = "$ndk_module"; then + ndk_module="$ndk_value" + break + fi + done +} + +# Parse a pkg-config style list of modules. Place the resulting list +# in ndk_modules. + +ndk_parse_pkg_config_string () { + ndk_input=[$]1 + ndk_modules= + while test -n "$ndk_input"; do + ndk_str=$(printf "$ndk_input" | cut -f1 -d' ') + ndk_input="$(printf "$ndk_input" | cut -s -f2- -d' ')" + + if test "$ndk_str" = ">=" || test "$ndk_str" = "<=" \ + || test "$ndk_str" = ">" || test "$ndk_str" = "<"; then + ndk_input="$(printf "$ndk_input" | cut -s -f2- -d' ')" + else + ndk_modules="$ndk_modules$ndk_str " + fi + done +} + +# Look for a suitable ar in the same directory as the C compiler. +ndk_where_cc=$(which $CC) +ndk_ar_search_path=$PATH + +# First, try to find $host_alias-ar in PATH. +AC_PATH_PROGS([AR], [$host_alias-ar], [], [$ndk_ar_search_path]) + +if test -z "$AR"; then + # Next, try finding either that or llvm-ar in the directory holding + # CC. + ndk_ar_search_path="$(dirname $ndk_where_cc):$ndk_ar_search_path" + AC_PATH_PROGS([AR], [$host_alias-ar llvm-ar], [], [$ndk_ar_search_path]) +fi + +# These variables have now been found. +]) + +# ndk_SEARCH_MODULE(MODULE, NAME, ACTION-IF-FOUND, [ACTION-IF-NOT-FOUND]) +# -------------------------------------------------------------------- +# Search for a module named MODULE in `with_ndk_path'. Add the file +# name of the module's Android.mk file to the variable ndk_MODULES. +# Set NAME_CFLAGS and NAME_LIBS to the appropriate values. Then, +# call ACTION-IF-FOUND, or ACTION-IF-NOT-FOUND upon failure. +AC_DEFUN([ndk_SEARCH_MODULE], +[ +module_name= +ndk_module=$1 +ndk_replace_pkg_config_package +AC_MSG_CHECKING([for Android.mk that builds $ndk_module]) + +for ndk_android_mk in $ndk_module_files; do + # Read this Android.mk file. Set NDK_ROOT to /tmp: the Android in + # tree build system sets it to a meaning value, but build files just + # use it to test whether or not the NDK is being used. + ndk_commands=$(make -s -f build-aux/ndk-build-helper.mk EMACS_SRCDIR=. \ + EMACS_ABI=$ndk_ABI ANDROID_MAKEFILE="$ndk_android_mk" \ + ANDROID_MODULE_DIRECTORY=$(dirname "$ndk_android_mk") \ + NDK_BUILD_DIR="$ndk_DIR" NDK_ROOT="/tmp" \ + | awk -f build-aux/ndk-module-extract.awk \ + MODULE="$ndk_module") + + AS_IF([test -n "${ndk_commands//\n }"], [eval "$ndk_commands"]) + + if test -n "$module_name"; then + break + fi +done + +if test -z "$module_name"; then + AC_MSG_RESULT([no]) + $4 +else + $2[]_CFLAGS="[$]$2[]_CFLAGS $module_cflags $module_includes" + $2[]_LIBS="[$]$2[]_LIBS $module_ldflags" + ndk_MAKEFILES="$ndk_MAKEFILES $ndk_android_mk" + ndk_MODULES="$ndk_MODULES $module_target" + AC_MSG_RESULT([yes]) + $3 +fi +]) + +# ndk_CHECK_MODULES(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], +# [ACTION-IF-NOT-FOUND]) +# -------------------------------------------------------------- +# Just like `PKG_CHECK_MODULES'. However, it uses the ndk-build +# system instead. + +AC_DEFUN([ndk_CHECK_MODULES], +[ + ndk_modules= + ndk_parse_pkg_config_string "$2" + ndk_found=no + + for module in $ndk_modules; do + ndk_SEARCH_MODULE([$module], [$1], [ndk_found=yes], [ndk_found=no]) + done + + AS_IF([test "$ndk_found" = "yes"],[$3],[$4]) +]) + +# ndk_CONFIG_FILES +# ------------------------------------------------------------- +# Write out the NDK build Makefile with the appropriate variables +# set if the NDK has been initialized. + +AC_DEFUN_ONCE([ndk_CONFIG_FILES], +[ + if test "$ndk_INITIALIZED" = "yes"; then + NDK_BUILD_ANDROID_MK="$ndk_MAKEFILES" + NDK_BUILD_ARCH=$ndk_ARCH + NDK_BUILD_ABI=$ndk_ABI + NDK_BUILD_SDK=$ndk_API + NDK_BUILD_CC=$CC + NDK_BUILD_AR=$AR + NDK_BUILD_MODULES="$ndk_MODULES" + + AC_SUBST([NDK_BUILD_ANDROID_MK]) + AC_SUBST([NDK_BUILD_ARCH]) + AC_SUBST([NDK_BUILD_ABI]) + AC_SUBST([NDK_BUILD_SDK]) + AC_SUBST([NDK_BUILD_CC]) + AC_SUBST([NDK_BUILD_AR]) + AC_SUBST([NDK_BUILD_MODULES]) + + AC_CONFIG_FILES([$ndk_DIR/Makefile]) + AC_CONFIG_FILES([$ndk_DIR/ndk-build.mk]) + fi +]) diff --git a/src/Makefile.in b/src/Makefile.in index 1b4a8a16c46..ddc0b89dda1 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -35,7 +35,7 @@ abs_top_srcdir=@abs_top_srcdir@ VPATH = $(srcdir) # This is not empty if this is a Makefile that will be copied to -# xcompile/src. +# cross/src. XCONFIGURE = @XCONFIGURE@ ifneq ($(XCONFIGURE),) @@ -138,6 +138,8 @@ LIB_PTHREAD=@LIB_PTHREAD@ LIBIMAGE=@LIBTIFF@ @LIBJPEG@ @LIBPNG@ @LIBGIF@ @LIBXPM@ @WEBP_LIBS@ +GIF_CFLAGS=@GIF_CFLAGS@ + XCB_LIBS=@XCB_LIBS@ XFT_LIBS=@XFT_LIBS@ XRENDER_LIBS=@XRENDER_LIBS@ @@ -384,6 +386,7 @@ HAIKU_CFLAGS = @HAIKU_CFLAGS@ ANDROID_OBJ = @ANDROID_OBJ@ ANDROID_LIBS = @ANDROID_LIBS@ +ANDROID_LDFLAGS = @ANDROID_LDFLAGS@ ANDROID_CFLAGS = @ANDROID_CFLAGS@ DUMPING=@DUMPING@ @@ -428,7 +431,7 @@ EMACS_CFLAGS=-Demacs $(MYCPPFLAGS) -I. -I$(srcdir) \ $(LIBSYSTEMD_CFLAGS) $(JSON_CFLAGS) $(XSYNC_CFLAGS) $(TREE_SITTER_CFLAGS) \ $(LIBGNUTLS_CFLAGS) $(NOTIFY_CFLAGS) $(CAIRO_CFLAGS) \ $(WERROR_CFLAGS) $(HAIKU_CFLAGS) $(XCOMPOSITE_CFLAGS) $(XSHAPE_CFLAGS) \ - $(ANDROID_CFLAGS) + $(ANDROID_CFLAGS) $(GIF_CFLAGS) ALL_CFLAGS = $(EMACS_CFLAGS) $(WARN_CFLAGS) $(CFLAGS) ALL_OBJC_CFLAGS = $(EMACS_CFLAGS) \ $(filter-out $(NON_OBJC_CFLAGS),$(WARN_CFLAGS)) $(CFLAGS) \ @@ -745,10 +748,18 @@ ifeq ($(XCONFIGURE),android) ## is not dumped here. Instead, it dumps itself the first time it ## starts on the user's device. +# Include ndk-build.mk in order to build Emacs dependencies. +old_top_builddir := $(top_builddir) +top_builddir := $(old_top_builddir)/.. +include $(old_top_builddir)/ndk-build/ndk-build.mk +top_builddir := $(old_top_builddir) + libemacs.so: $(ALLOBJS) $(LIBEGNU_ARCHIVE) $(EMACSRES) \ - $(MAKE_PDUMPER_FINGERPRINT) + $(MAKE_PDUMPER_FINGERPRINT) $(NDK_BUILD_SHARED) \ + $(NDK_BUILD_STATIC) $(AM_V_CCLD)$(CC) -o $@ $(ALL_CFLAGS) $(TEMACS_LDFLAGS) \ - $(LDFLAGS) -shared $(ALLOBJS) $(LIBEGNU_ARCHIVE) $(LIBES) + $(ANDROID_LDFLAGS) $(LDFLAGS) -shared $(ALLOBJS) \ + $(LIBEGNU_ARCHIVE) $(LIBES) $(AM_V_at)$(MAKE_PDUMPER_FINGERPRINT) $@ # There is also a binary named `android-emacs' which simply calls diff --git a/src/android.c b/src/android.c index a51d0518484..d788c6f19b4 100644 --- a/src/android.c +++ b/src/android.c @@ -1086,6 +1086,39 @@ android_hack_asset_fd (AAsset *asset) return fd; } +/* Read two bytes from FD and see if they are ``PK'', denoting ZIP + archive compressed data. + + If they are not, rewind the file descriptor to offset 0. + + If either operation fails, return -1 and close FD. Else, value is + FD. */ + +static int +android_check_compressed_file (int fd) +{ + char bytes[2]; + + if (read (fd, bytes, 2) != 2) + goto lseek_back; + + if (bytes[0] != 'P' || bytes[1] != 'K') + goto lseek_back; + + /* This could be compressed data! */ + return -1; + + lseek_back: + /* Seek back to offset 0. If this fails, return -1. */ + if (lseek (fd, 0, SEEK_SET) != 0) + { + close (fd); + return -1; + } + + return fd; +} + /* `open' and such are modified even though they exist on Android, because Emacs treats "/assets/" as a special directory that must contain all assets in the application package. */ @@ -1095,7 +1128,7 @@ android_open (const char *filename, int oflag, int mode) { const char *name; AAsset *asset; - int fd; + int fd, oldfd; off_t out_start, out_length; bool fd_hacked; @@ -1118,8 +1151,12 @@ android_open (const char *filename, int oflag, int mode) return -1; } + /* If given AASSET_MODE_BUFFER (which is what Emacs probably + does, given that a file descriptor is not always available), + the framework fails to uncompress the data before it returns + a file descriptor. */ asset = AAssetManager_open (asset_manager, name, - AASSET_MODE_BUFFER); + AASSET_MODE_STREAMING); if (!asset) { @@ -1132,6 +1169,11 @@ android_open (const char *filename, int oflag, int mode) fd = AAsset_openFileDescriptor (asset, &out_start, &out_length); + /* The platform sometimes returns a file descriptor to ZIP + compressed data. Detect that and fall back to creating a + shared memory file descriptor. */ + fd = android_check_compressed_file (fd); + if (fd == -1) { /* The asset can't be accessed for some reason. Try to @@ -1152,7 +1194,13 @@ android_open (const char *filename, int oflag, int mode) which will close the original file descriptor. */ if (!fd_hacked) - fd = fcntl (fd, F_DUPFD_CLOEXEC); + { + oldfd = fd; + fd = fcntl (oldfd, F_DUPFD_CLOEXEC); + + /* Close the original file descriptor. */ + close (oldfd); + } if (fd >= ANDROID_MAX_ASSET_FD || fd < 0) { @@ -1978,6 +2026,25 @@ NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, return event_serial; } +extern JNIEXPORT jlong JNICALL +NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, + jshort window, jint x, jint y, + jint width, jint height) +{ + union android_event event; + + event.xexpose.type = ANDROID_EXPOSE; + event.xexpose.serial = ++event_serial; + event.xexpose.window = window; + event.xexpose.x = x; + event.xexpose.y = y; + event.xexpose.width = width; + event.xexpose.height = height; + + android_write_event (&event); + return event_serial; +} + #ifdef __clang__ #pragma clang diagnostic pop #else @@ -4222,7 +4289,14 @@ android_readdir (struct android_dir *dir) dirent.d_ino = 0; dirent.d_off = 0; dirent.d_reclen = sizeof dirent; - dirent.d_type = DT_UNKNOWN; + + /* If this is not a directory, return DT_UNKNOWN. Otherwise, + return DT_DIR. */ + + if (android_is_directory (dir->asset_dir)) + dirent.d_type = DT_DIR; + else + dirent.d_type = DT_UNKNOWN; /* Note that dir->asset_dir is actually a NULL terminated string. */ diff --git a/src/androidgui.h b/src/androidgui.h index 3b9a74dc0b0..619c5e44f15 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -234,6 +234,7 @@ enum android_event_type ANDROID_ICONIFIED, ANDROID_DEICONIFIED, ANDROID_CONTEXT_MENU, + ANDROID_EXPOSE, }; struct android_any_event @@ -332,6 +333,15 @@ struct android_button_event unsigned int button; }; +struct android_expose_event +{ + enum android_event_type type; + unsigned long serial; + android_window window; + int x, y; + int width, height; +}; + struct android_touch_event { /* Type of the event. */ @@ -415,6 +425,7 @@ union android_event struct android_crossing_event xcrossing; struct android_motion_event xmotion; struct android_button_event xbutton; + struct android_expose_event xexpose; /* This has no parallel in X, since the X model of having monotonically increasing touch IDs can't work on Android. */ diff --git a/src/androidselect.c b/src/androidselect.c new file mode 100644 index 00000000000..4a71b376e28 --- /dev/null +++ b/src/androidselect.c @@ -0,0 +1,249 @@ +/* Communication module for Android terminals. + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +#include +#include + +#include "lisp.h" +#include "blockinput.h" +#include "coding.h" +#include "android.h" +#include "androidterm.h" + +/* Selection support on Android is confined to copying and pasting of + plain text from the clipboard. There is no primary selection. + + While newer versions of Android are supposed to have the necessary + interfaces for transferring other kinds of selection data, doing so + is too complicated, and involves registering ``content providers'' + and all kinds of other stuff. */ + + + +/* Structure describing the EmacsClipboard class. */ + +struct android_emacs_clipboard +{ + jclass class; + jmethodID set_clipboard; + jmethodID owns_clipboard; + jmethodID clipboard_exists; + jmethodID get_clipboard; + jmethodID make_clipboard; +}; + +/* Methods associated with the EmacsClipboard class. */ +static struct android_emacs_clipboard clipboard_class; + +/* Reference to the EmacsClipboard object. */ +static jobject clipboard; + + + +static void +android_init_emacs_clipboard (void) +{ + jclass old; + + clipboard_class.class + = (*android_java_env)->FindClass (android_java_env, + "org/gnu/emacs/EmacsClipboard"); + eassert (clipboard_class.class); + + old = clipboard_class.class; + clipboard_class.class + = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, + old); + ANDROID_DELETE_LOCAL_REF (old); + + if (!clipboard_class.class) + emacs_abort (); + +#define FIND_METHOD(c_name, name, signature) \ + clipboard_class.c_name \ + = (*android_java_env)->GetMethodID (android_java_env, \ + clipboard_class.class, \ + name, signature); \ + assert (clipboard_class.c_name); + + FIND_METHOD (set_clipboard, "setClipboard", "([B)V"); + FIND_METHOD (owns_clipboard, "ownsClipboard", "()I"); + FIND_METHOD (clipboard_exists, "clipboardExists", "()Z"); + FIND_METHOD (get_clipboard, "getClipboard", "()[B"); + + clipboard_class.make_clipboard + = (*android_java_env)->GetStaticMethodID (android_java_env, + clipboard_class.class, + "makeClipboard", + "()Lorg/gnu/emacs/" + "EmacsClipboard;"); + assert (clipboard_class.make_clipboard); + +#undef FIND_METHOD +} + + + + +DEFUN ("android-clipboard-owner-p", Fandroid_clipboard_owner_p, + Sandroid_clipboard_owner_p, 0, 0, 0, + doc: /* Return whether or not Emacs owns the clipboard. +Alternatively, return the symbol `lambda' if that could not be +determined. */) + (void) +{ + jint rc; + + block_input (); + rc = (*android_java_env)->CallIntMethod (android_java_env, + clipboard, + clipboard_class.owns_clipboard); + android_exception_check (); + unblock_input (); + + /* If rc is 0 or 1, then Emacs knows whether or not it owns the + clipboard. If rc is -1, then Emacs does not. */ + + if (rc < 0) + return Qlambda; + + return rc ? Qt : Qnil; +} + +DEFUN ("android-set-clipboard", Fandroid_set_clipboard, + Sandroid_set_clipboard, 1, 1, 0, + doc: /* Set the clipboard text to STRING. */) + (Lisp_Object string) +{ + jarray bytes; + + CHECK_STRING (string); + string = ENCODE_UTF_8 (string); + + bytes = (*android_java_env)->NewByteArray (android_java_env, + SBYTES (string)); + android_exception_check (); + + (*android_java_env)->SetByteArrayRegion (android_java_env, bytes, + 0, SBYTES (string), + (jbyte *) SDATA (string)); + (*android_java_env)->CallVoidMethod (android_java_env, + clipboard, + clipboard_class.set_clipboard, + bytes); + android_exception_check (); + + ANDROID_DELETE_LOCAL_REF (bytes); + return Qnil; +} + +DEFUN ("android-get-clipboard", Fandroid_get_clipboard, + Sandroid_get_clipboard, 0, 0, 0, + doc: /* Return the current contents of the clipboard. +Value is a multibyte string containing decoded clipboard +text. +Alternatively, return nil if the clipboard is empty. */) + (void) +{ + Lisp_Object string; + jarray bytes; + jmethodID method; + size_t length; + jbyte *data; + + method = clipboard_class.get_clipboard; + bytes + = (*android_java_env)->CallObjectMethod (android_java_env, + clipboard, + method); + + if (!bytes) + { + android_exception_check (); + return Qnil; + } + + length = (*android_java_env)->GetArrayLength (android_java_env, + bytes); + data = (*android_java_env)->GetByteArrayElements (android_java_env, + bytes, NULL); + android_exception_check (); + + string = make_unibyte_string ((char *) data, length); + + (*android_java_env)->ReleaseByteArrayElements (android_java_env, + bytes, data, + JNI_ABORT); + ANDROID_DELETE_LOCAL_REF (bytes); + + /* Now decode the resulting string. */ + return code_convert_string_norecord (string, Qutf_8, Qnil); +} + +DEFUN ("android-clipboard-exists-p", Fandroid_clipboard_exists_p, + Sandroid_clipboard_exists_p, 0, 0, 0, + doc: /* Return whether or not clipboard contents exist. */) + (void) +{ + jboolean rc; + jmethodID method; + + method = clipboard_class.clipboard_exists; + rc = (*android_java_env)->CallBooleanMethod (android_java_env, + clipboard, + method); + android_exception_check (); + + return rc ? Qt : Qnil; +} + + + +void +init_androidselect (void) +{ + jobject tem; + jmethodID make_clipboard; + + android_init_emacs_clipboard (); + + make_clipboard = clipboard_class.make_clipboard; + tem + = (*android_java_env)->CallStaticObjectMethod (android_java_env, + clipboard_class.class, + make_clipboard); + if (!tem) + emacs_abort (); + + clipboard = (*android_java_env)->NewGlobalRef (android_java_env, tem); + + if (!clipboard) + emacs_abort (); + + ANDROID_DELETE_LOCAL_REF (tem); +} + +void +syms_of_androidselect (void) +{ + defsubr (&Sandroid_clipboard_owner_p); + defsubr (&Sandroid_set_clipboard); + defsubr (&Sandroid_get_clipboard); + defsubr (&Sandroid_clipboard_exists_p); +} diff --git a/src/androidterm.c b/src/androidterm.c index 1141f50f266..d5dafe9b0d7 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -818,6 +818,28 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto OTHER; + case ANDROID_EXPOSE: + + f = any; + + if (f) + { + if (!FRAME_VISIBLE_P (f)) + { + f->output_data.android->has_been_visible = true; + SET_FRAME_GARBAGED (f); + } + + if (!FRAME_GARBAGED_P (f)) + { + expose_frame (f, event->xexpose.x, event->xexpose.y, + event->xexpose.width, event->xexpose.height); + show_back_buffer (f); + } + } + + goto OTHER; + case ANDROID_BUTTON_PRESS: case ANDROID_BUTTON_RELEASE: /* If we decide we want to generate an event to be seen diff --git a/src/androidterm.h b/src/androidterm.h index 11b24d40b73..8cc31f1ab57 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -432,6 +432,15 @@ extern void sfntfont_android_shrink_scanline_buffer (void); extern void init_sfntfont_android (void); extern void syms_of_sfntfont_android (void); +/* Defined in androidselect.c */ + +#ifndef ANDROID_STUBIFY + +extern void init_androidselect (void); +extern void syms_of_androidselect (void); + +#endif + #define RGB_TO_ULONG(r, g, b) (((r) << 16) | ((g) << 8) | (b)) diff --git a/src/emacs.c b/src/emacs.c index 994a4d1db93..02016e53c45 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -2409,6 +2409,7 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem syms_of_fontset (); #if !defined ANDROID_STUBIFY syms_of_androidfont (); + syms_of_androidselect (); syms_of_sfntfont (); syms_of_sfntfont_android (); #endif /* !ANDROID_STUBIFY */ @@ -2514,6 +2515,7 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY init_androidfont (); + init_androidselect (); init_sfntfont (); init_sfntfont_android (); #endif -- cgit v1.3 From 56e55a80080f78754db6f385d574d17c3631ee30 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 24 Jan 2023 17:31:16 +0800 Subject: Update Android port * INSTALL.android: Update. * build-aux/ndk-build-helper-1.mk: Fix typo. * configure.ac: Enable --with-json on Android. * cross/ndk-build/ndk-build-shared-library.mk: (NDK_CFLAGS_$(LOCAL_MODULE)): (LOCAL_MODULE_FILENAME): * cross/ndk-build/ndk-build-static-library.mk: (ALL_OBJECT_FILES$(LOCAL_MODULE)): (LOCAL_MODULE_FILENAME): Recursively resolve dependencies. * cross/ndk-build/ndk-resolve.mk: New function. * doc/emacs/android.texi (Android Startup): Document how Emacs is dumped during initial startup. * java/Makefile.in (filename): Fix build with multiple shared libraries. * java/README: Improve commentary. * java/org/gnu/emacs/EmacsApplication.java (onCreate): Look and set dump file. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function getFingerprint. * java/org/gnu/emacs/EmacsPreferencesActivity.java (onCreate): Add option to erase the dump file. * java/org/gnu/emacs/EmacsService.java (browseUrl): New function. * java/org/gnu/emacs/EmacsThread.java (run): Specify dump file if found. * lisp/loadup.el: Always dump during loadup on Android. * lisp/net/browse-url.el (browse-url--browser-defcustom-type): (browse-url-default-browser): (browse-url-default-android-browser): New browse url type. * m4/ndk-build.m4 (ndk_package_map): Map jansson to libjansson. * src/android.c (struct android_emacs_service): New method `browse_url'. (getFingerprint): New function. (android_init_emacs_service): Initialize new method. (android_browse_url): New function. * src/android.h: Update prototypes. * src/androidselect.c (Fandroid_browse_url): New function. (syms_of_androidselect): Define it. * src/emacs.c (load_pdump): Don't look in fancy places on Android. * src/pdumper.c (Fdump_emacs_portable): Allow dumping while interactive on Android. (syms_of_pdumper): New variable `pdumper-fingerprint'. * src/sfntfont-android.c (sfntfont_android_composite_bitmap): Fix unused variables. --- INSTALL.android | 37 +++++- build-aux/ndk-build-helper-1.mk | 2 +- configure.ac | 3 +- cross/ndk-build/ndk-build-shared-library.mk | 23 +++- cross/ndk-build/ndk-build-static-library.mk | 14 ++- cross/ndk-build/ndk-resolve.mk | 47 ++++++++ doc/emacs/android.texi | 45 ++++--- java/Makefile.in | 3 +- java/README | 8 +- java/org/gnu/emacs/EmacsApplication.java | 54 ++++++++- java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsPreferencesActivity.java | 27 +++++ java/org/gnu/emacs/EmacsService.java | 25 ++++ java/org/gnu/emacs/EmacsThread.java | 19 ++- lisp/loadup.el | 146 ++++++++++++++--------- lisp/net/browse-url.el | 22 ++++ m4/ndk-build.m4 | 2 +- src/android.c | 61 ++++++++++ src/android.h | 4 + src/androidselect.c | 20 ++++ src/emacs.c | 29 +++++ src/pdumper.c | 18 +++ src/sfntfont-android.c | 7 +- 23 files changed, 518 insertions(+), 102 deletions(-) create mode 100644 cross/ndk-build/ndk-resolve.mk (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/INSTALL.android b/INSTALL.android index 9abea74a55d..79366425125 100644 --- a/INSTALL.android +++ b/INSTALL.android @@ -148,6 +148,14 @@ work: (You must add LOCAL_EXPORT_CFLAGS := -I$(LOCAL_PATH) before its Android.mk includes $(BUILD_STATIC_LIBRARY)) +In addition, some Emacs dependencies provide `ndk-build' support +themselves: + + libjansson - https://github.com/akheron/jansson + (You must add LOCAL_EXPORT_INCLUDES := $(LOCAL_C_INCLUDES) before + its Android.mk includes $(BUILD_SHARED_LIBRARY), then copy + android/jansson_config.h to android/jansson_private_config.h.) + We anticipate that most untested non-trivial ndk-build dependencies will need adjustments in Emacs to work, as the Emacs build system which emulates ndk-build is in an extremely early state. @@ -173,7 +181,7 @@ one awk script in build-awx, run during configure: build-aux/ndk-module-extract.awk -six Makefiles in cross/ndk-build, +seven Makefiles in cross/ndk-build, cross/ndk-build/ndk-build-shared-library.mk cross/ndk-build/ndk-build-static-library.mk @@ -181,6 +189,7 @@ six Makefiles in cross/ndk-build, cross/ndk-build/ndk-clear-vars.mk cross/ndk-build/ndk-prebuilt-shared-library.mk cross/ndk-build/ndk-prebuilt-static-library.mk + cross/ndk-build/ndk-resolve.mk and finally, two more Makefiles in cross/ndk-build, generated by configure: @@ -373,11 +382,27 @@ them. The name under which the module is linked is the same as the Make target found on the sixth line of output from build-aux/ndk-build-helper.mk. -However, none of the Makefiles in -cross/ndk-build/ndk-build-shared-library.mk perform any kind of -dependency resolution! Instead, they only define rules to build -individual modules, leaving dependency resolution up to the Makefiles -in the `build-aux' directory. +While the rules defined by the Makefiles in cross/ndk-build do not +have their dependencies as prerequisites (with the assumption that the +ndk-build.m4 and the Makefiles in build-aux have already added all of +the necessary targets to ndk-build.mk), dependency resolution is still +performed, as the CFLAGS and includes from dependencies must be +appended to the module's CFLAGS. + +This is done by including cross/ndk-build/ndk-resolve.mk each time a +shared or static library module is going to be built. How is this +done? + +First, ndk-resolve.mk saves the LOCAL_PATH, LOCAL_STATIC_LIBRARIES, +LOCAL_SHARED_LIBRARIES, LOCAL_EXPORT_CFLAGS and +LOCAL_EXPORT_C_INCLUDES from the module. + +Next, ndk-resolve loops through the dependencies the module has +specified, appending its CFLAGS and includes to the command line for +the current module. + +Then, that process is repeated for each such dependency which has not +already been resolved, until all dependencies have been resolved. libpng is a very simple module, providing only a single shared object module. This module is named libpng_emacs.so and is eventually built diff --git a/build-aux/ndk-build-helper-1.mk b/build-aux/ndk-build-helper-1.mk index fb8e4da2f0a..d20f7d4472f 100644 --- a/build-aux/ndk-build-helper-1.mk +++ b/build-aux/ndk-build-helper-1.mk @@ -33,7 +33,7 @@ NDK_$(LOCAL_MODULE)_SHARED_LIBRARIES := $(LOCAL_SHARED_LIBRARIES) $(info Building $(build_kind)) $(info $(LOCAL_MODULE)) -$(info $(addprefix $(ANDROID_MODULE_DIRECTORY)?,$(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES$(EMACS_ABI)))) +$(info $(addprefix $(ANDROID_MODULE_DIRECTORY)/,$(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES$(EMACS_ABI)))) $(info $(foreach dir,$(LOCAL_EXPORT_C_INCLUDE_DIRS) $(LOCAL_EXPORT_C_INCLUDES),-I$(dir))) $(info $(LOCAL_EXPORT_CFLAGS)) diff --git a/configure.ac b/configure.ac index 9df4a6a073f..f3a4b4301a4 100644 --- a/configure.ac +++ b/configure.ac @@ -1045,6 +1045,7 @@ package will likely install on older systems but crash on startup.]) passthrough="$passthrough --with-png=$with_png" passthrough="$passthrough --with-webp=$with_webp" passthrough="$passthrough --with-gif=$with_gif" + passthrough="$passthrough --with-json=$with_json" AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" \ ANDROID_SDK="$android_sdk" android_abi=$android_abi \ @@ -1112,6 +1113,7 @@ if test "$ANDROID" = "yes"; then with_png=no with_webp=no with_gif=no + with_json=no fi with_xml2=no @@ -1121,7 +1123,6 @@ if test "$ANDROID" = "yes"; then with_libsystemd=no with_cairo=no with_imagemagick=no - with_json=no with_tree_sitter=no with_xft=no with_harfbuzz=no diff --git a/cross/ndk-build/ndk-build-shared-library.mk b/cross/ndk-build/ndk-build-shared-library.mk index 0d90d89e55d..c4fb523a0c1 100644 --- a/cross/ndk-build/ndk-build-shared-library.mk +++ b/cross/ndk-build/ndk-build-shared-library.mk @@ -44,11 +44,13 @@ ALL_OBJECT_FILES$(LOCAL_MODULE) += $(call objname,$(LOCAL_MODULE),$(basename $(1 endef -NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(addprefix $(LOCAL_PATH),$(LOCAL_C_INCLUDES))) -NDK_CFLAGS_$(LOCAL_MODULE) ::= -fPIC -iquote $(LOCAL_EXPORT_CFLAGS) $(LOCAL_PATH) $(LOCAL_CFLAGS) -NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) -NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDFLAGS) -ALL_OBJECT_FILES_$(LOCAL_MODULE) := +# Make sure to not add a prefix to local includes that already specify +# $(LOCAL_PATH). +NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(LOCAL_C_INCLUDES)) +NDK_CFLAGS_$(LOCAL_MODULE) += -fPIC -iquote $(LOCAL_PATH) $(LOCAL_EXPORT_CFLAGS) $(LOCAL_CFLAGS) $(LOCAL_CFLAGS$(NDK_BUILD_ARCH)) +NDK_ASFLAGS_$(LOCAL_MODULE) := $(LOCAL_ASFLAGS) $(LOCAL_ASFLAGS_$(NDK_BUILD_ARCH)) +NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) $(LOCAL_LDFLAGS) +ALL_OBJECT_FILES$(LOCAL_MODULE) := ifeq ($(NDK_BUILD_ARCH)$(NDK_ARM_MODE),armarm) NDK_CFLAGS ::= -marm @@ -74,8 +76,17 @@ endif LOCAL_MODULE_FILENAME := $(LOCAL_MODULE_FILENAME).so +# Record this module's dependencies and exported includes and CFLAGS, +# and then add that of its dependencies. + +include ndk-resolve.mk + # Then define rules to build all objects. -ALL_SOURCE_FILES = $(LOCAL_SRC_FILES) +ALL_SOURCE_FILES = $(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES_$(NDK_BUILD_ARCH)) + +# This defines all dependencies. +ALL_OBJECT_FILES$(LOCAL_MODULE) = + $(foreach source,$(ALL_SOURCE_FILES),$(eval $(call single-object-target,$(source)))) # Now define the rule to build the shared library. diff --git a/cross/ndk-build/ndk-build-static-library.mk b/cross/ndk-build/ndk-build-static-library.mk index 1423cf243cf..2eddb215429 100644 --- a/cross/ndk-build/ndk-build-static-library.mk +++ b/cross/ndk-build/ndk-build-static-library.mk @@ -43,11 +43,10 @@ endif ALL_OBJECT_FILES$(LOCAL_MODULE) += $(call objname,$(LOCAL_MODULE),$(basename $(1))) endef -NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(addprefix $(LOCAL_PATH),$(LOCAL_C_INCLUDES))) -NDK_CFLAGS_$(LOCAL_MODULE) ::= -iquote $(LOCAL_PATH) $(LOCAL_EXPORT_CFLAGS) $(LOCAL_CFLAGS) $(LOCAL_CFLAGS_$(NDK_BUILD_ARCH)) -NDK_ASFLAGS_$(LOCAL_MODULE) ::= $(LOCAL_ASFLAGS) $(LOCAL_ASFLAGS_$(NDK_BUILD_ARCH)) -NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) -NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDFLAGS) +NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(LOCAL_C_INCLUDES)) +NDK_CFLAGS_$(LOCAL_MODULE) += -iquote $(LOCAL_PATH) $(LOCAL_EXPORT_CFLAGS) $(LOCAL_CFLAGS) $(LOCAL_CFLAGS_$(NDK_BUILD_ARCH)) +NDK_ASFLAGS_$(LOCAL_MODULE) := $(LOCAL_ASFLAGS) $(LOCAL_ASFLAGS_$(NDK_BUILD_ARCH)) +NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) $(LOCAL_LDFLAGS) ALL_OBJECT_FILES$(LOCAL_MODULE) := ifeq ($(NDK_BUILD_ARCH)$(NDK_ARM_MODE),armarm) @@ -70,6 +69,11 @@ endif LOCAL_MODULE_FILENAME := $(LOCAL_MODULE_FILENAME).a +# Record this module's dependencies and exported includes and CFLAGS, +# and then add that of its dependencies. + +include ndk-resolve.mk + # Then define rules to build all objects. ALL_SOURCE_FILES = $(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES_$(NDK_BUILD_ARCH)) diff --git a/cross/ndk-build/ndk-resolve.mk b/cross/ndk-build/ndk-resolve.mk new file mode 100644 index 00000000000..29dd5c9908d --- /dev/null +++ b/cross/ndk-build/ndk-resolve.mk @@ -0,0 +1,47 @@ +# Copyright 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# ndk-build works by including a bunch of Makefiles which set +# variables, and then having those Makefiles include another makefile +# which actually builds targets. + +# Save information. +NDK_LOCAL_PATH_$(LOCAL_MODULE) := $(LOCAL_PATH) +NDK_LOCAL_STATIC_LIBRARIES_$(LOCAL_MODULE) := $(LOCAL_STATIC_LIBRARIES) $(LOCAL_WHOLE_STATIC_LIBRARIES) +NDK_LOCAL_SHARED_LIBRARIES_$(LOCAL_MODULE) := $(LOCAL_SHARED_LIBRARIES) +NDK_LOCAL_EXPORT_CFLAGS_$(LOCAL_MODULE) := $(LOCAL_EXPORT_CFLAGS) +NDK_LOCAL_EXPORT_C_INCLUDES_$(LOCAL_MODULE) := $(LOCAL_EXPORT_C_INCLUDES) + +# List of all dependencies resolved for this module thus far. +# Used to avoid infinite recursion. +NDK_RESOLVED_$(LOCAL_MODULE) := + +define ndk-resolve + +ifeq ($(patsubst $(1),,$(NDK_RESOLVED$(LOCAL_MODULE))),$(NDK_RESOLVED$(LOCAL_MODULE))) +NDK_RESOLVED$(LOCAL_MODULE) += $(1) +NDK_CFLAGS_$(LOCAL_MODULE) += $(NDK_LOCAL_EXPORT_CFLAGS_$(1)) +NDK_CFLAGS_$(LOCAL_MODULE) += $(addprefix -I,$(NDK_LOCAL_EXPORT_C_INCLUDES_$(1))) + +$$(foreach module,$$(NDK_LOCAL_STATIC_LIBRARIES_$(1)),$$(eval $$(call ndk-resolve,$$(module)))) +$$(foreach module,$$(NDK_LOCAL_SHARED_LIBRARIES_$(1)),$$(eval $$(call ndk-resolve,$$(module)))) +endif + +endef + +$(foreach module,$(LOCAL_SHARED_LIBRARIES),$(eval $(call ndk-resolve,$(module)))) +$(foreach module,$(LOCAL_STATIC_LIBRARIES) $(LOCAL_WHOLE_STATIC_LIBRARIES),$(eval $(call ndk-resolve,$(module)))) diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index e4498da401d..cda0eadf226 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -63,18 +63,18 @@ package manager. Instead, Emacs is compiled for Android on a different operating system, with the resulting binaries packaged into an archive, that is then transferred to the device and unpacked. -After being unpacked, Emacs instructs the system to display an + After being unpacked, Emacs instructs the system to display an application icon on the desktop. Emacs then starts up once the application icon is clicked. @cindex ``adb logcat'' -During startup, Emacs will display messages in the system log buffer; -reading that buffer requires the Android Debug Bridge (@code{adb}) -utility to be installed on another computer; it cannot be read on the -computer running Android itself. + During startup, Emacs will display messages in the system log +buffer; reading that buffer requires the Android Debug Bridge +(@code{adb}) utility to be installed on another computer; it cannot be +read on the computer running Android itself. -After enabling the ``USB Debugging'' feature on the Android system, + After enabling the ``USB Debugging'' feature on the Android system, and connecting it via USB to another system with the @code{adb} utility installed, the log can be viewed by running the following command on that other system: @@ -84,14 +84,31 @@ $ adb logcat | grep -E "(android_run_debug_thread|[Ee]macs)" @end example @cindex emacs -Q, android -Since Android has no command line, there is normally no way to specify -command-line arguments. However, Emacs can be started with the -equivalent of the @code{--quick} option (@pxref{Initial Options}) -through a special preferences screen, which can be accessed through -the Emacs ``app info'' page in the system settings application. - -Consult the manufacturer of your device for more details, as how to do -this varies by device. + Since Android has no command line, there is normally no way to +specify command-line arguments when starting Emacs. However, Emacs +can be started with the equivalent of the @code{--quick} option +(@pxref{Initial Options}) through a special preferences screen, which +can be accessed through the Emacs ``app info'' page in the system +settings application. + + Consult the manufacturer of your device for more details, as how to +do this varies by device. + +@cindex dumping, android + The first time any given copy of Emacs starts on a device, it spends +a while loading the preloaded Lisp files which normally come with +Emacs. This produces a ``dump file'' (@pxref{Initial Options}) in the +files directory, containing an identifier unique to this copy of +Emacs. + + The next time that same copy of Emacs starts up, it simply loads the +preloaded Lisp files contained in that dump file, greatly improving +start up time. + + However, if by some unforseen circumstance the dump file is +corrupted, Emacs can crash. If that happens, the dump file stored in +the Emacs files directory can be erased through the same preferences +screen. @node Android File System @section What files Emacs can access under Android diff --git a/java/Makefile.in b/java/Makefile.in index d27775ea3db..b5e0cd7bb56 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -148,8 +148,7 @@ emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) $(libsrc)/asset-directory-tool \ cp -f $$file install_temp/lib/$(ANDROID_ABI); \ fi \ done - $(foreach module,$(NDK_BUILD_SHARED), \ - cp -f $(module) install_temp/lib/$(ANDROID_ABI)) + cp -f $(NDK_BUILD_SHARED) install_temp/lib/$(ANDROID_ABI) # Package everything. Specifying the assets on this command line is # necessary for AAssetManager_getNextFileName to work on old versions # of Android. diff --git a/java/README b/java/README index 50c2332ce95..05edf7744de 100644 --- a/java/README +++ b/java/README @@ -1,5 +1,11 @@ This directory holds the Java sources of the port of GNU Emacs to -Android-like systems. +Android-like systems, along with files needed to create an application +package out of them. + +`emacs.keystore' is the signing key used to build Emacs. It is kept +here, and we encourage all people redistributing Emacs to use this +key. It holds no security value, and otherwise it will be impossible +to install different builds of Emacs on top of each other. Please keep the Java code indented with tabs and formatted according to the rules for C code in the GNU coding standards. Always use diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index 125da05cfd4..87085c32d62 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -19,9 +19,59 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import java.io.File; +import java.io.FileFilter; + import android.app.Application; +import android.util.Log; -public class EmacsApplication extends Application +public class EmacsApplication extends Application implements FileFilter { - /* This class currently does nothing. */ + private static final String TAG = "EmacsApplication"; + + /* The name of the dump file to use. */ + public static String dumpFileName; + + @Override + public boolean + accept (File file) + { + return (!file.isDirectory () + && file.getName ().endsWith (".pdmp")); + } + + @Override + public void + onCreate () + { + File filesDirectory; + File[] allFiles; + String wantedDumpFile; + int i; + + wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () + + ".pdmp"); + + Log.d (TAG, "onCreate: looking for " + wantedDumpFile); + + /* Obtain a list of all files ending with ``.pdmp''. Then, look + for a file named ``emacs-.pdmp'' and delete the + rest. */ + filesDirectory = getFilesDir (); + allFiles = filesDirectory.listFiles (this); + + /* Now try to find the right dump file. */ + for (i = 0; i < allFiles.length; ++i) + { + if (allFiles[i].getName ().equals (wantedDumpFile)) + dumpFileName = allFiles[i].getAbsolutePath (); + else + /* Delete this outdated dump file. */ + allFiles[i].delete (); + } + + Log.d (TAG, "onCreate: found " + dumpFileName); + + super.onCreate (); + } }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 962538bef7b..9636561a524 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,6 +25,10 @@ import android.content.res.AssetManager; public class EmacsNative { + /* Obtain the fingerprint of this build of Emacs. The fingerprint + can be used to determine the dump file name. */ + public static native String getFingerprint (); + /* Set certain parameters before initializing Emacs. This proves that libemacs.so is being loaded from Java code. diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index 0db983984fd..ed1db68f732 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -19,6 +19,8 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import java.io.File; + import android.app.Activity; import android.content.Intent; import android.os.Bundle; @@ -93,6 +95,31 @@ public class EmacsPreferencesActivity extends Activity }); layout.addView (textView); + textView = new TextView (this); + textView.setPadding (8, 20, 20, 8); + + params = new LinearLayout.LayoutParams (LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + textView.setLayoutParams (params); + textView.setText ("Erase dump file"); + textView.setOnClickListener (new View.OnClickListener () { + @Override + public void + onClick (View view) + { + String wantedDumpFile; + File file; + + wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () + + ".pdmp"); + file = new File (getFilesDir (), wantedDumpFile); + + if (file.exists ()) + file.delete (); + } + }); + layout.addView (textView); + super.onCreate (savedInstanceState); } }; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 95f21b211a3..4db1ea5359f 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -43,6 +43,8 @@ import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; +import android.net.Uri; + import android.os.Build; import android.os.Looper; import android.os.IBinder; @@ -504,4 +506,27 @@ public class EmacsService extends Service EmacsService.class)); } } + + /* Ask the system to open the specified URL. + Value is NULL upon success, or a string describing the error + upon failure. */ + + public String + browseUrl (String url) + { + Intent intent; + + try + { + intent = new Intent (Intent.ACTION_VIEW, Uri.parse (url)); + intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity (intent); + } + catch (Exception e) + { + return e.toString (); + } + + return null; + } }; diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 21d8612703a..5b76d11db4b 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -38,10 +38,23 @@ public class EmacsThread extends Thread { String args[]; - if (!startDashQ) - args = new String[] { "libandroid-emacs.so", }; + if (EmacsApplication.dumpFileName == null) + { + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", }; + else + args = new String[] { "libandroid-emacs.so", "-Q", }; + } else - args = new String[] { "libandroid-emacs.so", "-Q", }; + { + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", "--dump-file", + EmacsApplication.dumpFileName, }; + else + args = new String[] { "libandroid-emacs.so", "-Q", + "--dump-file", + EmacsApplication.dumpFileName, }; + } /* Run the native code now. */ EmacsNative.initEmacs (args); diff --git a/lisp/loadup.el b/lisp/loadup.el index b9ae8212d2d..8d2e4bbb7c2 100644 --- a/lisp/loadup.el +++ b/lisp/loadup.el @@ -550,66 +550,96 @@ lost after dumping"))) -(if dump-mode - (let ((output (cond ((equal dump-mode "pdump") "emacs.pdmp") - ((equal dump-mode "dump") "emacs") - ((equal dump-mode "bootstrap") "emacs") - ((equal dump-mode "pbootstrap") "bootstrap-emacs.pdmp") - (t (error "Unrecognized dump mode %s" dump-mode))))) - (when (and (featurep 'native-compile) - (equal dump-mode "pdump")) - ;; Don't enable this before bootstrap is completed, as the - ;; compiler infrastructure may not be usable yet. - (setq comp-enable-subr-trampolines t)) - (message "Dumping under the name %s" output) - (condition-case () - (delete-file output) - (file-error nil)) - ;; On MS-Windows, the current directory is not necessarily the - ;; same as invocation-directory. - (let (success) - (unwind-protect - (let ((tmp-dump-mode dump-mode) - (dump-mode nil) - (lexical-binding nil)) - (if (member tmp-dump-mode '("pdump" "pbootstrap")) - (dump-emacs-portable (expand-file-name output invocation-directory)) - (dump-emacs output (if (eq system-type 'ms-dos) - "temacs.exe" - "temacs")) - (message "%d pure bytes used" pure-bytes-used)) - (setq success t)) - (unless success - (ignore-errors - (delete-file output))))) - ;; Recompute NAME now, so that it isn't set when we dump. - (if (not (or (eq system-type 'ms-dos) - (eq system-type 'haiku) ;; BFS doesn't support hard links - ;; Don't bother adding another name if we're just - ;; building bootstrap-emacs. - (member dump-mode '("pbootstrap" "bootstrap")))) - (let ((name (format "emacs-%s.%d" emacs-version emacs-build-number)) - (exe (if (eq system-type 'windows-nt) ".exe" ""))) - (while (string-match "[^-+_.a-zA-Z0-9]+" name) - (setq name (concat (downcase (substring name 0 (match-beginning 0))) - "-" - (substring name (match-end 0))))) - (message "Adding name %s" (concat name exe)) - ;; When this runs on Windows, invocation-directory is not - ;; necessarily the current directory. - (add-name-to-file (expand-file-name (concat "emacs" exe) - invocation-directory) - (expand-file-name (concat name exe) - invocation-directory) - t) - (when (equal dump-mode "pdump") - (message "Adding name %s" (concat name ".pdmp")) - (add-name-to-file (expand-file-name "emacs.pdmp" +(if (eq system-type 'android) + (progn + ;; Dumping Emacs on Android works slightly differently from + ;; everywhere else. The first time Emacs starts, Emacs dumps + ;; itself to "emacs-%s.pdump", and then proceeds with loadup, + ;; where %s is replaced by the dump fingerprint. + ;; EmacsApplication.java removes any pdump files with a + ;; different build fingerprint upon being created, which happens + ;; the moment the Android system starts Emacs. Then, it passes + ;; the appropriate "--dump-file" to libemacs.so as it starts. + (let ((temp-dir (getenv "TEMP")) + (dump-file-name (format "%semacs-%s.pdmp" + (file-name-as-directory "~") + pdumper-fingerprint)) + (dump-temp-file-name (format "%s~emacs-%s.pdmp" + (file-name-as-directory "~") + pdumper-fingerprint))) + (unless (pdumper-stats) + (condition-case () + (progn + (dump-emacs-portable dump-temp-file-name) + ;; Move the dumped file to the actual dump file name. + (rename-file dump-temp-file-name dump-file-name) + ;; Continue with loadup. + nil) + (error nil))))) + (if dump-mode + (let ((output (cond ((equal dump-mode "pdump") "emacs.pdmp") + ((equal dump-mode "dump") "emacs") + ((equal dump-mode "bootstrap") "emacs") + ((equal dump-mode "pbootstrap") "bootstrap-emacs.pdmp") + (t (error "Unrecognized dump mode %s" dump-mode))))) + (when (and (featurep 'native-compile) + (equal dump-mode "pdump")) + ;; Don't enable this before bootstrap is completed, as the + ;; compiler infrastructure may not be usable yet. + (setq comp-enable-subr-trampolines t)) + (message "Dumping under the name %s" output) + (condition-case () + (delete-file output) + (file-error nil)) + ;; On MS-Windows, the current directory is not necessarily the + ;; same as invocation-directory. + (let (success) + (unwind-protect + (let ((tmp-dump-mode dump-mode) + (dump-mode nil) + (lexical-binding nil)) + (if (member tmp-dump-mode '("pdump" "pbootstrap")) + (dump-emacs-portable (expand-file-name output invocation-directory)) + (dump-emacs output (if (eq system-type 'ms-dos) + "temacs.exe" + "temacs")) + (message "%d pure bytes used" pure-bytes-used)) + (setq success t)) + (unless success + (ignore-errors + (delete-file output))))) + ;; Recompute NAME now, so that it isn't set when we dump. + (if (not (or (eq system-type 'ms-dos) + (eq system-type 'haiku) ;; BFS doesn't support hard links + ;; There's no point keeping old dumps around for + ;; the binary used to build Lisp on the build + ;; machine. + (featurep 'android) + ;; Don't bother adding another name if we're just + ;; building bootstrap-emacs. + (member dump-mode '("pbootstrap" "bootstrap")))) + (let ((name (format "emacs-%s.%d" emacs-version emacs-build-number)) + (exe (if (eq system-type 'windows-nt) ".exe" ""))) + (while (string-match "[^-+_.a-zA-Z0-9]+" name) + (setq name (concat (downcase (substring name 0 (match-beginning 0))) + "-" + (substring name (match-end 0))))) + (message "Adding name %s" (concat name exe)) + ;; When this runs on Windows, invocation-directory is not + ;; necessarily the current directory. + (add-name-to-file (expand-file-name (concat "emacs" exe) invocation-directory) - (expand-file-name (concat name ".pdmp") + (expand-file-name (concat name exe) invocation-directory) - t)))) - (kill-emacs))) + t) + (when (equal dump-mode "pdump") + (message "Adding name %s" (concat name ".pdmp")) + (add-name-to-file (expand-file-name "emacs.pdmp" + invocation-directory) + (expand-file-name (concat name ".pdmp") + invocation-directory) + t)))) + (kill-emacs)))) ;; This file must be loaded each time Emacs is run from scratch, e.g., temacs. ;; So run the startup code now. First, remove `-l loadup' from args. diff --git a/lisp/net/browse-url.el b/lisp/net/browse-url.el index 0177d12f236..c2629b69d59 100644 --- a/lisp/net/browse-url.el +++ b/lisp/net/browse-url.el @@ -52,6 +52,7 @@ ;; browse-url-xdg-open freedesktop.org xdg-open ;; browse-url-kde KDE konqueror (kfm) ;; browse-url-elinks Elinks Don't know (tried with 0.12.GIT) +;; browse-url-default-android-browser Android 2.3.3 (should work on 2.2 too) ;; eww-browse-url Emacs Web Wowser ;; Browsers can cache web pages so it may be necessary to tell them to @@ -173,6 +174,9 @@ ,@(when (eq system-type 'darwin) (list '(function-item :tag "Default macOS browser" :value browse-url-default-macosx-browser))) + ,@(when (eq system-type 'android) + (list '(function-item :tag "Default Android browser" + :value browse-url-default-android-browser))) (function-item :tag "Default browser" :value browse-url-default-browser) (function :tag "Your own function") @@ -1057,6 +1061,8 @@ instead of `browse-url-new-window-flag'." 'browse-url-default-macosx-browser) ((featurep 'haiku) 'browse-url-default-haiku-browser) + ((eq system-type 'android) + 'browse-url-default-android-browser) ((browse-url-can-use-xdg-open) 'browse-url-xdg-open) ;;; ((executable-find browse-url-gnome-moz-program) 'browse-url-gnome-moz) ((executable-find browse-url-firefox-program) 'browse-url-firefox) @@ -1294,6 +1300,22 @@ Default to the URL around or before point." (function-put 'browse-url-default-haiku-browser 'browse-url-browser-kind 'external) +(declare-function android-browse-url "androidselect.c") + +;;;###autoload +(defun browse-url-default-android-browser (url &optional _new-window) + "Browse URL with the system default browser. +Default to the URL around or before point." + (interactive (browse-url-interactive-arg "URL: ")) + (setq url (browse-url-encode-url url)) + ;; Make sure the URL starts with an appropriate scheme. + (unless (string-match "\\(.+\\):/" url) + (setq url (concat "http://" url))) + (android-browse-url url)) + +(function-put 'browse-url-default-android-browser + 'browse-url-browser-kind 'external) + ;;;###autoload (defun browse-url-emacs (url &optional same-window) "Ask Emacs to load URL into a buffer and show it in another window. diff --git a/m4/ndk-build.m4 b/m4/ndk-build.m4 index a35900884d9..d3614a9f490 100644 --- a/m4/ndk-build.m4 +++ b/m4/ndk-build.m4 @@ -67,7 +67,7 @@ esac # This is a map between pkg-config style package names and Android # ones. -ndk_package_map="libwebpdemux:webpdemux libxml-2.0:libxml2" +ndk_package_map="libwebpdemux:webpdemux libxml-2.0:libxml2 jansson:libjansson" # Replace ndk_module with the appropriate Android module name if it is # found in ndk_package_map. diff --git a/src/android.c b/src/android.c index d788c6f19b4..c186b462360 100644 --- a/src/android.c +++ b/src/android.c @@ -33,6 +33,7 @@ along with GNU Emacs. If not, see . */ #include #include +#include #include "android.h" #include "androidgui.h" @@ -95,6 +96,7 @@ struct android_emacs_service jmethodID detect_mouse; jmethodID name_keysym; jmethodID sync; + jmethodID browse_url; }; struct android_emacs_pixmap @@ -1294,6 +1296,18 @@ android_get_home_directory (void) #pragma GCC diagnostic ignored "-Wmissing-prototypes" #endif +JNIEXPORT jstring JNICALL +NATIVE_NAME (getFingerprint) (JNIEnv *env, jobject object) +{ + char buffer[sizeof fingerprint * 2 + 1]; + + memset (buffer, 0, sizeof buffer); + hexbuf_digest (buffer, (char *) fingerprint, + sizeof fingerprint); + + return (*env)->NewStringUTF (env, buffer); +} + JNIEXPORT void JNICALL NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject local_asset_manager, @@ -1514,6 +1528,8 @@ android_init_emacs_service (void) FIND_METHOD (detect_mouse, "detectMouse", "()Z"); FIND_METHOD (name_keysym, "nameKeysym", "(I)Ljava/lang/String;"); FIND_METHOD (sync, "sync", "()V"); + FIND_METHOD (browse_url, "browseUrl", "(Ljava/lang/String;)" + "Ljava/lang/String;"); #undef FIND_METHOD } @@ -4735,6 +4751,51 @@ android_project_image_nearest (struct android_image *image, +/* Other miscellaneous functions. */ + +/* Ask the system to start browsing the specified encoded URL. Upon + failure, return a string describing the error. Else, value is + nil. */ + +Lisp_Object +android_browse_url (Lisp_Object url) +{ + jobject value, string; + Lisp_Object tem; + const char *buffer; + + string = android_build_string (url); + value = (*android_java_env)->CallObjectMethod (android_java_env, + emacs_service, + service_class.browse_url, + string); + android_exception_check (); + + ANDROID_DELETE_LOCAL_REF (string); + + /* If no string was returned, return Qnil. */ + if (!value) + return Qnil; + + buffer = (*android_java_env)->GetStringUTFChars (android_java_env, + (jstring) value, + NULL); + android_exception_check (); + + /* Otherwise, build the string describing the error. */ + tem = build_string_from_utf8 (buffer); + + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + (jstring) value, + buffer); + + /* And return it. */ + ANDROID_DELETE_LOCAL_REF (value); + return tem; +} + + + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index eee3a7a6498..ce0ccfc4338 100644 --- a/src/android.h +++ b/src/android.h @@ -107,6 +107,10 @@ extern void android_closedir (struct android_dir *); +/* Very miscellaneous functions. */ + +extern Lisp_Object android_browse_url (Lisp_Object); + #endif /* JNI functions should not be built when Emacs is stubbed out for the diff --git a/src/androidselect.c b/src/androidselect.c index 4a71b376e28..2e2f2d7e483 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -213,6 +213,25 @@ DEFUN ("android-clipboard-exists-p", Fandroid_clipboard_exists_p, return rc ? Qt : Qnil; } +DEFUN ("android-browse-url", Fandroid_browse_url, + Sandroid_browse_url, 1, 1, 0, + doc: /* Start the system web browser. +Then, point the web browser to URL, which should be a URL-encoded +URL with a scheme specified. Signal an error upon failure. */) + (Lisp_Object url) +{ + Lisp_Object value; + + CHECK_STRING (url); + value = android_browse_url (url); + + /* Signal an error upon failure. */ + if (!NILP (value)) + signal_error ("Error browsing URL", value); + + return Qnil; +} + void @@ -246,4 +265,5 @@ syms_of_androidselect (void) defsubr (&Sandroid_set_clipboard); defsubr (&Sandroid_get_clipboard); defsubr (&Sandroid_clipboard_exists_p); + defsubr (&Sandroid_browse_url); } diff --git a/src/emacs.c b/src/emacs.c index 02016e53c45..b0c19fe0070 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -744,6 +744,8 @@ argmatch (char **argv, int argc, const char *sstr, const char *lstr, } } +#if !defined HAVE_ANDROID || defined ANDROID_STUBIFY + /* Find a name (absolute or relative) of the Emacs executable whose name (as passed into this program) is ARGV0. Called early in initialization by portable dumper loading code, so avoid Lisp and @@ -843,6 +845,8 @@ find_emacs_executable (char const *argv0, ptrdiff_t *candidate_size) #endif /* !WINDOWSNT */ } +#endif + #ifdef HAVE_PDUMPER static const char * @@ -875,6 +879,30 @@ dump_error_to_string (int result) static char * load_pdump (int argc, char **argv) { +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + char *dump_file = NULL; + int skip_args = 0, result; + + while (skip_args < argc - 1) + { + if (argmatch (argv, argc, "-dump-file", "--dump-file", 6, + &dump_file, &skip_args) + || argmatch (argv, argc, "--", NULL, 2, NULL, &skip_args)) + break; + skip_args++; + } + + if (!dump_file) + return argv[0]; + + result = pdumper_load (dump_file, argv[0]); + + if (result != PDUMPER_LOAD_SUCCESS) + fatal ("could not load dump file \"%s\": %s", + dump_file, dump_error_to_string (result)); + return argv[0]; +#else + const char *const suffix = ".pdmp"; int result; char *emacs_executable = argv[0]; @@ -1067,6 +1095,7 @@ load_pdump (int argc, char **argv) xfree (dump_file); return emacs_executable; +#endif } #endif /* HAVE_PDUMPER */ diff --git a/src/pdumper.c b/src/pdumper.c index eca64725211..dbdeae9a799 100644 --- a/src/pdumper.c +++ b/src/pdumper.c @@ -4072,10 +4072,12 @@ types. */) { eassert (initialized); +#ifndef HAVE_ANDROID if (! noninteractive) error ("Dumping Emacs currently works only in batch mode. " "If you'd like it to work interactively, please consider " "contributing a patch to Emacs."); +#endif if (will_dump_with_unexec_p ()) error ("This Emacs instance was started under the assumption " @@ -5842,6 +5844,10 @@ void syms_of_pdumper (void) { #ifdef HAVE_PDUMPER + unsigned char desired[sizeof fingerprint]; + int i; + char hexbuf[2 * sizeof fingerprint]; + defsubr (&Sdump_emacs_portable); defsubr (&Sdump_emacs_portable__sort_predicate); defsubr (&Sdump_emacs_portable__sort_predicate_copied); @@ -5854,5 +5860,17 @@ syms_of_pdumper (void) DEFSYM (Qdump_file_name, "dump-file-name"); DEFSYM (Qafter_pdump_load_hook, "after-pdump-load-hook"); defsubr (&Spdumper_stats); + + for (i = 0; i < sizeof fingerprint; i++) + desired[i] = fingerprint[i]; + + hexbuf_digest (hexbuf, desired, sizeof desired); + + DEFVAR_LISP ("pdumper-fingerprint", Vpdumper_fingerprint, + doc: /* The fingerprint of this Emacs binary. +It is a string that is supposed to be unique to each build of +Emacs. */); + Vpdumper_fingerprint = make_unibyte_string ((char *) hexbuf, + sizeof hexbuf); #endif /* HAVE_PDUMPER */ } diff --git a/src/sfntfont-android.c b/src/sfntfont-android.c index 5c9835fb13e..bc8f1b35b84 100644 --- a/src/sfntfont-android.c +++ b/src/sfntfont-android.c @@ -320,7 +320,10 @@ sfntfont_android_composite_bitmap (unsigned char *restrict buffer, { unsigned int *src_row; unsigned int *dst_row; - unsigned int i, src_y, x, src_x, max_x, dst_x, lim_x; + unsigned int i, src_y, x, src_x, max_x, dst_x; +#ifdef __aarch64__ + unsigned int lim_x; +#endif if ((intptr_t) dest & 3 || bitmap_info->stride & 3) /* This shouldn't be possible as Android is supposed to align the @@ -353,10 +356,10 @@ sfntfont_android_composite_bitmap (unsigned char *restrict buffer, src_x = x + (rect->x - text_rectangle->x); dst_x = x + rect->x; +#ifdef __aarch64__ /* This is the largest value of src_x. */ lim_x = max_x + (rect->x - text_rectangle->x); -#ifdef __aarch64__ if (!sfntfont_android_over_8888 (src_row + src_x, dst_row + dst_x, src_row + lim_x, -- cgit v1.3 From 0900bfbcc57c555909cb75c38eb0ed26fb6964ef Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 25 Jan 2023 18:44:47 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Startup, Android Environment): Document that restrictions on starting Emacs have been lifted. * java/README: Document Java for Emacs developers and how the Android port works. * java/org/gnu/emacs/EmacsApplication.java (EmacsApplication) (findDumpFile): New function. (onCreate): Factor out dump file finding functions to there. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Update function declarations. * java/org/gnu/emacs/EmacsNoninteractive.java (EmacsNoninteractive): New class. * java/org/gnu/emacs/EmacsService.java (EmacsService, getApkFile) (onCreate): Pass classpath to setEmacsParams. * java/org/gnu/emacs/EmacsThread.java (EmacsThread): Make run an override. * lisp/loadup.el: Don't dump on Android when noninteractive. * lisp/shell.el (shell--command-completion-data): Handle inaccessible directories. * src/Makefile.in (android-emacs): Link with gnulib. * src/android-emacs.c (main): Implement to launch app-process and then EmacsNoninteractive. * src/android.c (setEmacsParams): New argument `class_path'. Don't set stuff up when running noninteractive. * src/android.h (initEmacs): Likewise. * src/androidfont.c (init_androidfont): * src/androidselect.c (init_androidselect): Don't initialize when running noninteractive. * src/emacs.c (load_pdump): New argument `dump_file'. (android_emacs_init): Give new argument `dump_file' to `load_pdump'. * src/sfntfont-android.c (init_sfntfont_android): Don't initialize when running noninteractive. --- doc/emacs/android.texi | 18 +- java/README | 824 ++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsApplication.java | 39 +- java/org/gnu/emacs/EmacsNative.java | 16 +- java/org/gnu/emacs/EmacsNoninteractive.java | 164 ++++++ java/org/gnu/emacs/EmacsService.java | 48 +- java/org/gnu/emacs/EmacsThread.java | 22 +- lisp/loadup.el | 33 +- lisp/shell.el | 7 +- src/Makefile.in | 2 +- src/android-emacs.c | 83 ++- src/android.c | 114 +++- src/android.h | 8 +- src/androidfont.c | 3 + src/androidselect.c | 3 + src/emacs.c | 28 +- src/sfntfont-android.c | 3 + 17 files changed, 1306 insertions(+), 109 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsNoninteractive.java (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index cda0eadf226..e910e482ad8 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -102,8 +102,7 @@ files directory, containing an identifier unique to this copy of Emacs. The next time that same copy of Emacs starts up, it simply loads the -preloaded Lisp files contained in that dump file, greatly improving -start up time. +data contained in that dump file, greatly improving start up time. However, if by some unforseen circumstance the dump file is corrupted, Emacs can crash. If that happens, the dump file stored in @@ -192,12 +191,17 @@ installed. This means, instead of specifying @code{ctags} or @code{libctags.so} or @code{libemacsclient.so} on the commnd line instead when starting either of those programs in a subprocess. -@c TODO: remove this limitation. - In addition, the @file{/assets} directory containing Emacs start-up -files is inaccessible to processes not directly created by + The @file{/assets} directory containing Emacs start-up files is +supposed to be inaccessible to processes not directly created by @code{zygote}, the system service responsible for starting -applications. This makes it impossible to run Emacs in a subprocess -within itself. +applications. Since required Lisp is found in the @file{/assets} +directory, it would thus follow that it is not possible for Emacs to +start itself as a subprocess. A special binary named +@command{libandroid-emacs.so} is provided with Emacs, and does its +best to start Emacs, for the purpose of running Lisp in batch mode. +However, the approach it takes was devised by reading Android source +code, and is not sanctioned by the Android compatibility definition +documents, so your mileage may vary. @section Running Emacs in the background @cindex emacs killed, android diff --git a/java/README b/java/README index 05edf7744de..44f5a415162 100644 --- a/java/README +++ b/java/README @@ -10,3 +10,827 @@ to install different builds of Emacs on top of each other. Please keep the Java code indented with tabs and formatted according to the rules for C code in the GNU coding standards. Always use C-style comments. + +====================================================================== + +OVERVIEW OF JAVA + +Emacs developers do not know Java, and there is no reason they should +have to. Thus, the code in this directory is confined to what is +strictly necessary to support Emacs, and only uses a subset of Java +written in a way that is easily understandable to C programmers. + +Java is required because the entire Android runtime is based around +Java, and there is no way to write an Android program which runs +without Java. + +This text exists to prime other Emacs developers, already familar with +C, on the basic architecture of the Android port, and to teach them +how to read and write the Java code found in this directory. + +Java is an object oriented language with automatic memory management +compiled down to bytecode, which is then subject to interpretation by +a Java virtual machine. + +What that means, is that: + +struct emacs_window +{ + int some_fields; + int of_emacs_window; +}; + +static void +do_something_with_emacs_window (struct emacs_window *a, int n) +{ + a->some_fields = a->of_emacs_window + n; +} + +would be written: + +public class EmacsWindow +{ + public int someFields; + public int ofEmacsWindow; + + public void + doSomething (int n) + { + someFields = ofEmacsWindow + n; + } +} + +and instead of doing: + +do_something_with_emacs_window (my_window, 1); + +you say: + +myWindow.doSomething (1); + +In addition to functions associated with an object of a given class +(such as EmacsWindow), Java also has two other kinds of functions. + +The first are so-called ``static'' functions (the static means +something entirely different from what it does in C.) + +A static function, while still having to be defined within a class, +can be called without any object. Instead of the object, you write +the name of the Java class within which it is defined. For example, +the following C code: + +int +multiply_a_with_b_and_then_add_c (int a, int b, int c) +{ + return a * b + c; +} + +would be: + +public class EmacsSomething +{ + public static int + multiplyAWithBAndThenAddC (int a, int b, int c) + { + return a * b + c; + } +}; + +Then, instead of calling: + +int foo; + +foo = multiply_a_with_b_then_add_c (1, 2, 3); + +you say: + +int foo; + +foo = EmacsSomething.multiplyAWithBAndThenAddC (1, 2, 3); + +In Java, ``static'' does not mean that the function is only used +within its compilation unit! Instead, the ``private'' qualifier is +used to mean more or less the same thing: + +static void +this_procedure_is_only_used_within_this_file (void) +{ + do_something (); +} + +becomes + +public class EmacsSomething +{ + private static void + thisProcedureIsOnlyUsedWithinThisClass () + { + + } +} + +the other kind are called ``constructors''. They are functions that +must be called to allocate memory to hold a class: + +public class EmacsFoo +{ + int bar; + + public + EmacsFoo (int tokenA, int tokenB) + { + bar = tokenA + tokenB; + } +} + +now, the following statement: + +EmacsFoo foo; + +foo = new EmacsFoo (1, 2); + +becomes more or less equivalent to the following C code: + +struct emacs_foo +{ + int bar; +}; + +struct emacs_foo * +make_emacs_foo (int token_a, int token_b) +{ + struct emacs_foo *foo; + + foo = xmalloc (sizeof *foo); + foo->bar = token_a + token_b; + + return foo; +} + +/* ... */ + +struct emacs_foo *foo; + +foo = make_emacs_foo (1, 2); + +A class may have any number of constructors, or no constructors at +all, in which case the compiler inserts an empty constructor. + + + +Sometimes, you will see Java code that looks like this: + + allFiles = filesDirectory.listFiles (new FileFilter () { + @Override + public boolean + accept (File file) + { + return (!file.isDirectory () + && file.getName ().endsWith (".pdmp")); + } + }); + +This is Java's version of GCC's nested function extension. The major +difference is that the nested function may still be called even after +it goes out of scope, and always retains a reference to the class and +local variables around where it was called. + +Being an object-oriented language, Java also allows defining that a +class ``extends'' another class. The following C code: + +struct a +{ + long thirty_two; +}; + +struct b +{ + struct a a; + long long sixty_four; +}; + +extern void do_something (struct a *); + +void +my_function (struct b *b) +{ + do_something (&b->a); +} + +is roughly equivalent to the following Java code, split into two +files: + + A.java + +public class A +{ + int thirtyTwo; + + public void + doSomething () + { + etcEtcEtc (); + } +}; + + B.java + +public class B extends A +{ + long sixty_four; + + public static void + myFunction (B b) + { + b.doSomething (); + } +} + +the Java runtime has transformed the call to ``b.doSomething'' to +``((A) b).doSomething''. + +However, Java also allows overriding this behavior, by specifying the +@Override keyword: + +public class B extends A +{ + long sixty_four; + + @Override + public void + doSomething () + { + Something.doSomethingTwo (); + super.doSomething (); + } +} + +now, any call to ``doSomething'' on a ``B'' created using ``new B ()'' +will end up calling ``Something.doSomethingTwo'', before calling back +to ``A.doSomething''. This override also applies in reverse; that is +to say, even if you write: + + ((A) b).doSomething (); + +B's version of doSomething will still be called, if ``b'' was created +using ``new B ()''. + +This mechanism is used extensively throughout the Java language and +Android windowing APIs. + +Elsewhere, you will encounter Java code that defines arrays: + +public class EmacsFrobinicator +{ + public static void + emacsFrobinicate (int something) + { + int[] primesFromSomething; + + primesFromSomething = new int[numberOfPrimes]; + /* ... */ + } +} + +Java arrays are similar to C arrays in that they can not grow. But +they are very much unlike C arrays in that they are always references +(as opposed to decaying into pointers in various situations), and +contain information about their length. + +If another function named ``frobinicate1'' takes an array as an +argument, then it need not take the length of the array. + +Instead, it simply iterates over the array like so: + +int i, k; + +for (i = 0; i < array.length; ++i) + { + k = array[i]; + + Whatever.doSomethingWithK (k); + } + +The syntax used to define arrays is also slightly different. As +arrays are always references, there is no way for you to tell the +runtime to allocate an array of size N in a structure (class.) + +Instead, if you need an array of that size, you must declare a field +with the type of the array, and allocate the array inside the class's +constructor, like so: + +public class EmacsArrayContainer +{ + public int[] myArray; + + public + EmacsArrayContainer () + { + myArray = new array[10]; + } +} + +while in C, you could just have written: + +struct emacs_array_container +{ + int my_array[10]; +}; + +or, possibly even better, + +typedef int my_array[10]; + +Alas, Java has no equivalent of `typedef'. + +JAVA NATIVE INTERFACE + +Java also provides an interface for C code to interface with Java. + +C functions exported from a shared library become static Java +functions within a class, like so: + +public class EmacsNative +{ + /* Obtain the fingerprint of this build of Emacs. The fingerprint + can be used to determine the dump file name. */ + public static native String getFingerprint (); + + /* Set certain parameters before initializing Emacs. + + assetManager must be the asset manager associated with the + context that is loading Emacs. It is saved and remains for the + remainder the lifetime of the Emacs process. + + filesDir must be the package's data storage location for the + current Android user. + + libDir must be the package's data storage location for native + libraries. It is used as PATH. + + cacheDir must be the package's cache directory. It is used as + the `temporary-file-directory'. + + pixelDensityX and pixelDensityY are the DPI values that will be + used by Emacs. + + classPath must be the classpath of this app_process process, or + NULL. + + emacsService must be the EmacsService singleton, or NULL. */ + public static native void setEmacsParams (AssetManager assetManager, + String filesDir, + String libDir, + String cacheDir, + float pixelDensityX, + float pixelDensityY, + String classPath, + EmacsService emacsService); +} + +Where the corresponding C functions are located in android.c, and +loaded by the special invocation: + + static + { + System.loadLibrary ("emacs"); + }; + + +See http://docs.oracle.com/en/java/javase/19/docs/specs/jni/intro.html +for more details. + + + +OVERVIEW OF ANDROID + +When the Android system starts an application, it does not actually +call the application's ``main'' function. It may not even start the +application's process if one is already running. + +Instead, Android is organized around components. When the user opens +the ``Emacs'' icon, the Android system looks up and starts the +component associated with the ``Emacs'' icon. In this case, the +component is called an activity, and is declared in +the AndroidManifest.xml in this directory: + + + + + + + + + +This tells Android to start the activity defined in ``EmacsActivity'' +(defined in org/gnu/emacs/EmacsActivity.java), a class extending the +Android class ``Activity''. + +To do so, the Android system creates an instance of ``EmacsActivity'' +and the window system window associated with it, and eventually calls: + + Activity activity; + + activity.onCreate (...); + +But which ``onCreate'' is really called? +It is actually the ``onCreate'' defined in EmacsActivity.java, as +it overrides the ``onCreate'' defined in Android's own Activity class: + + @Override + public void + onCreate (Bundle savedInstanceState) + { + FrameLayout.LayoutParams params; + Intent intent; + +Then, this is what happens step-by-step within the ``onCreate'' +function: + + /* See if Emacs should be started with -Q. */ + intent = getIntent (); + EmacsService.needDashQ + = intent.getBooleanExtra ("org.gnu.emacs.START_DASH_Q", + false); + +Here, Emacs obtains the intent (a request to start a component) which +was used to start Emacs, and sets a special flag if it contains a +request for Emacs to start with the ``-Q'' command-line argument. + + /* Set the theme to one without a title bar. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme (android.R.style.Theme_DeviceDefault_NoActionBar); + else + setTheme (android.R.style.Theme_NoTitleBar); + +Next, Emacs sets an appropriate theme for the activity's associated +window decorations. + + params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + + /* Make the frame layout. */ + layout = new FrameLayout (this); + layout.setLayoutParams (params); + + /* Set it as the content view. */ + setContentView (layout); + +Then, Emacs creates a ``FrameLayout'', a widget that holds a single +other widget, and makes it the activity's ``content view''. + +The activity itself is a ``FrameLayout'', so the ``layout parameters'' +here apply to the FrameLayout itself, and not its children. + + /* Maybe start the Emacs service if necessary. */ + EmacsService.startEmacsService (this); + +And after that, Emacs calls the static function ``startEmacsService'', +defined in the class ``EmacsService''. This starts the Emacs service +component if necessary. + + /* Add this activity to the list of available activities. */ + EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this); + + super.onCreate (savedInstanceState); + +Finally, Emacs registers that this activity is now ready to receive +top-level frames (windows) created from Lisp. + +Activities come and go, but Emacs has to stay running in the mean +time. Thus, Emacs also defines a ``service'', which is a long-running +component that the Android system allows to run in the background. + +Let us go back and review the definition of ``startEmacsService'': + + public static void + startEmacsService (Context context) + { + if (EmacsService.SERVICE == null) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + /* Start the Emacs service now. */ + context.startService (new Intent (context, + EmacsService.class)); + else + /* Display the permanant notification and start Emacs as a + foreground service. */ + context.startForegroundService (new Intent (context, + EmacsService.class)); + } + } + +If ``EmacsService.SERVICE'' does not yet exist, what this does is to +tell the ``context'' (the equivalent of an Xlib Display *) to start a +service defined by the class ``EmacsService''. Eventually, this +results in ``EmacsService.onCreate'' being called: + + @Override + public void + onCreate () + { + AssetManager manager; + Context app_context; + String filesDir, libDir, cacheDir, classPath; + double pixelDensityX; + double pixelDensityY; + +Here is what this function does, step-by-step: + + SERVICE = this; + +First, it sets the special static variable ``SERVICE'' to ``this'', +which is a pointer to the ``EmacsService' object that was created. + + handler = new Handler (Looper.getMainLooper ()); + +Next, it creates a ``Handler'' object for the ``main looper''. +This is a helper structure which allows executing code on the Android +user interface thread. + + manager = getAssets (); + app_context = getApplicationContext (); + metrics = getResources ().getDisplayMetrics (); + pixelDensityX = metrics.xdpi; + pixelDensityY = metrics.ydpi; + +Finally, it obtains: + + - the asset manager, which is used to retrieve assets packaged + into the Emacs application package. + + - the application context, used to obtain application specific + information. + + - the display metrics, and from them, the X and Y densities in dots + per inch. + +Then, inside a ``try'' block: + + try + { + /* Configure Emacs with the asset manager and other necessary + parameters. */ + filesDir = app_context.getFilesDir ().getCanonicalPath (); + libDir = getLibraryDirectory (); + cacheDir = app_context.getCacheDir ().getCanonicalPath (); + +It obtains the names of the Emacs home, shared library, and temporary +file directories. + + /* Now provide this application's apk file, so a recursive + invocation of app_process (through android-emacs) can + find EmacsNoninteractive. */ + classPath = getApkFile (); + +The name of the Emacs application package. + + Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir + + ", libDir = " + libDir + ", and classPath = " + classPath); + +Prints a debug message to the Android system log with this +information. + + EmacsNative.setEmacsParams (manager, filesDir, libDir, + cacheDir, (float) pixelDensityX, + (float) pixelDensityY, + classPath, this); + +And calls the native function ``setEmacsParams'' (defined in +android.c) to configure Emacs with this information. + + /* Start the thread that runs Emacs. */ + thread = new EmacsThread (this, needDashQ); + thread.start (); + +Then, it allocates an ``EmacsThread'' object, and starts that thread. +Inside that thread is where Emacs's C code runs. + + } + catch (IOException exception) + { + EmacsNative.emacsAbort (); + return; + +And here is the purpose of the ``try'' block. Functions related to +file names in Java will signal errors of various types upon failure. + +This ``catch'' block means that the Java virtual machine will abort +execution of the contents of the ``try'' block as soon as an error of +type ``IOException'' is encountered, and begin executing the contents +of the ``catch'' block. + +Any failure of that type here is a crash, and +``EmacsNative.emacsAbort'' is called to quickly abort the process to +get a useful backtrace. + } + } + +Now, let us look at the definition of the class ``EmacsThread'', found +in org/gnu/emacs/EmacsThread.java: + +public class EmacsThread extends Thread +{ + /* Whether or not Emacs should be started -Q. */ + private boolean startDashQ; + + public + EmacsThread (EmacsService service, boolean startDashQ) + { + super ("Emacs main thread"); + this.startDashQ = startDashQ; + } + + @Override + public void + run () + { + String args[]; + + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", }; + else + args = new String[] { "libandroid-emacs.so", "-Q", }; + + /* Run the native code now. */ + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); + } +}; + +The class itself defines a single field, ``startDashQ'', a constructor +with an unused argument of the type ``EmacsService'' (which is useful +while debugging) and a flag ``startDashQ'', and a single function +``run'', overriding the same function in the class ``Thread''. + +When ``thread.start'' is called, the Java virtual machine creates a +new thread, and then calls the function ``run'' within that thread. + +This function then computes a suitable argument vector, and calls +``EmacsNative.initEmacs'' (defined in android.c), which then calls a +modified version of the regular Emacs ``main'' function. + +At that point, Emacs initialization proceeds as usual: +Vinitial_window_system is set, loadup.el calls `normal-top-level', +which calls `command-line', and finally +`window-system-initialization', which initializes the `android' +terminal interface as usual. + +What happens here is the same as on other platforms. Now, here is +what happens when the initial frame is created: Fx_create_frame calls +`android_create_frame_window' to create a top level window: + +static void +android_create_frame_window (struct frame *f) +{ + struct android_set_window_attributes attributes; + enum android_window_value_mask attribute_mask; + + attributes.background_pixel = FRAME_BACKGROUND_PIXEL (f); + attribute_mask = ANDROID_CW_BACK_PIXEL; + + block_input (); + FRAME_ANDROID_WINDOW (f) + = android_create_window (FRAME_DISPLAY_INFO (f)->root_window, + f->left_pos, + f->top_pos, + FRAME_PIXEL_WIDTH (f), + FRAME_PIXEL_HEIGHT (f), + attribute_mask, &attributes); + unblock_input (); +} + +This calls the function `android_create_window' with some arguments +whose meanings are identical to the arguments to `XCreateWindow'. + +Here is the definition of `android_create_window', in android.c: + +android_window +android_create_window (android_window parent, int x, int y, + int width, int height, + enum android_window_value_mask value_mask, + struct android_set_window_attributes *attrs) +{ + static jclass class; + static jmethodID constructor; + jobject object, parent_object, old; + android_window window; + android_handle prev_max_handle; + bool override_redirect; + +What does it do? First, some context: + +At any time, there can be at most 65535 Java objects referred to by +the rest of Emacs through the Java native interface. Each such object +is assigned a ``handle'' (similar to an XID on X) and given a unique +type. The function `android_resolve_handle' returns the JNI `jobject' +associated with a given handle. + + parent_object = android_resolve_handle (parent, ANDROID_HANDLE_WINDOW); + +Here, it is being used to look up the `jobject' associated with the +`parent' handle. + + prev_max_handle = max_handle; + window = android_alloc_id (); + +Next, `max_handle' is saved, and a new handle is allocated for +`window'. + + if (!window) + error ("Out of window handles!"); + +An error is signalled if Emacs runs out of available handles. + + if (!class) + { + class = (*android_java_env)->FindClass (android_java_env, + "org/gnu/emacs/EmacsWindow"); + assert (class != NULL); + +Then, if this initialization has not yet been completed, Emacs +proceeds to find the Java class named ``EmacsWindow''. + + constructor + = (*android_java_env)->GetMethodID (android_java_env, class, "", + "(SLorg/gnu/emacs/EmacsWindow;" + "IIIIZ)V"); + assert (constructor != NULL); + +And it tries to look up the constructor, which should take seven +arguments: + + S - a short. (the handle ID) + Lorg/gnu/Emacs/EmacsWindow; - an instance of the EmacsWindow + class. (the parent) + IIII - four ints. (the window geometry.) + Z - a boolean. (whether or not the + window is override-redirect; see + XChangeWindowAttributes.) + + old = class; + class = (*android_java_env)->NewGlobalRef (android_java_env, class); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (old); + +Next, it saves a global reference to the class and deletes the local +reference. Global references will never be deallocated by the Java +virtual machine as long as they still exist. + + if (!class) + memory_full (0); + } + + /* N.B. that ANDROID_CW_OVERRIDE_REDIRECT can only be set at window + creation time. */ + override_redirect = ((value_mask + & ANDROID_CW_OVERRIDE_REDIRECT) + && attrs->override_redirect); + + object = (*android_java_env)->NewObject (android_java_env, class, + constructor, (jshort) window, + parent_object, (jint) x, (jint) y, + (jint) width, (jint) height, + (jboolean) override_redirect); + +Then, it creates an instance of the ``EmacsWindow'' class with the +appropriate arguments and previously determined constructor. + + if (!object) + { + (*android_java_env)->ExceptionClear (android_java_env); + + max_handle = prev_max_handle; + memory_full (0); + +If creating the object fails, Emacs clears the ``pending exception'' +and signals that it is out of memory. + } + + android_handles[window].type = ANDROID_HANDLE_WINDOW; + android_handles[window].handle + = (*android_java_env)->NewGlobalRef (android_java_env, + object); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (object); + +Otherwise, it associates a new global reference to the object with the +handle, and deletes the local reference returned from the JNI +NewObject function. + + if (!android_handles[window].handle) + memory_full (0); + +If allocating the global reference fails, Emacs signals that it is out +of memory. + + android_change_window_attributes (window, value_mask, attrs); + return window; + +Otherwise, it applies the specified window attributes and returns the +handle of the new window. +} diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index 87085c32d62..96328b99d1c 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -22,27 +22,20 @@ package org.gnu.emacs; import java.io.File; import java.io.FileFilter; +import android.content.Context; + import android.app.Application; import android.util.Log; -public class EmacsApplication extends Application implements FileFilter +public class EmacsApplication extends Application { private static final String TAG = "EmacsApplication"; /* The name of the dump file to use. */ public static String dumpFileName; - @Override - public boolean - accept (File file) - { - return (!file.isDirectory () - && file.getName ().endsWith (".pdmp")); - } - - @Override - public void - onCreate () + public static void + findDumpFile (Context context) { File filesDirectory; File[] allFiles; @@ -52,13 +45,19 @@ public class EmacsApplication extends Application implements FileFilter wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () + ".pdmp"); - Log.d (TAG, "onCreate: looking for " + wantedDumpFile); - /* Obtain a list of all files ending with ``.pdmp''. Then, look for a file named ``emacs-.pdmp'' and delete the rest. */ - filesDirectory = getFilesDir (); - allFiles = filesDirectory.listFiles (this); + filesDirectory = context.getFilesDir (); + allFiles = filesDirectory.listFiles (new FileFilter () { + @Override + public boolean + accept (File file) + { + return (!file.isDirectory () + && file.getName ().endsWith (".pdmp")); + } + }); /* Now try to find the right dump file. */ for (i = 0; i < allFiles.length; ++i) @@ -69,9 +68,13 @@ public class EmacsApplication extends Application implements FileFilter /* Delete this outdated dump file. */ allFiles[i].delete (); } + } - Log.d (TAG, "onCreate: found " + dumpFileName); - + @Override + public void + onCreate () + { + findDumpFile (this); super.onCreate (); } }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 9636561a524..a772b965301 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -29,8 +29,7 @@ public class EmacsNative can be used to determine the dump file name. */ public static native String getFingerprint (); - /* Set certain parameters before initializing Emacs. This proves - that libemacs.so is being loaded from Java code. + /* Set certain parameters before initializing Emacs. assetManager must be the asset manager associated with the context that is loading Emacs. It is saved and remains for the @@ -48,19 +47,26 @@ public class EmacsNative pixelDensityX and pixelDensityY are the DPI values that will be used by Emacs. - emacsService must be the emacsService singleton. */ + classPath must be the classpath of this app_process process, or + NULL. + + emacsService must be the EmacsService singleton, or NULL. */ public static native void setEmacsParams (AssetManager assetManager, String filesDir, String libDir, String cacheDir, float pixelDensityX, float pixelDensityY, + String classPath, EmacsService emacsService); /* Initialize Emacs with the argument array ARGV. Each argument must contain a NULL terminated string, or else the behavior is - undefined. */ - public static native void initEmacs (String argv[]); + undefined. + + DUMPFILE is the dump file to use, or NULL if Emacs is to load + loadup.el itself. */ + public static native void initEmacs (String argv[], String dumpFile); /* Abort and generate a native core dump. */ public static native void emacsAbort (); diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java new file mode 100644 index 00000000000..a3aefee5e0b --- /dev/null +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -0,0 +1,164 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.os.Looper; +import android.os.Build; + +import android.content.Context; +import android.content.res.AssetManager; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/* Noninteractive Emacs. + + This is the class that libandroid-emacs.so starts. + libandroid-emacs.so figures out the system classpath, then starts + dalvikvm with the framework jars. + + At that point, dalvikvm calls main, which sets up the main looper, + creates an ActivityThread and attaches it to the main thread. + + Then, it obtains an application context for the LoadedApk in the + application thread. + + Finally, it obtains the necessary context specific objects and + initializes Emacs. */ + +@SuppressWarnings ("unchecked") +public class EmacsNoninteractive +{ + private static String + getLibraryDirectory (Context context) + { + int apiLevel; + + apiLevel = Build.VERSION.SDK_INT; + + if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) + return context.getApplicationInfo().nativeLibraryDir; + else if (apiLevel >= Build.VERSION_CODES.DONUT) + return context.getApplicationInfo().dataDir + "/lib"; + + return "/data/data/" + context.getPackageName() + "/lib"; + } + + public static void + main (String[] args) + { + Object activityThread, loadedApk; + Class activityThreadClass, loadedApkClass, contextImplClass; + Class compatibilityInfoClass; + Method method; + Context context; + AssetManager assets; + String filesDir, libDir, cacheDir; + + Looper.prepare (); + context = null; + assets = null; + filesDir = libDir = cacheDir = null; + + try + { + /* Get the activity thread. */ + activityThreadClass = Class.forName ("android.app.ActivityThread"); + + /* Get the systemMain method. */ + method = activityThreadClass.getMethod ("systemMain"); + + /* Create and attach the activity thread. */ + activityThread = method.invoke (null); + + /* Now get an LoadedApk. */ + loadedApkClass = Class.forName ("android.app.LoadedApk"); + + /* Get a LoadedApk. How to do this varies by Android version. + On Android 2.3.3 and earlier, there is no + ``compatibilityInfo'' argument to getPackageInfo. */ + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD) + { + method + = activityThreadClass.getMethod ("getPackageInfo", + String.class, + int.class); + loadedApk = method.invoke (activityThread, "org.gnu.emacs", + 0); + } + else + { + compatibilityInfoClass + = Class.forName ("android.content.res.CompatibilityInfo"); + + method + = activityThreadClass.getMethod ("getPackageInfo", + String.class, + compatibilityInfoClass, + int.class); + loadedApk = method.invoke (activityThread, "org.gnu.emacs", null, + 0); + } + + if (loadedApk == null) + throw new RuntimeException ("getPackageInfo returned NULL"); + + /* Now, get a context. */ + contextImplClass = Class.forName ("android.app.ContextImpl"); + method = contextImplClass.getDeclaredMethod ("createAppContext", + activityThreadClass, + loadedApkClass); + method.setAccessible (true); + context = (Context) method.invoke (null, activityThread, loadedApk); + + /* Don't actually start the looper or anything. Instead, obtain + an AssetManager. */ + assets = context.getAssets (); + + /* Now configure Emacs. The class path should already be set. */ + + filesDir = context.getFilesDir ().getCanonicalPath (); + libDir = getLibraryDirectory (context); + cacheDir = context.getCacheDir ().getCanonicalPath (); + } + catch (Exception e) + { + System.err.println ("Internal error: " + e); + System.err.println ("This means that the Android platform changed,"); + System.err.println ("and that Emacs needs adjustments in order to"); + System.err.println ("obtain required system internal resources."); + System.err.println ("Please report this bug to bug-gnu-emacs@gnu.org."); + + System.exit (1); + } + + EmacsNative.setEmacsParams (assets, filesDir, + libDir, cacheDir, 0.0f, + 0.0f, null, null); + + /* Now find the dump file that Emacs should use, if it has already + been dumped. */ + EmacsApplication.findDumpFile (context); + + /* Start Emacs. */ + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); + } +}; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 4db1ea5359f..91db76b08e3 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -41,6 +41,9 @@ import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.ApplicationInfoFlags; +import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.net.Uri; @@ -118,8 +121,38 @@ public class EmacsService extends Service return null; } + @SuppressWarnings ("deprecation") + private String + getApkFile () + { + PackageManager manager; + ApplicationInfo info; + + manager = getPackageManager (); + + try + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) + info = manager.getApplicationInfo ("org.gnu.emacs", 0); + else + info = manager.getApplicationInfo ("org.gnu.emacs", + ApplicationInfoFlags.of (0)); + + /* Return an empty string upon failure. */ + + if (info.sourceDir != null) + return info.sourceDir; + + return ""; + } + catch (Exception e) + { + return ""; + } + } + @TargetApi (Build.VERSION_CODES.GINGERBREAD) - String + private String getLibraryDirectory () { int apiLevel; @@ -142,7 +175,7 @@ public class EmacsService extends Service { AssetManager manager; Context app_context; - String filesDir, libDir, cacheDir; + String filesDir, libDir, cacheDir, classPath; double pixelDensityX; double pixelDensityY; @@ -162,13 +195,18 @@ public class EmacsService extends Service libDir = getLibraryDirectory (); cacheDir = app_context.getCacheDir ().getCanonicalPath (); + /* Now provide this application's apk file, so a recursive + invocation of app_process (through android-emacs) can + find EmacsNoninteractive. */ + classPath = getApkFile (); + Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir - + " and libDir = " + libDir); + + ", libDir = " + libDir + ", and classPath = " + classPath); EmacsNative.setEmacsParams (manager, filesDir, libDir, cacheDir, (float) pixelDensityX, (float) pixelDensityY, - this); + classPath, this); /* Start the thread that runs Emacs. */ thread = new EmacsThread (this, needDashQ); @@ -491,8 +529,6 @@ public class EmacsService extends Service public static void startEmacsService (Context context) { - PendingIntent intent; - if (EmacsService.SERVICE == null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 5b76d11db4b..f5e9d54044a 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -33,30 +33,18 @@ public class EmacsThread extends Thread this.startDashQ = startDashQ; } + @Override public void run () { String args[]; - if (EmacsApplication.dumpFileName == null) - { - if (!startDashQ) - args = new String[] { "libandroid-emacs.so", }; - else - args = new String[] { "libandroid-emacs.so", "-Q", }; - } + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", }; else - { - if (!startDashQ) - args = new String[] { "libandroid-emacs.so", "--dump-file", - EmacsApplication.dumpFileName, }; - else - args = new String[] { "libandroid-emacs.so", "-Q", - "--dump-file", - EmacsApplication.dumpFileName, }; - } + args = new String[] { "libandroid-emacs.so", "-Q", }; /* Run the native code now. */ - EmacsNative.initEmacs (args); + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); } }; diff --git a/lisp/loadup.el b/lisp/loadup.el index 8d2e4bbb7c2..1747d1d960a 100644 --- a/lisp/loadup.el +++ b/lisp/loadup.el @@ -560,22 +560,23 @@ lost after dumping"))) ;; different build fingerprint upon being created, which happens ;; the moment the Android system starts Emacs. Then, it passes ;; the appropriate "--dump-file" to libemacs.so as it starts. - (let ((temp-dir (getenv "TEMP")) - (dump-file-name (format "%semacs-%s.pdmp" - (file-name-as-directory "~") - pdumper-fingerprint)) - (dump-temp-file-name (format "%s~emacs-%s.pdmp" - (file-name-as-directory "~") - pdumper-fingerprint))) - (unless (pdumper-stats) - (condition-case () - (progn - (dump-emacs-portable dump-temp-file-name) - ;; Move the dumped file to the actual dump file name. - (rename-file dump-temp-file-name dump-file-name) - ;; Continue with loadup. - nil) - (error nil))))) + (when (not noninteractive) + (let ((temp-dir (getenv "TEMP")) + (dump-file-name (format "%semacs-%s.pdmp" + (file-name-as-directory "~") + pdumper-fingerprint)) + (dump-temp-file-name (format "%s~emacs-%s.pdmp" + (file-name-as-directory "~") + pdumper-fingerprint))) + (unless (pdumper-stats) + (condition-case () + (progn + (dump-emacs-portable dump-temp-file-name) + ;; Move the dumped file to the actual dump file name. + (rename-file dump-temp-file-name dump-file-name) + ;; Continue with loadup. + nil) + (error nil)))))) (if dump-mode (let ((output (cond ((equal dump-mode "pdump") "emacs.pdmp") ((equal dump-mode "dump") "emacs") diff --git a/lisp/shell.el b/lisp/shell.el index 5cf108bfa3b..29f7d5c02d4 100644 --- a/lisp/shell.el +++ b/lisp/shell.el @@ -1332,7 +1332,12 @@ Returns t if successful." (while path-dirs (setq dir (file-name-as-directory (comint-directory (or (car path-dirs) "."))) comps-in-dir (and (file-accessible-directory-p dir) - (file-name-all-completions filenondir dir))) + (condition-case nil + (file-name-all-completions filenondir dir) + ;; Systems such as Android sometimes + ;; put inaccessible directories in + ;; PATH. + (permission-denied nil)))) ;; Go thru each completion found, to see whether it should be used. (while comps-in-dir (setq file (car comps-in-dir) diff --git a/src/Makefile.in b/src/Makefile.in index 29beb519abb..fe745770b9f 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -768,7 +768,7 @@ libemacs.so: $(ALLOBJS) $(LIBEGNU_ARCHIVE) $(EMACSRES) \ android-emacs: libemacs.so android-emacs.o $(AM_V_CCLD)$(CC) -o $@ $(ALL_CFLAGS) $(LDFLAGS) \ - -L. "-l:libemacs.so" android-emacs.o + $(LIBEGNU_ARCHIVE) -L. "-l:libemacs.so" android-emacs.o endif ## The following oldxmenu-related rules are only (possibly) used if diff --git a/src/android-emacs.c b/src/android-emacs.c index d4fa14e39fb..c1f2a6f43bb 100644 --- a/src/android-emacs.c +++ b/src/android-emacs.c @@ -18,13 +18,88 @@ You should have received a copy of the GNU General Public License along with GNU Emacs. If not, see . */ #include -#include "android.h" +#include +#include +#include +#include -/* android-emacs is a wrapper around libemacs. It simply calls - android_emacs_init with the argv and argc given to it. */ +/* android-emacs is a wrapper around /system/bin/app_process(64). + It invokes app_process(64) with the right class path and then + starts org.gnu.emacs.EmacsNoninteractive. + + The main function in that class tries to load an activity thread + and obtain a context and asset manager before calling + android_emacs_init, which is required for Emacs to find required + preloaded Lisp. */ int main (int argc, char **argv) { - return android_emacs_init (argc, argv); + char **args; + int i; + char *bootclasspath, *emacs_class_path; + + /* Allocate enough to hold the arguments to app_process. */ + args = alloca ((10 + argc) * sizeof *args); + + /* Clear args. */ + memset (args, 0, (10 + argc) * sizeof *args); + + /* First, figure out what program to start. */ +#if defined __x86_64__ || defined __aarch64__ + args[0] = (char *) "/system/bin/app_process64"; +#else + args[0] = (char *) "/system/bin/app_process"; +#endif + + /* Next, obtain the boot class path. */ + bootclasspath = getenv ("BOOTCLASSPATH"); + + /* And the Emacs class path. */ + emacs_class_path = getenv ("EMACS_CLASS_PATH"); + + if (!bootclasspath) + { + fprintf (stderr, "The BOOTCLASSPATH environment variable" + " is not set. As a result, Emacs does not know" + " how to start app_process.\n" + "This is likely a change in the Android platform." + " Please report this to bug-gnu-emacs@gnu.org.\n"); + return 1; + } + + if (!emacs_class_path) + { + fprintf (stderr, "EMACS_CLASS_PATH not set." + " Please make sure Emacs is being started" + " from within a running copy of Emacs.\n"); + return 1; + } + + if (asprintf (&bootclasspath, "-Djava.class.path=%s:%s", + bootclasspath, emacs_class_path) < 0) + { + perror ("asprintf"); + return 1; + } + + args[1] = bootclasspath; + args[2] = (char *) "/system/bin"; + args[3] = (char *) "--nice-name=emacs"; + args[4] = (char *) "org.gnu.emacs.EmacsNoninteractive"; + + /* Arguments from here on are passed to main in + EmacsNoninteractive.java. */ + args[5] = argv[0]; + + /* Now copy the rest of the arguments over. */ + for (i = 1; i < argc; ++i) + args[5 + i] = argv[i]; + + /* Finally, try to start the app_process. */ + execvp (args[0], args); + + /* If exit fails, return an error indication. */ + perror ("exec"); + return 1; } diff --git a/src/android.c b/src/android.c index c186b462360..8c4442e3397 100644 --- a/src/android.c +++ b/src/android.c @@ -145,6 +145,10 @@ char *android_game_path; /* The directory used to store temporary files. */ char *android_cache_dir; +/* The list of archive files within which the Java virtual macine + looks for class files. */ +char *android_class_path; + /* The display's pixel densities. */ double android_pixel_density_x, android_pixel_density_y; @@ -1315,6 +1319,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject cache_dir, jfloat pixel_density_x, jfloat pixel_density_y, + jobject class_path, jobject emacs_service_object) { int pipefd[2]; @@ -1372,19 +1377,30 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, object from being deleted. */ (*env)->NewGlobalRef (env, local_asset_manager); - /* Create a pipe and duplicate it to stdout and stderr. Next, make - a thread that prints stderr to the system log. */ + if (emacs_service_object) + { + /* Create a pipe and duplicate it to stdout and stderr. Next, + make a thread that prints stderr to the system log. - if (pipe2 (pipefd, O_CLOEXEC) < 0) - emacs_abort (); + Notice that this function is called in one of two ways. The + first is when Emacs is being started as a GUI application by + the system, and the second is when Emacs is being started by + libandroid-emacs.so as an ordinary noninteractive Emacs. - if (dup2 (pipefd[1], 2) < 0) - emacs_abort (); - close (pipefd[1]); + In the second case, stderr is usually connected to a PTY, so + this is unnecessary. */ - if (pthread_create (&thread, NULL, android_run_debug_thread, - (void *) (intptr_t) pipefd[0])) - emacs_abort (); + if (pipe2 (pipefd, O_CLOEXEC) < 0) + emacs_abort (); + + if (dup2 (pipefd[1], 2) < 0) + emacs_abort (); + close (pipefd[1]); + + if (pthread_create (&thread, NULL, android_run_debug_thread, + (void *) (intptr_t) pipefd[0])) + emacs_abort (); + } /* Now set the path to the site load directory. */ @@ -1430,6 +1446,23 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, (*env)->ReleaseStringUTFChars (env, (jstring) cache_dir, java_string); + if (class_path) + { + java_string = (*env)->GetStringUTFChars (env, (jstring) class_path, + NULL); + + if (!java_string) + emacs_abort (); + + android_class_path = strdup ((const char *) java_string); + + if (!android_files_dir) + emacs_abort (); + + (*env)->ReleaseStringUTFChars (env, (jstring) class_path, + java_string); + } + /* Calculate the site-lisp path. */ android_site_load_path = malloc (PATH_MAX + 1); @@ -1450,16 +1483,32 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, "Site-lisp directory: %s\n" "Files directory: %s\n" "Native code directory: %s\n" - "Game score path: %s\n", + "Game score path: %s\n" + "Class path: %s\n", android_site_load_path, android_files_dir, - android_lib_dir, android_game_path); + android_lib_dir, android_game_path, + (android_class_path + ? android_class_path + : "None")); + + if (android_class_path) + /* Set EMACS_CLASS_PATH to the class path where + EmacsNoninteractive can be found. */ + setenv ("EMACS_CLASS_PATH", android_class_path, 1); + + /* Set LD_LIBRARY_PATH to an appropriate value. */ + setenv ("LD_LIBRARY_PATH", android_lib_dir, 1); /* Make a reference to the Emacs service. */ - emacs_service = (*env)->NewGlobalRef (env, emacs_service_object); - if (!emacs_service) - emacs_abort (); + if (emacs_service_object) + { + emacs_service = (*env)->NewGlobalRef (env, emacs_service_object); + + if (!emacs_service) + emacs_abort (); + } /* Set up events. */ android_init_events (); @@ -1660,12 +1709,14 @@ android_init_emacs_window (void) } extern JNIEXPORT void JNICALL -NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) +NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, + jobject dump_file_object) { char **c_argv; jsize nelements, i; jobject argument; const char *c_argument; + char *dump_file; android_java_env = env; @@ -1705,9 +1756,34 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) __android_log_print (ANDROID_LOG_WARN, __func__, "chdir: %s", strerror (errno)); - /* Initialize the Android GUI. */ - android_init_gui = true; - android_emacs_init (nelements, c_argv); + /* Initialize the Android GUI as long as the service object was + set. */ + + if (emacs_service) + android_init_gui = true; + + /* Now see if a dump file has been specified and should be used. */ + dump_file = NULL; + + if (dump_file_object) + { + c_argument + = (*env)->GetStringUTFChars (env, (jstring) dump_file_object, + NULL); + + /* Copy the Java string data once. */ + dump_file = strdup (c_argument); + + /* Release the Java string data. */ + (*env)->ReleaseStringUTFChars (env, (jstring) dump_file_object, + c_argument); + } + + /* Delete local references to objects that are no longer needed. */ + ANDROID_DELETE_LOCAL_REF (argv); + ANDROID_DELETE_LOCAL_REF (dump_file_object); + + android_emacs_init (nelements, c_argv, dump_file); /* android_emacs_init should never return. */ emacs_abort (); } diff --git a/src/android.h b/src/android.h index ce0ccfc4338..6c20995e4a1 100644 --- a/src/android.h +++ b/src/android.h @@ -38,15 +38,11 @@ along with GNU Emacs. If not, see . */ #include "lisp.h" #endif -/* This must be used in every symbol declaration to export it to the - JNI Emacs wrapper. */ -#define ANDROID_EXPORT __attribute__ ((visibility ("default"))) - -extern bool ANDROID_EXPORT android_init_gui; -extern int ANDROID_EXPORT android_emacs_init (int, char **); +extern bool android_init_gui; #ifndef ANDROID_STUBIFY +extern int android_emacs_init (int, char **, char *); extern int android_select (int, fd_set *, fd_set *, fd_set *, struct timespec *); diff --git a/src/androidfont.c b/src/androidfont.c index bec5a59ca77..55f45758352 100644 --- a/src/androidfont.c +++ b/src/androidfont.c @@ -1104,6 +1104,9 @@ syms_of_androidfont (void) void init_androidfont (void) { + if (!android_init_gui) + return; + android_init_font_driver (); android_init_font_spec (); android_init_font_metrics (); diff --git a/src/androidselect.c b/src/androidselect.c index 2e2f2d7e483..4585d64b7e8 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -240,6 +240,9 @@ init_androidselect (void) jobject tem; jmethodID make_clipboard; + if (!android_init_gui) + return; + android_init_emacs_clipboard (); make_clipboard = clipboard_class.make_clipboard; diff --git a/src/emacs.c b/src/emacs.c index b0c19fe0070..a24f9960494 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -875,19 +875,23 @@ dump_error_to_string (int result) } } -/* This function returns the Emacs executable. */ +/* This function returns the Emacs executable. DUMP_FILE is ignored + outside of Android. Otherwise, it is the name of the dump file to + use, or NULL if Emacs should look for a ``--dump-file'' argument + instead. */ + static char * -load_pdump (int argc, char **argv) +load_pdump (int argc, char **argv, char *dump_file) { #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY - char *dump_file = NULL; int skip_args = 0, result; while (skip_args < argc - 1) { - if (argmatch (argv, argc, "-dump-file", "--dump-file", 6, - &dump_file, &skip_args) - || argmatch (argv, argc, "--", NULL, 2, NULL, &skip_args)) + if (argmatch (argv, argc, "-dump-file", "--dump-file", + 6, &dump_file, &skip_args) + || argmatch (argv, argc, "--", NULL, 2, NULL, + &skip_args)) break; skip_args++; } @@ -933,7 +937,7 @@ load_pdump (int argc, char **argv) /* Look for an explicitly-specified dump file. */ const char *path_exec = PATH_EXEC; - char *dump_file = NULL; + dump_file = NULL; int skip_args = 0; while (skip_args < argc - 1) { @@ -1276,7 +1280,7 @@ maybe_load_seccomp (int argc, char **argv) #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY int -android_emacs_init (int argc, char **argv) +android_emacs_init (int argc, char **argv, char *dump_file) #else int main (int argc, char **argv) @@ -1286,6 +1290,12 @@ main (int argc, char **argv) for pointers. */ void *stack_bottom_variable; int old_argc; +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + char *dump_file; + + /* This is just a dummy argument used to avoid extra defines. */ + dump_file = NULL; +#endif /* First, check whether we should apply a seccomp filter. This should come at the very beginning to allow the filter to protect @@ -1415,7 +1425,7 @@ main (int argc, char **argv) #ifdef HAVE_PDUMPER if (attempt_load_pdump) - initial_emacs_executable = load_pdump (argc, argv); + initial_emacs_executable = load_pdump (argc, argv, dump_file); #else ptrdiff_t bufsize; initial_emacs_executable = find_emacs_executable (argv[0], &bufsize); diff --git a/src/sfntfont-android.c b/src/sfntfont-android.c index bc8f1b35b84..ab90a2e4ecd 100644 --- a/src/sfntfont-android.c +++ b/src/sfntfont-android.c @@ -729,6 +729,9 @@ syms_of_sfntfont_android_for_pdumper (void) void init_sfntfont_android (void) { + if (!android_init_gui) + return; + /* Make sure to pick the right Sans Serif font depending on what version of Android the device is running. */ #if HAVE_DECL_ANDROID_GET_DEVICE_API_LEVEL -- cgit v1.3 From 22f7ad1057e1a1e20933e0a1ff2a858ecd9e3fec Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 26 Jan 2023 19:54:38 +0800 Subject: Update Android port * INSTALL.android: Document how to install sqlite3. * build-aux/ndk-build-helper-1.mk (SYSTEM_LIBRARIES): * build-aux/ndk-build-helper-2.mk (SYSTEM_LIBRARIES): Add liblog and libandroid. * configure.ac (SQLITE3_LIBS, HAVE_SQLITE3) (HAVE_SQLITE3_LOAD_EXTENSION): Support on Android. (APKSIGNER): Look for this new required binary. * cross/ndk-build/ndk-build-shared-library.mk (objname): * cross/ndk-build/ndk-build-static-library.mk (objname): Avoid duplicate rules by prefixing objects with module type. * cross/ndk-build/ndk-build.mk.in (NDK_BUILD_SHARED): Fix definition. * cross/ndk-build/ndk-resolve.mk: (NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE)): Handle new system libraries. * doc/emacs/android.texi (Android File System): Document Android 10 system restriction. * java/AndroidManifest.xml.in: Target Android 33, not 28. * java/Makefile.in (SIGN_EMACS_V2, APKSIGNER): New variables. ($(APK_NAME)): Make sure to apply a ``version 2 signature'' to the package as well. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New argument apiLevel. * java/org/gnu/emacs/EmacsNoninteractive.java (main): * java/org/gnu/emacs/EmacsThread.java (run): Pass API level. * m4/ndk-build.m4 (ndk_package_mape): Add package mapping for sqlite3. * src/Makefile.in (SQLITE3_CFLAGS): New substition. (EMACS_CFLAGS): Add that variable. * src/android.c (android_api_level): New variable. (initEmacs): Set it. (android_file_access_p): Make static. (android_hack_asset_fd): Adjust for restrictions in Android 29 and later. (android_close_on_exec): New function. (android_open): Adjust to not duplicate file descriptor even if CLOEXEC. (android_faccessat): Use fstatat at-func emulation. * src/android.h: Update prototypes. * src/dired.c (file_name_completion_dirp): * src/fileio.c (file_access_p, Faccess_file): Now that sys_faccessat takes care of everything, stop calling android_file_access_p. --- INSTALL.android | 18 +++ build-aux/ndk-build-helper-1.mk | 2 +- build-aux/ndk-build-helper-2.mk | 2 +- configure.ac | 69 ++++++--- cross/ndk-build/ndk-build-shared-library.mk | 7 +- cross/ndk-build/ndk-build-static-library.mk | 2 +- cross/ndk-build/ndk-build.mk.in | 2 +- cross/ndk-build/ndk-resolve.mk | 30 +++- doc/emacs/android.texi | 28 +++- java/AndroidManifest.xml.in | 2 +- java/Makefile.in | 9 +- java/org/gnu/emacs/EmacsNative.java | 7 +- java/org/gnu/emacs/EmacsNoninteractive.java | 3 +- java/org/gnu/emacs/EmacsThread.java | 5 +- m4/ndk-build.m4 | 1 + src/Makefile.in | 3 +- src/android.c | 220 ++++++++++++++++++++-------- src/android.h | 1 - src/dired.c | 6 - src/fileio.c | 12 -- 20 files changed, 305 insertions(+), 124 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/INSTALL.android b/INSTALL.android index ed0bd0f68dc..06211e5ec93 100644 --- a/INSTALL.android +++ b/INSTALL.android @@ -165,6 +165,9 @@ work, along with what has to be patched to make them work: and apply the patch at the end of this file.) icu4c - https://android.googlesource.com/platform/external/icu/ (You must apply the patch at the end of this file.) + sqlite3 - https://android.googlesource.com/platform/external/sqlite/ + (You must apply the patch at the end of this file, and add the `dist' + directory to ``--with-ndk-path''.) Many of these dependencies have been migrated over to the ``Android.bp'' build system now used to build Android itself. @@ -592,3 +595,18 @@ index 8e5f757..44bb130 100644 LOCAL_MODULE_TAGS := optional LOCAL_MODULE := libicuuc LOCAL_RTTI_FLAG := -frtti + +PATCH FOR SQLITE3 + +diff --git a/dist/Android.mk b/dist/Android.mk +index bf277d2..36734d9 100644 +--- a/dist/Android.mk ++++ b/dist/Android.mk +@@ -141,6 +141,7 @@ include $(BUILD_HOST_EXECUTABLE) + include $(CLEAR_VARS) + LOCAL_SRC_FILES := $(common_src_files) + LOCAL_CFLAGS += $(minimal_sqlite_flags) ++LOCAL_EXPORT_C_INCLUDES += $(LOCAL_PATH) + LOCAL_MODULE:= libsqlite_static_minimal + LOCAL_SDK_VERSION := 23 + include $(BUILD_STATIC_LIBRARY) diff --git a/build-aux/ndk-build-helper-1.mk b/build-aux/ndk-build-helper-1.mk index 4ffe0d423e4..9035b5ca59a 100644 --- a/build-aux/ndk-build-helper-1.mk +++ b/build-aux/ndk-build-helper-1.mk @@ -77,7 +77,7 @@ endef # dependencies can be ignored while building a shared library, as they # will be linked in to the resulting shared object file later. -SYSTEM_LIBRARIES = z libz libc c libdl dl stdc++ libstdc++ +SYSTEM_LIBRARIES = z libz libc c libdl dl stdc++ libstdc++ log liblog android libandroid $(foreach module,$(filter-out $(SYSTEM_LIBRARIES), $(LOCAL_SHARED_LIBRARIES)),$(eval $(call add-so-name,$(module)))) $(foreach module,$(filter-out $(SYSTEM_LIBRARIES), $(LOCAL_SHARED_LIBRARIES) $(LOCAL_STATIC_LIBRARIES) $(LOCAL_WHOLE_LIBRARIES)),$(eval $(call add-includes,$(module)))) diff --git a/build-aux/ndk-build-helper-2.mk b/build-aux/ndk-build-helper-2.mk index 9c9e7cf09a9..8f2f397e534 100644 --- a/build-aux/ndk-build-helper-2.mk +++ b/build-aux/ndk-build-helper-2.mk @@ -87,7 +87,7 @@ endef # Resolve additional dependencies based on LOCAL_STATIC_LIBRARIES and # LOCAL_SHARED_LIBRARIES. -SYSTEM_LIBRARIES = z libz libc c libdl dl libstdc++ stdc++ +SYSTEM_LIBRARIES = z libz libc c libdl dl libstdc++ stdc++ log liblog android libandroid $(foreach module,$(filter-out $(SYSTEM_LIBRARIES), $(LOCAL_STATIC_LIBRARIES) $(LOCAL_WHOLE_STATIC_LIBRARIES)),$(eval $(call add-a-name,$(module)))) $(foreach module,$(filter-out $(SYSTEM_LIBRARIES), $(LOCAL_SHARED_LIBRARIES)),$(eval $(call add-so-name,$(module)))) diff --git a/configure.ac b/configure.ac index 95bb0cfca88..879e4ab74aa 100644 --- a/configure.ac +++ b/configure.ac @@ -752,6 +752,7 @@ ANDROID= JAVAC= AAPT= JARSIGNER= +APKSIGNER= ZIPALIGN= DX= ANDROID_JAR= @@ -763,6 +764,7 @@ android_makefiles="lib/Makefile lib/gnulib.mk lib-src/Makefile src/Makefile" AC_ARG_VAR([JAVAC], [Java compiler path. Used for Android.]) AC_ARG_VAR([JARSIGNER], [Java package signer path. Used for Android.]) +AC_ARG_VAR([APKSIGNER], [Android package signer path. Used for Android.]) AC_ARG_VAR([SDK_BULD_TOOLS], [Path to the Android SDK build tools.]) if test "$with_android" = "yes"; then @@ -792,6 +794,7 @@ the path to your Java compiler before configuring Emacs, like so: JAVAC=/opt/jdk/bin/javac ./configure --with-android]) fi + AC_CHECK_PROGS([JARSIGNER], [jarsigner]) if test "$JARSIGNER" = ""; then AC_MSG_ERROR([The Java package signing utility was not found. @@ -861,6 +864,12 @@ Android 13 (Tiramisu) or later.]) Please verify that the path to the SDK build tools you specified is correct]) fi + AC_PATH_PROGS([APKSIGNER], [apksigner], [], "${SDK_BUILD_TOOLS}:$PATH") + if test "$APKSIGNER" = ""; then + AC_MSG_ERROR([The Android package signing tool was not found. +Please verify that the path to the SDK build tools you specified is correct]) + fi + AC_PATH_PROGS([D8], [d8], [], "${SDK_BUILD_TOOLS}:$PATH") if test "D8" = ""; then AC_MSG_ERROR([The Android dexer was not found. @@ -1051,6 +1060,7 @@ package will likely install on older systems but crash on startup.]) passthrough="$passthrough --with-json=$with_json" passthrough="$passthrough --with-jpeg=$with_jpeg" passthrough="$passthrough --with-xml2=$with_xml2" + passthrough="$passthrough --with-sqlite3=$with_sqlite3" AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" \ ANDROID_SDK="$android_sdk" android_abi=$android_abi \ @@ -1120,10 +1130,10 @@ if test "$ANDROID" = "yes"; then with_json=no with_jpeg=no with_xml2=no + with_sqlite3=no fi with_rsvg=no - with_sqlite3=no with_lcms2=no with_libsystemd=no with_cairo=no @@ -3348,30 +3358,49 @@ fi ### Use -lsqlite3 if available, unless '--with-sqlite3=no' HAVE_SQLITE3=no +SQLITE3_LIBS= +SQLITE3_CFLAGS= if test "${with_sqlite3}" != "no"; then - AC_CHECK_LIB([sqlite3], [sqlite3_open_v2], - [HAVE_SQLITE3=yes], - [HAVE_SQLITE3=no]) - if test "$HAVE_SQLITE3" = "yes"; then - SQLITE3_LIBS=-lsqlite3 - AC_SUBST([SQLITE3_LIBS]) - LIBS="$SQLITE3_LIBS $LIBS" - AC_DEFINE([HAVE_SQLITE3], [1], - [Define to 1 if you have the libsqlite3 library (-lsqlite).]) - # Windows loads libsqlite dynamically - if test "${opsys}" = "mingw32"; then - SQLITE3_LIBS= + if test "${REALLY_ANDROID}" = "yes"; then + ndk_SEARCH_MODULE([sqlite3], [SQLITE3], [HAVE_SQLITE3=yes]) + + if test "$HAVE_SQLITE3" = "yes"; then + SAVE_CFLAGS="$CFLAGS" + CFLAGS="$CFLAGS $SQLITE3_CFLAGS" + AC_CHECK_DECL([sqlite3_open_v2], [HAVE_SQLITE=yes], + [HAVE_SQLITE3=no], [#include ]) + CFLAGS="$SAVE_CFLAGS" fi - AC_CHECK_LIB([sqlite3], [sqlite3_load_extension], - [HAVE_SQLITE3_LOAD_EXTENSION=yes], - [HAVE_SQLITE3_LOAD_EXTENSION=no]) - if test "$HAVE_SQLITE3_LOAD_EXTENSION" = "yes"; then - AC_DEFINE([HAVE_SQLITE3_LOAD_EXTENSION], [1], - [Define to 1 if sqlite3 supports loading extensions.]) + else + AC_CHECK_LIB([sqlite3], [sqlite3_open_v2], + [HAVE_SQLITE3=yes], + [HAVE_SQLITE3=no]) + if test "$HAVE_SQLITE3" = "yes"; then + SQLITE3_LIBS=-lsqlite3 + LIBS="$SQLITE3_LIBS $LIBS" + # Windows loads libsqlite dynamically + if test "${opsys}" = "mingw32"; then + SQLITE3_LIBS= + fi + AC_CHECK_LIB([sqlite3], [sqlite3_load_extension], + [HAVE_SQLITE3_LOAD_EXTENSION=yes], + [HAVE_SQLITE3_LOAD_EXTENSION=no]) + if test "$HAVE_SQLITE3_LOAD_EXTENSION" = "yes"; then + AC_DEFINE([HAVE_SQLITE3_LOAD_EXTENSION], [1], + [Define to 1 if sqlite3 supports loading extensions.]) + fi fi - fi + fi + + if test "$HAVE_SQLITE3" = "yes"; then + AC_DEFINE([HAVE_SQLITE3], [1], + [Define to 1 if you have the libsqlite3 library (-lsqlite).]) + fi fi +AC_SUBST([SQLITE3_LIBS]) +AC_SUBST([SQLITE3_CFLAGS]) + HAVE_IMAGEMAGICK=no if test "${HAVE_X11}" = "yes" || test "${HAVE_NS}" = "yes" || test "${HAVE_W32}" = "yes" || \ test "${HAVE_BE_APP}" = "yes" || test "${window_system}" = "pgtk"; then diff --git a/cross/ndk-build/ndk-build-shared-library.mk b/cross/ndk-build/ndk-build-shared-library.mk index a4b7b47f749..b69641ba9b0 100644 --- a/cross/ndk-build/ndk-build-shared-library.mk +++ b/cross/ndk-build/ndk-build-shared-library.mk @@ -20,7 +20,12 @@ # which actually builds targets. eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) -objname = $(1)-$(subst /,_,$(2).o) + +# Objects for shared libraries are prefixed with `-shared-' in +# addition to the name of the module, because a common practice in +# Android.mk files written by Google is to define two modules with the +# same name but of different types. +objname = $(1)-shared-$(subst /,_,$(2).o) # Here are the default flags to link shared libraries with. NDK_SO_DEFAULT_LDFLAGS := -lc -lm diff --git a/cross/ndk-build/ndk-build-static-library.mk b/cross/ndk-build/ndk-build-static-library.mk index 4d16d81330c..349b9242b1f 100644 --- a/cross/ndk-build/ndk-build-static-library.mk +++ b/cross/ndk-build/ndk-build-static-library.mk @@ -20,7 +20,7 @@ # which actually builds targets. eq = $(and $(findstring $(1),$(2)),$(findstring $(2),$(1))) -objname = $(1)-$(subst /,_,$(2).o) +objname = $(1)-static-$(subst /,_,$(2).o) define single-object-target diff --git a/cross/ndk-build/ndk-build.mk.in b/cross/ndk-build/ndk-build.mk.in index 798dcf6c19e..5b0aa82856d 100644 --- a/cross/ndk-build/ndk-build.mk.in +++ b/cross/ndk-build/ndk-build.mk.in @@ -38,7 +38,7 @@ NDK_BUILD_MODULES := $(call uniqify,$(NDK_BUILD_MODULES)) # requires the C++ standard library. ifneq ($(NDK_BUILD_ANY_CXX_MODULE),) -NDK_BUILD_SHARED += $(NDK_BUILD_ANY_CXX_SHARED) +NDK_BUILD_SHARED += $(NDK_BUILD_CXX_SHARED) endif define subr-1 diff --git a/cross/ndk-build/ndk-resolve.mk b/cross/ndk-build/ndk-resolve.mk index 910be8dab53..c2e281c53ba 100644 --- a/cross/ndk-build/ndk-resolve.mk +++ b/cross/ndk-build/ndk-resolve.mk @@ -43,32 +43,50 @@ NDK_CFLAGS_$(LOCAL_MODULE) += $(addprefix -I,$(NDK_LOCAL_EXPORT_C_INCLUDES_$(1)) # If the module happens to be zlib, then add -lz to the shared library # flags. -ifneq ($(strip $(1)),libz) +ifeq ($(strip $(1)),libz) NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -lz endif -ifneq ($(strip $(1)),z) +ifeq ($(strip $(1)),z) NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -lz endif # Likewise for libdl. -ifneq ($(strip $(1)),libdl) +ifeq ($(strip $(1)),libdl) NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -ldl endif -ifneq ($(strip $(1)),dl) +ifeq ($(strip $(1)),dl) NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -ldl endif # Likewise for libstdc++. -ifneq ($(strip $(1)),libstdc++) +ifeq ($(strip $(1)),libstdc++) NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -lstdc++ endif -ifneq ($(strip $(1)),dl) +ifeq ($(strip $(1)),dl) NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -lstdc++ endif +# Likewise for liblog. +ifeq ($(strip $(1)),liblog) +NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -llog +endif + +ifeq ($(strip $(1)),log) +NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -llog +endif + +# Likewise for libandroid. +ifeq ($(strip $(1)),libandroid) +NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -landroid +endif + +ifeq ($(strip $(1)),android) +NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE) += -landroid +endif + ifneq ($(2),) ifneq ($(findstring lib,$(1)),) NDK_LOCAL_A_NAMES_$(LOCAL_MODULE) += $(1).a diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index e910e482ad8..98d7f1e1d9e 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -160,13 +160,35 @@ The @dfn{app library} directory. This is automatically appended to @item The @dfn{external storage} directory. This is accessible to Emacs -when the user grants the @code{Files and media} permission to Emacs -via system settings. +when the user grants the ``Files and Media'' permission to Emacs via +system settings. @end itemize -The external storage directory is found at @file{/sdcard}; the other + The external storage directory is found at @file{/sdcard}; the other directories are not found at any fixed location. +@cindex file system limitations, Android 10 + On Android 10 and later, the Android system restricts applications +from accessing files in the @file{/sdcard} directory using +file-related system calls such as @code{open} and @code{readdir}. + + This restriction is known as ``Scoped Storage'', and supposedly +makes the system more secure. Unfortunately, it also means that Emacs +cannot access files in those directories, despite holding the +necessary permissions. Thankfully, the Open Handset Alliance's +version of Android allows this restriction to be disabled on a +per-program basis; the corresponding option in the system settings +panel is: + +@indentedblock +System -> Developer Options -> App Compatibility Changes -> Emacs -> +DEFAULT_SCOPED_STORAGE +@end indentedblock + + After you disable this setting and grant Emacs the ``Files and +Media'' permission, it will be able to access files under +@file{/sdcard} as usual. + @node Android Environment @section Running Emacs under Android diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index c4a9d1f5177..527ce74c474 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -53,7 +53,7 @@ along with GNU Emacs. If not, see . --> + android:targetSdkVersion="33"/> = ANDROID_MAX_ASSET_FD || fd < 0) { - /* Too bad. You lose. */ + /* Too bad. Pretend this is an out of memory error. */ errno = ENOMEM; if (fd >= 0) @@ -1713,7 +1791,7 @@ android_init_emacs_window (void) extern JNIEXPORT void JNICALL NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, - jobject dump_file_object) + jobject dump_file_object, jint api_level) { char **c_argv; jsize nelements, i; @@ -1732,6 +1810,9 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, /* Trick GCC into not optimizing this variable away. */ unused_pointer = buffer; + /* Set the Android API level. */ + android_api_level = api_level; + android_java_env = env; nelements = (*env)->GetArrayLength (env, argv); @@ -4214,34 +4295,47 @@ android_window_updated (android_window window, unsigned long serial) -#if __ANDROID_API__ >= 16 +/* When calling the system's faccessat, make sure to clear the flag + AT_EACCESS. -/* Replace the system faccessat with one which understands AT_EACCESS. Android's faccessat simply fails upon using AT_EACCESS, so replace - it with zero here. This isn't caught during configuration. + it with zero here. This isn't caught during configuration as Emacs + is being cross compiled. This replacement is only done when building for Android 16 or later, because earlier versions use the gnulib replacement that - lacks these issues. */ + lacks these issues. + + This is unnecessary on earlier API versions, as gnulib's + rpl_faccessat will be used instead, which lacks these problems. */ + +/* Like faccessat, except it also understands DIRFD opened using + android_dirfd. */ int android_faccessat (int dirfd, const char *pathname, int mode, int flags) { - return faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); -} + const char *asset; -#else /* __ANDROID_API__ < 16 */ + if (dirfd != AT_FDCWD) + dirfd + = android_lookup_asset_directory_fd (dirfd, &pathname, + pathname); -/* This is unnecessary on earlier API versions, as gnulib's - rpl_faccessat will be used instead. */ + /* Check if pathname is actually an asset. If that is the case, + simply fall back to android_file_access_p. */ -int -android_faccessat (int dirfd, const char *pathname, int mode, int flags) -{ - return faccessat (dirfd, pathname, mode, flags); -} + if (dirfd == AT_FDCWD + && asset_manager + && (asset = android_get_asset_name (pathname))) + return !android_file_access_p (asset, mode); +#if __ANDROID_API__ >= 16 + return faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); +#else + return faccessat (dirfd, pathname, mode, flags); #endif +} @@ -4468,10 +4562,10 @@ android_closedir (struct android_dir *dir) xfree (dir); } -/* Subroutine used by android_fstatat. If DIRFD belongs to an open - asset directory and FILE is a relative file name, then return - AT_FDCWD and the absolute file name of the directory prepended to - FILE in *PATHNAME. Else, return DIRFD. */ +/* Subroutine used by android_fstatat and android_faccessat. If DIRFD + belongs to an open asset directory and FILE is a relative file + name, then return AT_FDCWD and the absolute file name of the + directory prepended to FILE in *PATHNAME. Else, return DIRFD. */ int android_lookup_asset_directory_fd (int dirfd, diff --git a/src/android.h b/src/android.h index 33fad512d4a..8234dbb07c0 100644 --- a/src/android.h +++ b/src/android.h @@ -46,7 +46,6 @@ extern int android_emacs_init (int, char **, char *); extern int android_select (int, fd_set *, fd_set *, fd_set *, struct timespec *); -extern bool android_file_access_p (const char *, int); extern int android_open (const char *, int, int); extern char *android_user_full_name (struct passwd *); extern int android_fstat (int, struct stat *); diff --git a/src/dired.c b/src/dired.c index b38416e981a..93487d552e2 100644 --- a/src/dired.c +++ b/src/dired.c @@ -888,12 +888,6 @@ file_name_completion_dirp (int fd, struct dirent *dp, ptrdiff_t len) memcpy (subdir_name, dp->d_name, len); strcpy (subdir_name + len, "/"); -#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY - /* Check if subdir_name lies in the assets directory. */ - if (android_file_access_p (subdir_name, F_OK)) - return true; -#endif - bool dirp = sys_faccessat (fd, subdir_name, F_OK, AT_EACCESS) == 0; SAFE_FREE (); diff --git a/src/fileio.c b/src/fileio.c index 6f25506dbc2..71f83ea6daf 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -182,12 +182,6 @@ file_access_p (char const *file, int amode) } #endif -#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY - /* FILE may be some kind of special Android file. */ - if (android_file_access_p (file, amode)) - return true; -#endif - if (sys_faccessat (AT_FDCWD, file, amode, AT_EACCESS) == 0) return true; @@ -3018,12 +3012,6 @@ If there is no error, returns nil. */) encoded_filename = ENCODE_FILE (absname); -#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY - /* FILE may be some kind of special Android file. */ - if (android_file_access_p (SSDATA (encoded_filename), R_OK)) - return Qnil; -#endif - if (sys_faccessat (AT_FDCWD, SSDATA (encoded_filename), R_OK, AT_EACCESS) != 0) report_file_error (SSDATA (string), filename); -- cgit v1.3 From b0e7ae6d5b68a56da40256c395141f071172a622 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 26 Jan 2023 22:11:04 +0800 Subject: Update Android port * INSTALL.android: Describe that apksigner is also required. * configure.ac: Correctly add cross/Makefile to SUBDIR_MAKEFILES. * cross/Makefile.in: (config.status): Depend on $(top_srcdir)/config.status. * doc/emacs/input.texi (On-Screen Keyboards): Document how to quit without a physical keyboard. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function `quit'. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow): New field `lastVolumeButtonPress'. (onKeyDown): Quit if necessary. * m4/ndk-build.m4 (ndk_where_cc): Fix search if CC is not a single word. * src/android.c (android_open): Remove unused variable. (quit): New function. * src/androidmenu.c (android_process_events_for_menu): Allow quitting the menu. * src/xterm.c (handle_one_xevent, x_term_init, syms_of_xterm): Implement features described above, so they work on free operating systems. * src/xterm.h (struct x_display_info): New fields `quit_keysym', `quit_keysym_time'. --- INSTALL.android | 4 +- configure.ac | 6 +-- cross/Makefile.in | 5 ++- doc/emacs/input.texi | 12 ++++++ java/org/gnu/emacs/EmacsNative.java | 4 ++ java/org/gnu/emacs/EmacsWindow.java | 19 +++++++++ m4/ndk-build.m4 | 2 +- src/android.c | 8 +++- src/androidmenu.c | 6 +++ src/xterm.c | 80 ++++++++++++++++++++++++++++++++++++- src/xterm.h | 7 ++++ 11 files changed, 141 insertions(+), 12 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/INSTALL.android b/INSTALL.android index 06211e5ec93..e5d7162140d 100644 --- a/INSTALL.android +++ b/INSTALL.android @@ -40,8 +40,8 @@ Replacing the paths in the command line above with: are building Emacs to run on. - the path to the directory in the Android SDK containing binaries - such as `aapt' and `d8'. These are used to build the application - package. + such as `aapt', `apksigner', and `d8'. These are used to build + the application package. After the configuration process completes, you may run: diff --git a/configure.ac b/configure.ac index 879e4ab74aa..aaee65016b2 100644 --- a/configure.ac +++ b/configure.ac @@ -7411,11 +7411,7 @@ if test -f "$srcdir/$opt_makefile.in"; then fi if test "$ANDROID" = "yes"; then - SUBDIR_MAKEFILES="$SUBDIR_MAKEFILES java/Makefile" -fi - -if test "$XCOMPILE" = "yes"; then - SUBDIR_MAKEFILES="$SUBDIR_MAKEFILES cross/Makefile" + SUBDIR_MAKEFILES="$SUBDIR_MAKEFILES java/Makefile cross/Makefile" fi dnl The admin/ directory used to be excluded from tarfiles. diff --git a/cross/Makefile.in b/cross/Makefile.in index 92f8d068975..a44550c6563 100644 --- a/cross/Makefile.in +++ b/cross/Makefile.in @@ -60,8 +60,9 @@ all: lib/libgnu.a src/libemacs.so src/android-emacs $(LIBSRC_BINARIES) # This Makefile relies on builddir and top_builddir being relative # paths in *.android. -# This file is used to trick lib/gnulib.mk, it is not actually useful. -config.status: +# This file is used to tell lib/gnulib.mk when +# $(top_srcdir)/config.status changes. +config.status: $(top_srcdir)/config.status touch config.status src/verbose.mk: verbose.mk.android diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index 1a58d1ca0ac..3894d4872e0 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -94,3 +94,15 @@ that the user is about to enter text in to the current buffer. Emacs also provides a set of functions to show or hide the on-screen keyboard. For more details, @pxref{On-Screen Keyboards,,, elisp, The Emacs Lisp Reference Manual}. + +@cindex quitting, without a keyboard + Since it may not be possible for Emacs to display the on screen +keyboard when it is executing a command, Emacs implements a feature on +devices with only an on-screen keyboard, by which two rapid clicks of +a hardware button that is always present on the device results in +Emacs quitting. @xref{Quitting}. + +@defvar x-quit-keysym + The exact button is used to do this varies by system: on X, it is +defined in the variable @code{x-quit-keysym}, and on Android, it is +always the volume down button. diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 7bf8b5f6081..4e91a7be322 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -74,6 +74,10 @@ public class EmacsNative /* Abort and generate a native core dump. */ public static native void emacsAbort (); + /* Set Vquit_flag to t, resulting in Emacs quitting as soon as + possible. */ + public static native void quit (); + /* Send an ANDROID_CONFIGURE_NOTIFY event. The values of all the functions below are the serials of the events sent. */ public static native long sendConfigureNotify (short window, long time, diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 8511af9193e..39eaf2fff80 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -124,6 +124,10 @@ public class EmacsWindow extends EmacsHandleObject there is no such window manager. */ private WindowManager windowManager; + /* The time of the last KEYCODE_VOLUME_DOWN press. This is used to + quit Emacs. */ + private long lastVolumeButtonPress; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height, boolean overrideRedirect) @@ -513,6 +517,7 @@ public class EmacsWindow extends EmacsHandleObject onKeyDown (int keyCode, KeyEvent event) { int state, state_1; + long time; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -544,6 +549,20 @@ public class EmacsWindow extends EmacsHandleObject state, keyCode, event.getUnicodeChar (state_1)); lastModifiers = state; + + if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) + { + /* Check if this volume down press should quit Emacs. + Most Android devices have no physical keyboard, so it + is unreasonably hard to press C-g. */ + + time = event.getEventTime (); + + if (lastVolumeButtonPress - time < 350) + EmacsNative.quit (); + + lastVolumeButtonPress = time; + } } public void diff --git a/m4/ndk-build.m4 b/m4/ndk-build.m4 index bcfe0fed6fe..0ab6197d735 100644 --- a/m4/ndk-build.m4 +++ b/m4/ndk-build.m4 @@ -166,7 +166,7 @@ that could not be found in the list of directories specified in \ } # Look for a suitable ar in the same directory as the C compiler. -ndk_where_cc=$(which $CC) +ndk_where_cc=$(which $(echo "$CC" | awk -- "{ print \[$]1 }")) ndk_ar_search_path=$PATH # First, try to find $host_alias-ar in PATH. diff --git a/src/android.c b/src/android.c index 1676cbf9942..379b54a65be 100644 --- a/src/android.c +++ b/src/android.c @@ -1228,7 +1228,7 @@ android_open (const char *filename, int oflag, int mode) { const char *name; AAsset *asset; - int fd, oldfd; + int fd; off_t out_start, out_length; if (asset_manager && (name = android_get_asset_name (filename))) @@ -1889,6 +1889,12 @@ NATIVE_NAME (emacsAbort) (JNIEnv *env, jobject object) emacs_abort (); } +extern JNIEXPORT void JNICALL +NATIVE_NAME (quit) (JNIEnv *env, jobject object) +{ + Vquit_flag = Qt; +} + extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, jshort window, jlong time, diff --git a/src/androidmenu.c b/src/androidmenu.c index f65b5d3ffd1..7b27825ad60 100644 --- a/src/androidmenu.c +++ b/src/androidmenu.c @@ -187,6 +187,12 @@ android_process_events_for_menu (int *id) /* Process pending signals. */ process_pending_signals (); + + /* Maybe quit. This is important because the framework (on + Android 4.0.3) can sometimes fail to deliver context menu + closed events if a submenu was opened, and the user still + needs to be able to quit. */ + maybe_quit (); } /* Restore the input block. */ diff --git a/src/xterm.c b/src/xterm.c index 1325d923be9..eeefed34d4b 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -20103,6 +20103,24 @@ handle_one_xevent (struct x_display_info *dpyinfo, } #endif + /* See if keysym should make Emacs quit. */ + + if (keysym == dpyinfo->quit_keysym + && (xkey.time - dpyinfo->quit_keysym_time + <= 350)) + { + Vquit_flag = Qt; + goto done_keysym; + } + + if (keysym == dpyinfo->quit_keysym) + { + /* Otherwise, set the last time that keysym was + pressed. */ + dpyinfo->quit_keysym_time = xkey.time; + goto done_keysym; + } + /* If not using XIM/XIC, and a compose sequence is in progress, we break here. Otherwise, chars_matched is always 0. */ if (compose_status.chars_matched > 0 && nbytes == 0) @@ -23851,6 +23869,24 @@ handle_one_xevent (struct x_display_info *dpyinfo, } #endif + /* See if keysym should make Emacs quit. */ + + if (keysym == dpyinfo->quit_keysym + && (xev->time - dpyinfo->quit_keysym_time + <= 350)) + { + Vquit_flag = Qt; + goto xi_done_keysym; + } + + if (keysym == dpyinfo->quit_keysym) + { + /* Otherwise, set the last time that keysym was + pressed. */ + dpyinfo->quit_keysym_time = xev->time; + goto xi_done_keysym; + } + /* First deal with keysyms which have defined translations to characters. */ if (keysym >= 32 && keysym < 128) @@ -29855,6 +29891,7 @@ x_term_init (Lisp_Object display_name, char *xrm_option, char *resource_name) struct terminal *terminal; struct x_display_info *dpyinfo; XrmDatabase xrdb; + Lisp_Object tem, quit_keysym; #ifdef USE_XCB xcb_connection_t *xcb_conn; #endif @@ -29865,7 +29902,7 @@ x_term_init (Lisp_Object display_name, char *xrm_option, char *resource_name) GdkScreen *gscr; #endif #ifdef HAVE_XFIXES - Lisp_Object tem, lisp_name; + Lisp_Object lisp_name; int num_fast_selections; Atom selection_name; #ifdef USE_XCB @@ -30142,6 +30179,28 @@ x_term_init (Lisp_Object display_name, char *xrm_option, char *resource_name) terminal->kboard->reference_count++; } + /* Now look through Vx_quit_keysym for the quit keysym associated + with this display. */ + tem = Vx_quit_keysym; + FOR_EACH_TAIL_SAFE (tem) + { + quit_keysym = XCAR (tem); + + /* Check if its car is a string and its cdr a valid keysym. + Skip if it is not. */ + + if (!CONSP (quit_keysym) || !FIXNUMP (XCDR (quit_keysym)) + || !STRINGP (XCAR (quit_keysym))) + continue; + + /* Check if this is the keysym to be used. */ + + if (strcmp (SSDATA (XCAR (quit_keysym)), ServerVendor (dpy))) + continue; + + dpyinfo->quit_keysym = XFIXNUM (XCDR (quit_keysym)); + } + /* Put this display on the chain. */ dpyinfo->next = x_display_list; x_display_list = dpyinfo; @@ -32200,4 +32259,23 @@ frame placement via frame parameters, `set-frame-position', and `set-frame-size', along with the actual state of a frame after `x_make_frame_invisible'. */); Vx_lax_frame_positioning = Qnil; + + DEFVAR_LISP ("x-quit-keysym", Vx_quit_keysym, + doc: /* Keysyms which will cause Emacs to quit if rapidly pressed twice. + +This is used to support quitting on devices that do not have any kind +of physical keyboard, or where the physical keyboard is incapable of +entering `C-g'. It defaults to `XF86XK_AudioLowerVolume' on XFree86 +and X.Org servers, and is unset. + +The value is an alist associating between strings, describing X server +vendor names, and a single number describing the keysym to use. The +keysym to use for each display connection is determined upon +connection setup, and does not reflect further changes to this +variable. */); + Vx_quit_keysym + = list2 (Fcons (build_string ("The X.Org Foundation"), + make_int (269025041)), + Fcons (build_string ("The XFree86 Project, Inc."), + make_int (269025041))); } diff --git a/src/xterm.h b/src/xterm.h index 28ae00ca190..406a7c5c060 100644 --- a/src/xterm.h +++ b/src/xterm.h @@ -920,6 +920,13 @@ struct x_display_info server_time_monotonic_p will be true). */ int_fast64_t server_time_offset; #endif + + /* Keysym that will cause Emacs to quit if pressed twice within 150 + ms. */ + KeySym quit_keysym; + + /* The last time that keysym was pressed. */ + Time quit_keysym_time; }; #ifdef HAVE_X_I18N -- cgit v1.3 From 420533a8f9b345699dad9eeafeb3ccecfed516b2 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 4 Feb 2023 23:32:07 +0800 Subject: Add emacsclient desktop file equivalent on Android * doc/emacs/android.texi (Android File System): * java/AndroidManifest.xml.in: Update with new activity. Remove Android 10 restrictions through a special flag. * java/org/gnu/emacs/EmacsNative.java (getProcName): New function. * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity): New file. * java/org/gnu/emacs/EmacsService.java (getLibraryDirection): Remove unused annotation. * lib-src/emacsclient.c (decode_options): Set alt_display on Android. * src/android.c (android_proc_name): New function. (NATIVE_NAME): Export via JNI. --- doc/emacs/android.texi | 16 +- java/AndroidManifest.xml.in | 79 +++++++ java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsOpenActivity.java | 357 ++++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsService.java | 1 - lib-src/emacsclient.c | 2 + src/android.c | 44 ++++ 7 files changed, 490 insertions(+), 13 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsOpenActivity.java (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index cfdf77454eb..9dc6fddbb72 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -167,8 +167,8 @@ system settings. The external storage directory is found at @file{/sdcard}; the other directories are not found at any fixed location. -@cindex file system limitations, Android 10 - On Android 10 and later, the Android system restricts applications +@cindex file system limitations, Android 11 + On Android 11 and later, the Android system restricts applications from accessing files in the @file{/sdcard} directory using file-related system calls such as @code{open} and @code{readdir}. @@ -177,16 +177,8 @@ makes the system more secure. Unfortunately, it also means that Emacs cannot access files in those directories, despite holding the necessary permissions. Thankfully, the Open Handset Alliance's version of Android allows this restriction to be disabled on a -per-program basis; on Android 10, the corresponding option in the -system settings panel is: - -@indentedblock -System -> Developer Options -> App Compatibility Changes -> Emacs -> -DEFAULT_SCOPED_STORAGE -@end indentedblock - - And on Android 11 and later, the corresponding option in the systems -settings panel is: +per-program basis; the corresponding option in the system settings +panel is: @indentedblock System -> Apps -> Special App Access -> All files access -> Emacs diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index 544c87e1f1e..923c5a005d5 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -24,6 +24,7 @@ along with GNU Emacs. If not, see . --> package="org.gnu.emacs" android:targetSandboxVersion="1" android:installLocation="auto" + android:requestLegacyExternalStorage="true" android:versionCode="@emacs_major_version@" android:versionName="@version@"> @@ -82,6 +83,84 @@ along with GNU Emacs. If not, see . --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + . */ + +package org.gnu.emacs; + +/* This class makes the Emacs server work reasonably on Android. + + There is no way to make the Unix socket publicly available on + Android. + + Instead, this activity tries to connect to the Emacs server, to + make it open files the system asks Emacs to open, and to emulate + some reasonable behavior when Emacs has not yet started. + + First, Emacs registers itself as an application that can open text + and image files. + + Then, when the user is asked to open a file and selects ``Emacs'' + as the application that will open the file, the system pops up a + window, this activity, and calls the `onCreate' function. + + `onCreate' then tries very to find the file name of the file that + was selected, and give it to emacsclient. + + If emacsclient successfully opens the file, then this activity + starts EmacsActivity (to bring it on to the screen); otherwise, it + displays the output of emacsclient or any error message that occurs + and exits. */ + +import android.app.AlertDialog; +import android.app.Activity; + +import android.content.Context; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.content.Intent; + +import android.net.Uri; + +import android.os.Build; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; + +import java.io.File; +import java.io.FileReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +public class EmacsOpenActivity extends Activity + implements DialogInterface.OnClickListener +{ + private class EmacsClientThread extends Thread + { + private ProcessBuilder builder; + + public + EmacsClientThread (ProcessBuilder processBuilder) + { + builder = processBuilder; + } + + @Override + public void + run () + { + Process process; + InputStream error; + String errorText; + + try + { + /* Start emacsclient. */ + process = builder.start (); + process.waitFor (); + + /* Now figure out whether or not starting the process was + successful. */ + if (process.exitValue () == 0) + finishSuccess (); + else + finishFailure ("Error opening file", null); + } + catch (IOException exception) + { + finishFailure ("Internal error", exception.toString ()); + } + catch (InterruptedException exception) + { + finishFailure ("Internal error", exception.toString ()); + } + } + } + + @Override + public void + onClick (DialogInterface dialog, int which) + { + finish (); + } + + public String + readEmacsClientLog () + { + File file, cache; + FileReader reader; + char[] buffer; + int rc; + String what; + + cache = getCacheDir (); + file = new File (cache, "emacsclient.log"); + what = ""; + + try + { + reader = new FileReader (file); + buffer = new char[2048]; + + while ((rc = reader.read (buffer, 0, 2048)) != -1) + what += String.valueOf (buffer, 0, 2048); + + reader.close (); + return what; + } + catch (IOException exception) + { + return ("Couldn't read emacsclient.log: " + + exception.toString ()); + } + } + + private void + displayFailureDialog (String title, String text) + { + AlertDialog.Builder builder; + AlertDialog dialog; + + builder = new AlertDialog.Builder (this); + dialog = builder.create (); + dialog.setTitle (title); + + if (text == null) + /* Read in emacsclient.log instead. */ + text = readEmacsClientLog (); + + dialog.setMessage (text); + dialog.setButton (DialogInterface.BUTTON_POSITIVE, "OK", this); + dialog.show (); + } + + /* Finish this activity in response to emacsclient having + successfully opened a file. + + In the main thread, close this window, and open a window + belonging to an Emacs frame. */ + + public void + finishSuccess () + { + runOnUiThread (new Runnable () { + @Override + public void + run () + { + Intent intent; + + intent = new Intent (EmacsOpenActivity.this, + EmacsActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity (intent); + + EmacsOpenActivity.this.finish (); + } + }); + } + + /* Finish this activity after displaying a dialog associated with + failure to open a file. + + Use TITLE as the title of the dialog. If TEXT is non-NULL, + display that text in the dialog. Otherwise, use the contents of + emacsclient.log in the cache directory instead. */ + + public void + finishFailure (final String title, final String text) + { + runOnUiThread (new Runnable () { + @Override + public void + run () + { + displayFailureDialog (title, text); + } + }); + } + + public String + getLibraryDirectory () + { + int apiLevel; + Context context; + + context = getApplicationContext (); + apiLevel = Build.VERSION.SDK_INT; + + if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) + return context.getApplicationInfo().nativeLibraryDir; + else if (apiLevel >= Build.VERSION_CODES.DONUT) + return context.getApplicationInfo().dataDir + "/lib"; + + return "/data/data/" + context.getPackageName() + "/lib"; + } + + public void + startEmacsClient (String fileName) + { + String libDir; + ProcessBuilder builder; + Process process; + EmacsClientThread thread; + File file; + + file = new File (getCacheDir (), "emacsclient.log"); + + libDir = getLibraryDirectory (); + builder = new ProcessBuilder (libDir + "/libemacsclient.so", + fileName, "--reuse-frame", + "--timeout=10", "--no-wait"); + + /* Redirect standard error to a file so that errors can be + meaningfully reported. */ + + if (file.exists ()) + file.delete (); + + builder.redirectError (file); + + /* Track process output in a new thread, since this is the UI + thread and doing so here can cause deadlocks when EmacsService + decides to wait for something. */ + + thread = new EmacsClientThread (builder); + thread.start (); + } + + @Override + public void + onCreate (Bundle savedInstanceState) + { + String action, fileName; + Intent intent; + Uri uri; + ContentResolver resolver; + ParcelFileDescriptor fd; + byte[] names; + String errorBlurb; + + super.onCreate (savedInstanceState); + + /* Obtain the intent that started Emacs. */ + intent = getIntent (); + action = intent.getAction (); + + if (action == null) + { + finish (); + return; + } + + /* Now see if the action specified is supported by Emacs. */ + + if (action.equals ("android.intent.action.VIEW") + || action.equals ("android.intent.action.EDIT") + || action.equals ("android.intent.action.PICK")) + { + /* Obtain the URI of the action. */ + uri = intent.getData (); + + if (uri == null) + { + finish (); + return; + } + + /* Now, try to get the file name. */ + + if (uri.getScheme ().equals ("file")) + fileName = uri.getPath (); + else + { + fileName = null; + + if (uri.getScheme ().equals ("content")) + { + /* This is one of the annoying Android ``content'' + URIs. Most of the time, there is actually an + underlying file, but it cannot be found without + opening the file and doing readlink on its file + descriptor in /proc/self/fd. */ + resolver = getContentResolver (); + + try + { + fd = resolver.openFileDescriptor (uri, "r"); + names = EmacsNative.getProcName (fd.getFd ()); + fd.close (); + + /* What is the right encoding here? */ + + if (names != null) + fileName = new String (names, "UTF-8"); + } + catch (FileNotFoundException exception) + { + /* Do nothing. */ + } + catch (IOException exception) + { + /* Do nothing. */ + } + } + + if (fileName == null) + { + errorBlurb = ("The URI: " + uri + " could not be opened" + + ", as it does not encode file name inform" + + "ation."); + displayFailureDialog ("Error opening file", errorBlurb); + return; + } + } + + /* And start emacsclient. */ + startEmacsClient (fileName); + } + else + finish (); + } +} diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index d17f6d1286c..2ec2ddf9bda 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -152,7 +152,6 @@ public class EmacsService extends Service } } - @TargetApi (Build.VERSION_CODES.GINGERBREAD) private String getLibraryDirectory () { diff --git a/lib-src/emacsclient.c b/lib-src/emacsclient.c index 698bf9b50ae..a72fced1bf2 100644 --- a/lib-src/emacsclient.c +++ b/lib-src/emacsclient.c @@ -626,6 +626,8 @@ decode_options (int argc, char **argv) alt_display = "w32"; #elif defined (HAVE_HAIKU) alt_display = "be"; +#elif defined (HAVE_ANDROID) + alt_display = "android"; #endif #ifdef HAVE_PGTK diff --git a/src/android.c b/src/android.c index 57a95bcd4f9..a0e64471a05 100644 --- a/src/android.c +++ b/src/android.c @@ -1369,6 +1369,27 @@ android_get_home_directory (void) return android_files_dir; } +/* Return the name of the file behind a file descriptor FD by reading + /proc/self/fd/. Place the name in BUFFER, which should be able to + hold size bytes. Value is 0 upon success, and 1 upon failure. */ + +static int +android_proc_name (int fd, char *buffer, size_t size) +{ + char format[sizeof "/proc/self/fd/" + + INT_STRLEN_BOUND (int)]; + ssize_t read; + + sprintf (format, "/proc/self/fd/%d", fd); + read = readlink (format, buffer, size - 1); + + if (read == -1) + return 1; + + buffer[read] = '\0'; + return 0; +} + /* JNI functions called by Java. */ @@ -1598,6 +1619,29 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, now. */ } +JNIEXPORT jobject JNICALL +NATIVE_NAME (getProcName) (JNIEnv *env, jobject object, jint fd) +{ + char buffer[PATH_MAX + 1]; + size_t length; + jbyteArray array; + + if (android_proc_name (fd, buffer, PATH_MAX + 1)) + return NULL; + + /* Return a byte array, as Java strings cannot always encode file + names. */ + length = strlen (buffer); + array = (*env)->NewByteArray (env, length); + if (!array) + return NULL; + + (*env)->SetByteArrayRegion (env, array, 0, length, + (jbyte *) buffer); + + return array; +} + /* Initialize service_class, aborting if something goes wrong. */ static void -- cgit v1.3 From 9b79f429edd173efd6ecbdcb2f0a5fafa8d6e523 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 6 Feb 2023 22:55:42 +0800 Subject: Port emacsclient wrapper to Android 7.1 and earlier * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Load every native library on which Emacs depends prior to loading libemacs itself. * java/org/gnu/emacs/EmacsOpenActivity.java (readEmacsClientLog) (EmacsOpenActivity, startEmacsClient): Don't use redirectError on Android 7.1 and earlier. --- java/org/gnu/emacs/EmacsNative.java | 152 ++++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsOpenActivity.java | 33 +++++-- 2 files changed, 177 insertions(+), 8 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index aba356051cd..939348ba420 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -159,6 +159,158 @@ public class EmacsNative static { + /* Older versions of Android cannot link correctly with shared + libraries that link with other shared libraries built along + Emacs unless all requisite shared libraries are explicitly + loaded from Java. + + Every time you add a new shared library dependency to Emacs, + please add it here as well. */ + + try + { + System.loadLibrary ("png_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("selinux_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("crypto_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("pcre_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("packagelistparser_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("gnutls_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("gmp_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("nettle_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("p11-kit_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("tasn1_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("hogweed_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("jansson_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("jpeg_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("tiff_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("xml2_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + + try + { + System.loadLibrary ("icuuc_emacs"); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } + System.loadLibrary ("emacs"); }; }; diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index e987e067a73..baf31039ecd 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -125,6 +125,16 @@ public class EmacsOpenActivity extends Activity int rc; String what; + /* Because the ProcessBuilder functions necessary to redirect + process output are not implemented on Android 7 and earlier, + print a generic error message. */ + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + return ("This is likely because the Emacs server" + + " is not running, or because you did" + + " not grant Emacs permission to access" + + " external storage."); + cache = getCacheDir (); file = new File (cache, "emacsclient.log"); what = ""; @@ -199,7 +209,8 @@ public class EmacsOpenActivity extends Activity Use TITLE as the title of the dialog. If TEXT is non-NULL, display that text in the dialog. Otherwise, use the contents of - emacsclient.log in the cache directory instead. */ + emacsclient.log in the cache directory instead, or describe why + that file cannot be read. */ public void finishFailure (final String title, final String text) @@ -240,20 +251,26 @@ public class EmacsOpenActivity extends Activity EmacsClientThread thread; File file; - file = new File (getCacheDir (), "emacsclient.log"); - libDir = getLibraryDirectory (); builder = new ProcessBuilder (libDir + "/libemacsclient.so", fileName, "--reuse-frame", "--timeout=10", "--no-wait"); - /* Redirect standard error to a file so that errors can be - meaningfully reported. */ + /* Redirection is unfortunately not possible in Android 7 and + earlier. */ - if (file.exists ()) - file.delete (); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + file = new File (getCacheDir (), "emacsclient.log"); - builder.redirectError (file); + /* Redirect standard error to a file so that errors can be + meaningfully reported. */ + + if (file.exists ()) + file.delete (); + + builder.redirectError (file); + } /* Track process output in a new thread, since this is the UI thread and doing so here can cause deadlocks when EmacsService -- cgit v1.3 From 0198b8cffd82893412c738dae8e50c45a99286f1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 12 Feb 2023 20:32:25 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Environment): Document notifications permission. * java/org/gnu/emacs/EmacsEditable.java (EmacsEditable): * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): New files. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Load library dependencies in a less verbose fashion. * java/org/gnu/emacs/EmacsView.java (EmacsView): Make imManager public. (onCreateInputConnection): Set InputType to TYPE_NULL for now. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, onKeyDown) (onKeyUp, getEventUnicodeChar): Correctly handle key events with strings. * lisp/term/android-win.el (android-clear-preedit-text) (android-preedit-text): New special event handlers. * src/android.c (struct android_emacs_window): Add function lookup_string. (android_init_emacs_window): Adjust accordingly. (android_wc_lookup_string): New function. * src/androidgui.h (struct android_key_event): Improve commentary. (enum android_lookup_status): New enum. * src/androidterm.c (handle_one_android_event): Synchronize IM lookup code with X. * src/coding.c (from_unicode_buffer): Implement on Android. * src/coding.h: * src/sfnt.c: Fix commentary. --- doc/emacs/android.texi | 2 + java/org/gnu/emacs/EmacsEditable.java | 300 +++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsInputConnection.java | 175 ++++++++++++++++ java/org/gnu/emacs/EmacsNative.java | 166 +++------------ java/org/gnu/emacs/EmacsView.java | 7 +- java/org/gnu/emacs/EmacsWindow.java | 96 ++++++++- lisp/term/android-win.el | 36 ++++ src/android.c | 111 ++++++++++ src/androidgui.h | 20 +- src/androidterm.c | 103 +++++++-- src/coding.c | 36 +++- src/coding.h | 4 +- src/sfnt.c | 11 +- 13 files changed, 880 insertions(+), 187 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsEditable.java create mode 100644 java/org/gnu/emacs/EmacsInputConnection.java (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 8df26560f02..308bd70c59c 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -334,6 +334,8 @@ permissions upon installation: @code{android.permission.TRANSMIT_IR} @item @code{android.permission.WAKE_LOCK} +@item +@code{android.permission.POST_NOTIFICATIONS} @end itemize Other permissions must be granted by the user through the system diff --git a/java/org/gnu/emacs/EmacsEditable.java b/java/org/gnu/emacs/EmacsEditable.java new file mode 100644 index 00000000000..79af65a6ccd --- /dev/null +++ b/java/org/gnu/emacs/EmacsEditable.java @@ -0,0 +1,300 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.text.InputFilter; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.SpanWatcher; +import android.text.Selection; + +import android.content.Context; + +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; + +import android.text.Spannable; + +import android.util.Log; + +import android.os.Build; + +/* Android input methods insist on having access to buffer contents. + Since Emacs is not designed like ``any other Android text editor'', + that is not possible. + + This file provides a fake editing buffer that is designed to weasel + as much information as possible out of an input method, without + actually providing buffer contents to Emacs. + + The basic idea is to have the fake editing buffer be initially + empty. + + When the input method inserts composed text, it sets a flag. + Updates to the buffer while the flag is set are sent to Emacs to be + displayed as ``preedit text''. + + Once some heuristics decide that composition has been completed, + the composed text is sent to Emacs, and the text that was inserted + in this editing buffer is erased. */ + +public class EmacsEditable extends SpannableStringBuilder + implements SpanWatcher +{ + private static final String TAG = "EmacsEditable"; + + /* Whether or not composition is currently in progress. */ + private boolean isComposing; + + /* The associated input connection. */ + private EmacsInputConnection connection; + + /* The associated IM manager. */ + private InputMethodManager imManager; + + /* Any extracted text an input method may be monitoring. */ + private ExtractedText extractedText; + + /* The corresponding text request. */ + private ExtractedTextRequest extractRequest; + + /* The number of nested batch edits. */ + private int batchEditCount; + + /* Whether or not invalidateInput should be called upon batch edits + ending. */ + private boolean pendingInvalidate; + + /* The ``composing span'' indicating the bounds of an ongoing + character composition. */ + private Object composingSpan; + + public + EmacsEditable (EmacsInputConnection connection) + { + /* Initialize the editable with one initial space, so backspace + always works. */ + super (); + + Object tem; + Context context; + + this.connection = connection; + + context = connection.view.getContext (); + tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); + imManager = (InputMethodManager) tem; + + /* To watch for changes to text properties on Android, you + add... a text property. */ + setSpan (this, 0, 0, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + public void + endBatchEdit () + { + if (batchEditCount < 1) + return; + + if (--batchEditCount == 0 && pendingInvalidate) + invalidateInput (); + } + + public void + beginBatchEdit () + { + ++batchEditCount; + } + + public void + setExtractedTextAndRequest (ExtractedText text, + ExtractedTextRequest request, + boolean monitor) + { + /* Extract the text. If monitor is set, also record it as the + text that is currently being extracted. */ + + text.startOffset = 0; + text.selectionStart = Selection.getSelectionStart (this); + text.selectionEnd = Selection.getSelectionStart (this); + text.text = this; + + if (monitor) + { + extractedText = text; + extractRequest = request; + } + } + + public void + compositionStart () + { + isComposing = true; + } + + public void + compositionEnd () + { + isComposing = false; + sendComposingText (null); + } + + private void + sendComposingText (String string) + { + EmacsWindow window; + long time, serial; + + window = connection.view.window; + + if (window.isDestroyed ()) + return; + + time = System.currentTimeMillis (); + + /* A composition event is simply a special key event with a + keycode of -1. */ + + synchronized (window.eventStrings) + { + serial + = EmacsNative.sendKeyPress (window.handle, time, 0, -1, -1); + + /* Save the string so that android_lookup_string can find + it. */ + if (string != null) + window.saveUnicodeString ((int) serial, string); + } + } + + private void + invalidateInput () + { + int start, end, composingSpanStart, composingSpanEnd; + + if (batchEditCount > 0) + { + Log.d (TAG, "invalidateInput: deferring for batch edit"); + pendingInvalidate = true; + return; + } + + pendingInvalidate = false; + + start = Selection.getSelectionStart (this); + end = Selection.getSelectionEnd (this); + + if (composingSpan != null) + { + composingSpanStart = getSpanStart (composingSpan); + composingSpanEnd = getSpanEnd (composingSpan); + } + else + { + composingSpanStart = -1; + composingSpanEnd = -1; + } + + Log.d (TAG, "invalidateInput: now " + start + ", " + end); + + /* Tell the input method that the cursor changed. */ + imManager.updateSelection (connection.view, start, end, + composingSpanStart, + composingSpanEnd); + + /* If there is any extracted text, tell the IME that it has + changed. */ + if (extractedText != null) + imManager.updateExtractedText (connection.view, + extractRequest.token, + extractedText); + } + + public SpannableStringBuilder + replace (int start, int end, CharSequence tb, int tbstart, + int tbend) + { + super.replace (start, end, tb, tbstart, tbend); + + /* If a change happens during composition, perform the change and + then send the text being composed. */ + + if (isComposing) + sendComposingText (toString ()); + + return this; + } + + private boolean + isSelectionSpan (Object span) + { + return ((Selection.SELECTION_START == span + || Selection.SELECTION_END == span) + && (getSpanFlags (span) + & Spanned.SPAN_INTERMEDIATE) == 0); + } + + @Override + public void + onSpanAdded (Spannable text, Object what, int start, int end) + { + Log.d (TAG, "onSpanAdded: " + text + " " + what + " " + + start + " " + end); + + /* Try to find the composing span. This isn't a public API. */ + + if (what.getClass ().getName ().contains ("ComposingText")) + composingSpan = what; + + if (isSelectionSpan (what)) + invalidateInput (); + } + + @Override + public void + onSpanChanged (Spannable text, Object what, int ostart, + int oend, int nstart, int nend) + { + Log.d (TAG, "onSpanChanged: " + text + " " + what + " " + + nstart + " " + nend); + + if (isSelectionSpan (what)) + invalidateInput (); + } + + @Override + public void + onSpanRemoved (Spannable text, Object what, + int start, int end) + { + Log.d (TAG, "onSpanRemoved: " + text + " " + what + " " + + start + " " + end); + + if (isSelectionSpan (what)) + invalidateInput (); + } + + public boolean + isInBatchEdit () + { + return batchEditCount > 0; + } +} diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java new file mode 100644 index 00000000000..897a393b984 --- /dev/null +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -0,0 +1,175 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.SurroundingText; +import android.view.KeyEvent; + +import android.text.Editable; + +import android.util.Log; + +/* Android input methods, take number six. + + See EmacsEditable for more details. */ + +public class EmacsInputConnection extends BaseInputConnection +{ + private static final String TAG = "EmacsInputConnection"; + public EmacsView view; + private EmacsEditable editable; + + /* The length of the last string to be committed. */ + private int lastCommitLength; + + int currentLargeOffset; + + public + EmacsInputConnection (EmacsView view) + { + super (view, false); + this.view = view; + this.editable = new EmacsEditable (this); + } + + @Override + public Editable + getEditable () + { + return editable; + } + + @Override + public boolean + setComposingText (CharSequence text, int newCursorPosition) + { + editable.compositionStart (); + super.setComposingText (text, newCursorPosition); + return true; + } + + @Override + public boolean + setComposingRegion (int start, int end) + { + int i; + + if (lastCommitLength != 0) + { + Log.d (TAG, "Restarting composition for: " + lastCommitLength); + + for (i = 0; i < lastCommitLength; ++i) + sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_DEL)); + + lastCommitLength = 0; + } + + editable.compositionStart (); + super.setComposingRegion (start, end); + return true; + } + + @Override + public boolean + finishComposingText () + { + editable.compositionEnd (); + return super.finishComposingText (); + } + + @Override + public boolean + beginBatchEdit () + { + editable.beginBatchEdit (); + return super.beginBatchEdit (); + } + + @Override + public boolean + endBatchEdit () + { + editable.endBatchEdit (); + return super.endBatchEdit (); + } + + @Override + public boolean + commitText (CharSequence text, int newCursorPosition) + { + editable.compositionEnd (); + super.commitText (text, newCursorPosition); + + /* An observation is that input methods rarely recompose trailing + spaces. Avoid re-setting the commit length in that case. */ + + if (text.toString ().equals (" ")) + lastCommitLength += 1; + else + /* At this point, the editable is now empty. + + The input method may try to cancel the edit upon a subsequent + backspace by calling setComposingRegion with a region that is + the length of TEXT. + + Record this length in order to be able to send backspace + events to ``delete'' the text in that case. */ + lastCommitLength = text.length (); + + Log.d (TAG, "commitText: \"" + text + "\""); + + return true; + } + + /* Return a large offset, cycling through 100000, 30000, 0. + The offset is typically used to force the input method to update + its notion of ``surrounding text'', bypassing any caching that + it might have in progress. + + There must be another way to do this, but I can't find it. */ + + public int + largeSelectionOffset () + { + switch (currentLargeOffset) + { + case 0: + currentLargeOffset = 100000; + return 100000; + + case 100000: + currentLargeOffset = 30000; + return 30000; + + case 30000: + currentLargeOffset = 0; + return 0; + } + + currentLargeOffset = 0; + return -1; + } +} diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 939348ba420..f0219843d35 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,6 +25,10 @@ import android.content.res.AssetManager; public class EmacsNative { + /* List of native libraries that must be loaded during class + initialization. */ + private static final String[] libraryDeps; + /* Obtain the fingerprint of this build of Emacs. The fingerprint can be used to determine the dump file name. */ public static native String getFingerprint (); @@ -167,148 +171,26 @@ public class EmacsNative Every time you add a new shared library dependency to Emacs, please add it here as well. */ - try - { - System.loadLibrary ("png_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("selinux_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("crypto_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("pcre_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("packagelistparser_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("gnutls_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("gmp_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("nettle_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("p11-kit_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("tasn1_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("hogweed_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("jansson_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("jpeg_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("tiff_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("xml2_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ - } - - try - { - System.loadLibrary ("icuuc_emacs"); - } - catch (UnsatisfiedLinkError exception) - { - /* Ignore this exception. */ + libraryDeps = new String[] { "png_emacs", "selinux_emacs", + "crypto_emacs", "pcre_emacs", + "packagelistparser_emacs", + "gnutls_emacs", "gmp_emacs", + "nettle_emacs", "p11-kit_emacs", + "tasn1_emacs", "hogweed_emacs", + "jansson_emacs", "jpeg_emacs", + "tiff_emacs", "xml2_emacs", + "icuuc_emacs", }; + + for (String dependency : libraryDeps) + { + try + { + System.loadLibrary (dependency); + } + catch (UnsatisfiedLinkError exception) + { + /* Ignore this exception. */ + } } System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 4fc8104e31f..bc3716f6da8 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -94,7 +94,7 @@ public class EmacsView extends ViewGroup private long lastClipSerial; /* The InputMethodManager for this view's context. */ - private InputMethodManager imManager; + public InputMethodManager imManager; /* Whether or not this view is attached to a window. */ public boolean isAttachedToWindow; @@ -558,8 +558,9 @@ public class EmacsView extends ViewGroup box that obscures Emacs. */ info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; - /* But don't return an InputConnection, in order to force the on - screen keyboard to work correctly. */ + /* Set a reasonable inputType. */ + info.inputType = InputType.TYPE_NULL; + return null; } diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 9e2f2f53270..0eca35cec61 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -23,6 +23,8 @@ import java.lang.IllegalStateException; import java.util.ArrayList; import java.util.List; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; import android.content.Context; @@ -100,7 +102,7 @@ public class EmacsWindow extends EmacsHandleObject /* The button state and keyboard modifier mask at the time of the last button press or release event. */ - private int lastButtonState, lastModifiers; + public int lastButtonState, lastModifiers; /* Whether or not the window is mapped, and whether or not it is deiconified. */ @@ -122,6 +124,10 @@ public class EmacsWindow extends EmacsHandleObject to quit Emacs. */ private long lastVolumeButtonRelease; + /* Linked list of character strings which were recently sent as + events. */ + public LinkedHashMap eventStrings; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height, boolean overrideRedirect) @@ -155,6 +161,19 @@ public class EmacsWindow extends EmacsHandleObject } scratchGC = new EmacsGC ((short) 0); + + /* Create the map of input method-committed strings. Keep at most + ten strings in the map. */ + + eventStrings + = new LinkedHashMap () { + @Override + protected boolean + removeEldestEntry (Map.Entry entry) + { + return size () > 10; + } + }; } public void @@ -507,10 +526,40 @@ public class EmacsWindow extends EmacsHandleObject return view.getBitmap (); } + /* event.getCharacters is used because older input methods still + require it. */ + @SuppressWarnings ("deprecation") + public int + getEventUnicodeChar (KeyEvent event, int state) + { + String characters; + + if (event.getUnicodeChar (state) != 0) + return event.getUnicodeChar (state); + + characters = event.getCharacters (); + + if (characters != null && characters.length () == 1) + return characters.charAt (0); + + return characters == null ? 0 : -1; + } + + public void + saveUnicodeString (int serial, String string) + { + eventStrings.put (serial, string); + } + + /* event.getCharacters is used because older input methods still + require it. */ + @SuppressWarnings ("deprecation") public void onKeyDown (int keyCode, KeyEvent event) { int state, state_1; + long serial; + String characters; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -537,18 +586,28 @@ public class EmacsWindow extends EmacsHandleObject state_1 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); - EmacsNative.sendKeyPress (this.handle, - event.getEventTime (), - state, keyCode, - event.getUnicodeChar (state_1)); - lastModifiers = state; + synchronized (eventStrings) + { + serial + = EmacsNative.sendKeyPress (this.handle, + event.getEventTime (), + state, keyCode, + getEventUnicodeChar (event, + state_1)); + lastModifiers = state; + + characters = event.getCharacters (); + + if (characters != null && characters.length () > 1) + saveUnicodeString ((int) serial, characters); + } } public void onKeyUp (int keyCode, KeyEvent event) { int state, state_1; - long time; + long time, serial; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -575,10 +634,12 @@ public class EmacsWindow extends EmacsHandleObject state_1 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); - EmacsNative.sendKeyRelease (this.handle, - event.getEventTime (), - state, keyCode, - event.getUnicodeChar (state_1)); + serial + = EmacsNative.sendKeyRelease (this.handle, + event.getEventTime (), + state, keyCode, + getEventUnicodeChar (event, + state_1)); lastModifiers = state; if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) @@ -1105,4 +1166,17 @@ public class EmacsWindow extends EmacsHandleObject } }); } + + public String + lookupString (int eventSerial) + { + String any; + + synchronized (eventStrings) + { + any = eventStrings.remove (eventSerial); + } + + return any; + } }; diff --git a/lisp/term/android-win.el b/lisp/term/android-win.el index aa5992a9f09..0984b4d5840 100644 --- a/lisp/term/android-win.el +++ b/lisp/term/android-win.el @@ -166,6 +166,42 @@ VALUE should be something suitable for passing to (android-set-clipboard string)) ((eq type 'PRIMARY) (setq android-primary-selection string))))) + +;;; Character composition display. + +(defvar android-preedit-overlay nil + "The overlay currently used to display preedit text from a compose sequence.") + +;; With some input methods, text gets inserted before Emacs is told to +;; remove any preedit text that was displayed, which causes both the +;; preedit overlay and the text to be visible for a brief period of +;; time. This pre-command-hook clears the overlay before any command +;; and should be set whenever a preedit overlay is visible. +(defun android-clear-preedit-text () + "Clear the pre-edit overlay and remove itself from pre-command-hook. +This function should be installed in `pre-command-hook' whenever +preedit text is displayed." + (when android-preedit-overlay + (delete-overlay android-preedit-overlay) + (setq android-preedit-overlay nil)) + (remove-hook 'pre-command-hook #'android-clear-preedit-text)) + +(defun android-preedit-text (event) + "Display preedit text from a compose sequence in EVENT. +EVENT is a preedit-text event." + (interactive "e") + (when android-preedit-overlay + (delete-overlay android-preedit-overlay) + (setq android-preedit-overlay nil) + (remove-hook 'pre-command-hook #'android-clear-preedit-text)) + (when (nth 1 event) + (let ((string (propertize (nth 1 event) 'face '(:underline t)))) + (setq android-preedit-overlay (make-overlay (point) (point))) + (add-hook 'pre-command-hook #'android-clear-preedit-text) + (overlay-put android-preedit-overlay 'window (selected-window)) + (overlay-put android-preedit-overlay 'before-string string)))) + +(define-key special-event-map [preedit-text] 'android-preedit-text) (provide 'android-win) ;; android-win.el ends here. diff --git a/src/android.c b/src/android.c index ebd374addbf..479eb9b10d4 100644 --- a/src/android.c +++ b/src/android.c @@ -125,6 +125,7 @@ struct android_emacs_window jclass class; jmethodID swap_buffers; jmethodID toggle_on_screen_keyboard; + jmethodID lookup_string; }; /* The API level of the current device. */ @@ -1829,6 +1830,7 @@ android_init_emacs_window (void) FIND_METHOD (swap_buffers, "swapBuffers", "()V"); FIND_METHOD (toggle_on_screen_keyboard, "toggleOnScreenKeyboard", "(Z)V"); + FIND_METHOD (lookup_string, "lookupString", "(I)Ljava/lang/String;"); #undef FIND_METHOD } @@ -4104,6 +4106,115 @@ android_sync (void) android_exception_check (); } +int +android_wc_lookup_string (android_key_pressed_event *event, + wchar_t *buffer_return, int wchars_buffer, + int *keysym_return, + enum android_lookup_status *status_return) +{ + enum android_lookup_status status; + int rc; + jobject window, string; + const jchar *characters; + jsize size; + size_t i; + + status = ANDROID_LOOKUP_NONE; + rc = 0; + + /* See if an actual lookup has to be made. Note that while + BUFFER_RETURN is wchar_t, the returned characters are always in + UCS. */ + + if (event->unicode_char != (uint32_t) -1) + { + if (event->unicode_char) + { + if (wchars_buffer < 1) + { + *status_return = ANDROID_BUFFER_OVERFLOW; + return 0; + } + else + { + buffer_return[0] = event->unicode_char; + status = ANDROID_LOOKUP_CHARS; + rc = 1; + } + } + + *keysym_return = event->keycode; + + if (status == ANDROID_LOOKUP_CHARS) + status = ANDROID_LOOKUP_BOTH; + else + { + status = ANDROID_LOOKUP_KEYSYM; + rc = 0; + } + + *status_return = status; + + return rc; + } + + /* Now look up the window. */ + rc = 0; + + if (!android_handles[event->window].handle + || (android_handles[event->window].type + != ANDROID_HANDLE_WINDOW)) + status = ANDROID_LOOKUP_NONE; + else + { + window = android_handles[event->window].handle; + string + = (*android_java_env)->CallObjectMethod (android_java_env, window, + window_class.lookup_string, + (jint) event->serial); + android_exception_check (); + + if (!string) + status = ANDROID_LOOKUP_NONE; + else + { + /* Now return this input method string. */ + characters = (*android_java_env)->GetStringChars (android_java_env, + string, NULL); + android_exception_check (); + + /* Figure out how big the string is. */ + size = (*android_java_env)->GetStringLength (android_java_env, + string); + + /* Copy over the string data. */ + for (i = 0; i < MIN ((unsigned int) wchars_buffer, size); ++i) + buffer_return[i] = characters[i]; + + if (i < size) + status = ANDROID_BUFFER_OVERFLOW; + else + status = ANDROID_LOOKUP_CHARS; + + /* Return the number of characters that should have been + written. */ + + if (size > INT_MAX) + rc = INT_MAX; + else + rc = size; + + (*android_java_env)->ReleaseStringChars (android_java_env, string, + characters); + android_exception_check (); + ANDROID_DELETE_LOCAL_REF (string); + } + } + + *status_return = status; + return rc; +} + /* Low level drawing primitives. */ diff --git a/src/androidgui.h b/src/androidgui.h index 619c5e44f15..1f0d34e67aa 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -209,7 +209,7 @@ typedef char Emacs_Cursor; #ifndef ANDROID_STUBIFY /* Universal NULL handle. */ -static const int ANDROID_NONE; +static const int ANDROID_NONE, ANDROID_NO_SYMBOL; /* Keep these as conceptually close to X as possible: that makes synchronizing code between the ports much easier. */ @@ -259,9 +259,14 @@ struct android_key_event android_time time; unsigned int state; unsigned int keycode; + + /* If this field is -1, then android_lookup_string should be called + to retrieve the associated individual characters. */ unsigned int unicode_char; }; +typedef struct android_key_event android_key_pressed_event; + /* These hard coded values are Android modifier keycodes derived through experimentation. */ @@ -449,6 +454,15 @@ enum ANDROID_CURRENT_TIME = 0L, }; +enum android_lookup_status + { + ANDROID_BUFFER_OVERFLOW, + ANDROID_LOOKUP_NONE, + ANDROID_LOOKUP_CHARS, + ANDROID_LOOKUP_KEYSYM, + ANDROID_LOOKUP_BOTH, + }; + extern int android_pending (void); extern void android_next_event (union android_event *); @@ -537,6 +551,10 @@ extern void android_translate_coordinates (android_window, int, int, int *, int *); extern void android_sync (void); +extern int android_wc_lookup_string (android_key_pressed_event *, + wchar_t *, int, int *, + enum android_lookup_status *); + #endif diff --git a/src/androidterm.c b/src/androidterm.c index 4aee1a90b68..a57d5623c66 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -541,6 +541,8 @@ handle_one_android_event (struct android_display_info *dpyinfo, Lisp_Object window; int scroll_height; double scroll_unit; + int keysym; + ptrdiff_t nchars, i; /* It is okay for this to not resemble handle_one_xevent so much. Differences in event handling code are much less nasty than @@ -550,6 +552,7 @@ handle_one_android_event (struct android_display_info *dpyinfo, hlinfo = &dpyinfo->mouse_highlight; *finish = ANDROID_EVENT_NORMAL; any = android_window_to_frame (dpyinfo, event->xany.window); + nchars = 0; if (any && any->wait_event_type == event->type) any->wait_event_type = 0; /* Indicates we got it. */ @@ -616,6 +619,13 @@ handle_one_android_event (struct android_display_info *dpyinfo, android_flush_dirty_back_buffer_on (mouse_frame); } + if (!f) + goto OTHER; + + wchar_t copy_buffer[129]; + wchar_t *copy_bufptr = copy_buffer; + int copy_bufsiz = 128 * sizeof (wchar_t); + event->xkey.state |= android_emacs_to_android_modifiers (dpyinfo, extra_keyboard_modifiers); @@ -627,31 +637,100 @@ handle_one_android_event (struct android_display_info *dpyinfo, = android_android_to_emacs_modifiers (dpyinfo, modifiers); inev.ie.timestamp = event->xkey.time; - /* First deal with keysyms which have defined translations to - characters. */ + keysym = event->xkey.keycode; + + { + enum android_lookup_status status_return; + + nchars = android_wc_lookup_string (&event->xkey, copy_bufptr, + copy_bufsiz, &keysym, + &status_return); + + /* android_lookup_string can't be called twice, so there's no + way to recover from buffer overflow. */ + if (status_return == ANDROID_BUFFER_OVERFLOW) + goto done_keysym; + else if (status_return == ANDROID_LOOKUP_NONE) + { + /* Don't skip preedit text events. */ + if (event->xkey.keycode != (uint32_t) -1) + goto done_keysym; + } + else if (status_return == ANDROID_LOOKUP_CHARS) + keysym = ANDROID_NO_SYMBOL; + else if (status_return != ANDROID_LOOKUP_KEYSYM + && status_return != ANDROID_LOOKUP_BOTH) + emacs_abort (); + + /* Deal with pre-edit text events. On Android, these are + simply encoded as events with associated strings and a + keycode set to ``-1''. */ + + if (event->xkey.keycode == (uint32_t) -1) + { + inev.ie.kind = PREEDIT_TEXT_EVENT; + inev.ie.arg = Qnil; + + /* If text was looked up, decode it and make it the + preedit text. */ + + if (status_return == ANDROID_LOOKUP_CHARS && nchars) + { + copy_bufptr[nchars] = 0; + inev.ie.arg = from_unicode_buffer (copy_bufptr); + } - if (event->xkey.unicode_char >= 32 - && event->xkey.unicode_char < 128) + goto done_keysym; + } + } + + if (nchars == 1 && copy_bufptr[0] >= 32) { - inev.ie.kind = ASCII_KEYSTROKE_EVENT; - inev.ie.code = event->xkey.unicode_char; + /* Deal with characters. */ + + if (copy_bufptr[0] < 128) + inev.ie.kind = ASCII_KEYSTROKE_EVENT; + else + inev.ie.kind = MULTIBYTE_CHAR_KEYSTROKE_EVENT; + + inev.ie.code = copy_bufptr[0]; } - else if (event->xkey.unicode_char < 32) + else if (nchars < 2 && keysym) { /* If the key is a modifier key, just return. */ - if (ANDROID_IS_MODIFIER_KEY (event->xkey.keycode)) + if (ANDROID_IS_MODIFIER_KEY (keysym)) goto done_keysym; /* Next, deal with special ``characters'' by giving the keycode to keyboard.c. */ inev.ie.kind = NON_ASCII_KEYSTROKE_EVENT; - inev.ie.code = event->xkey.keycode; + inev.ie.code = keysym; } else { - /* Finally, deal with Unicode characters. */ - inev.ie.kind = MULTIBYTE_CHAR_KEYSTROKE_EVENT; - inev.ie.code = event->xkey.unicode_char; + /* Finally, deal with strings. */ + + for (i = 0; i < nchars; ++i) + { + inev.ie.kind = (SINGLE_BYTE_CHAR_P (copy_bufptr[i]) + ? ASCII_KEYSTROKE_EVENT + : MULTIBYTE_CHAR_KEYSTROKE_EVENT); + inev.ie.code = copy_bufptr[i]; + + /* If the character is actually '\n', then change this + to RET. */ + + if (copy_bufptr[i] == '\n') + { + inev.ie.kind = NON_ASCII_KEYSTROKE_EVENT; + inev.ie.code = 66; + } + + kbd_buffer_store_buffered_event (&inev, hold_quit); + } + + count += nchars; + inev.ie.kind = NO_EVENT; /* Already stored above. */ } goto done_keysym; diff --git a/src/coding.c b/src/coding.c index a2e0d7040f8..be5a0252ede 100644 --- a/src/coding.c +++ b/src/coding.c @@ -8495,7 +8495,7 @@ preferred_coding_system (void) return CODING_ID_NAME (id); } -#if defined (WINDOWSNT) || defined (CYGWIN) +#if defined (WINDOWSNT) || defined (CYGWIN) || defined HAVE_ANDROID Lisp_Object from_unicode (Lisp_Object str) @@ -8513,10 +8513,34 @@ from_unicode (Lisp_Object str) Lisp_Object from_unicode_buffer (const wchar_t *wstr) { - /* We get one of the two final null bytes for free. */ - ptrdiff_t len = 1 + sizeof (wchar_t) * wcslen (wstr); - AUTO_STRING_WITH_LEN (str, (char *) wstr, len); - return from_unicode (str); + if (sizeof (wchar_t) == 2) + { + /* We get one of the two final null bytes for free. */ + ptrdiff_t len = 1 + sizeof (wchar_t) * wcslen (wstr); + AUTO_STRING_WITH_LEN (str, (char *) wstr, len); + return from_unicode (str); + } + else + { + /* This code is used only on Android, where little endian UTF-16 + strings are extended to 32-bit wchar_t. */ + + uint16_t *words; + size_t length, i; + + length = wcslen (wstr) + 1; + + USE_SAFE_ALLOCA; + SAFE_NALLOCA (words, sizeof *words, length); + + for (i = 0; i < length - 1; ++i) + words[i] = wstr[i]; + + words[i] = '\0'; + AUTO_STRING_WITH_LEN (str, (char *) words, + (length - 1) * sizeof *words); + return unbind_to (sa_count, from_unicode (str)); + } } wchar_t * @@ -8536,7 +8560,7 @@ to_unicode (Lisp_Object str, Lisp_Object *buf) return WCSDATA (*buf); } -#endif /* WINDOWSNT || CYGWIN */ +#endif /* WINDOWSNT || CYGWIN || HAVE_ANDROID */ /*** 8. Emacs Lisp library functions ***/ diff --git a/src/coding.h b/src/coding.h index b8d4f5f27e1..08c29c884a5 100644 --- a/src/coding.h +++ b/src/coding.h @@ -709,7 +709,7 @@ extern void encode_coding_object (struct coding_system *, /* Defined in this file. */ INLINE int surrogates_to_codepoint (int, int); -#if defined (WINDOWSNT) || defined (CYGWIN) +#if defined (WINDOWSNT) || defined (CYGWIN) || defined HAVE_ANDROID /* These functions use Lisp string objects to store the UTF-16LE strings that modern versions of Windows expect. These strings are @@ -732,7 +732,7 @@ extern Lisp_Object from_unicode (Lisp_Object str); /* Convert WSTR to an Emacs string. */ extern Lisp_Object from_unicode_buffer (const wchar_t *wstr); -#endif /* WINDOWSNT || CYGWIN */ +#endif /* WINDOWSNT || CYGWIN || HAVE_ANDROID */ /* Macros for backward compatibility. */ diff --git a/src/sfnt.c b/src/sfnt.c index cf36f388819..38f6984b93c 100644 --- a/src/sfnt.c +++ b/src/sfnt.c @@ -3758,16 +3758,7 @@ sfnt_edge_sort (struct sfnt_edge *edges, size_t size) coordinates, and incrementing the Y axis by SFNT_POLY_STEP instead of 1. SFNT_POLY_STEP is chosen to always keep Y aligned to a grid placed such that there are always 1 << SFNT_POLY_SHIFT positions - available for each integral pixel coordinate. - - Moving upwards is performed using Bresenham's algorithm. Prior to - an edge being created, a single slope error is computed. This is - how much X should increase for each increase in Y. - - Upon each increase in Y, X initially does not move. Instead, the - ``slope error'' is accumulated; once it exceeds the sample size, - one sample is subtracted from the accumulator, and X is increased - by one step. */ + available for each integral pixel coordinate. */ static void sfnt_poly_edges (struct sfnt_edge *edges, size_t size, -- cgit v1.3 From a158c1d5b964fda36f752998cef076d581dc4df4 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 15 Feb 2023 12:23:03 +0800 Subject: Update Android port * configure.ac (HAVE_TEXT_CONVERSION): Define on Android. * doc/emacs/input.texi (On-Screen Keyboards): Document ``text conversion'' slightly. * doc/lispref/commands.texi (Misc Events): Document new `text-conversion' event. * java/org/gnu/emacs/EmacsContextMenu.java (display): Use `syncRunnable'. * java/org/gnu/emacs/EmacsDialog.java (display): Likewise. * java/org/gnu/emacs/EmacsEditable.java: Delete file. * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): Reimplement from scratch. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Add new functions. * java/org/gnu/emacs/EmacsService.java (EmacsService, getEmacsView) (getLocationOnScreen, sync, getClipboardManager, restartEmacs): Use syncRunnable. (syncRunnable): New function. (updateIC, resetIC): New functions. * java/org/gnu/emacs/EmacsView.java (EmacsView): New field `inputConnection' and `icMode'. (onCreateInputConnection): Update accordingly. (setICMode, getICMode): New functions. * lisp/bindings.el (global-map): Ignore text conversion events. * src/alloc.c (mark_frame): Mark text conversion data. * src/android.c (struct android_emacs_service): New fields `update_ic' and `reset_ic'. (event_serial): Export. (android_query_sem): New function. (android_init_events): Initialize new semaphore. (android_write_event): Export. (android_select): Check for UI thread code. (setEmacsParams, android_init_emacs_service): Initialize new methods. (android_check_query, android_begin_query, android_end_query) (android_run_in_emacs_thread): (android_update_ic, android_reset_ic): New functions for managing synchronous queries from one thread to another. * src/android.h: Export new functions. * src/androidgui.h (enum android_event_type): Add input method events. (enum android_ime_operation, struct android_ime_event) (union android_event, enum android_ic_mode): New structs and enums. * src/androidterm.c (android_window_to_frame): Allow DPYINFO to be NULL. (android_decode_utf16, android_handle_ime_event) (handle_one_android_event, android_sync_edit) (android_copy_java_string, beginBatchEdit, endBatchEdit) (commitCompletion, deleteSurroundingText, finishComposingText) (getSelectedtext, getTextAfterCursor, getTextBeforeCursor) (setComposingText, setComposingRegion, setSelection, getSelection) (performEditorAction, getExtractedText): New functions. (struct android_conversion_query_context): (android_perform_conversion_query): (android_text_to_string): (struct android_get_selection_context): (android_get_selection): (struct android_get_extracted_text_context): (android_get_extracted_text): (struct android_extracted_text_request_class): (struct android_extracted_text_class): (android_update_selection): (android_reset_conversion): (android_set_point): (android_compose_region_changed): (android_notify_conversion): (text_conversion_interface): New functions and structures. (android_term_init): Initialize text conversion. * src/coding.c (syms_of_coding): Define Qutf_16le on Android. * src/frame.c (make_frame): Clear conversion data. (delete_frame): Reset conversion state. * src/frame.h (enum text_conversion_operation) (struct text_conversion_action, struct text_conversion_state) (GCALIGNED_STRUCT): Update structures. * src/keyboard.c (read_char, readable_events, kbd_buffer_get_event) (syms_of_keyboard): Handle text conversion events. * src/lisp.h: * src/process.c: Fix includes. * src/textconv.c (enum textconv_batch_edit_flags, textconv_query) (reset_frame_state, detect_conversion_events) (restore_selected_window, really_commit_text) (really_finish_composing_text, really_set_composing_text) (really_set_composing_region, really_delete_surrounding_text) (really_set_point, complete_edit) (handle_pending_conversion_events_1) (handle_pending_conversion_events, start_batch_edit) (end_batch_edit, commit_text, finish_composing_text) (set_composing_text, set_composing_region, textconv_set_point) (delete_surrounding_text, get_extracted_text) (report_selected_window_change, report_point_change) (register_texconv_interface): New functions. * src/textconv.h (struct textconv_interface) (TEXTCONV_SKIP_CONVERSION_REGION): Update prototype. * src/xdisp.c (mark_window_display_accurate_1): * src/xfns.c (xic_string_conversion_callback): * src/xterm.c (init_xterm): Adjust accordingly. --- configure.ac | 2 +- doc/emacs/input.texi | 17 + doc/lispref/commands.texi | 9 + java/org/gnu/emacs/EmacsContextMenu.java | 15 +- java/org/gnu/emacs/EmacsDialog.java | 15 +- java/org/gnu/emacs/EmacsEditable.java | 300 -------- java/org/gnu/emacs/EmacsInputConnection.java | 187 ++--- java/org/gnu/emacs/EmacsNative.java | 46 ++ java/org/gnu/emacs/EmacsService.java | 117 +-- java/org/gnu/emacs/EmacsView.java | 55 +- lisp/bindings.el | 3 + src/alloc.c | 14 + src/android.c | 295 +++++++- src/android.h | 10 + src/androidgui.h | 60 ++ src/androidterm.c | 1037 +++++++++++++++++++++++++- src/coding.c | 2 +- src/frame.c | 17 + src/frame.h | 62 ++ src/keyboard.c | 50 ++ src/lisp.h | 3 + src/process.c | 1 + src/textconv.c | 906 +++++++++++++++++++++- src/textconv.h | 38 +- src/xdisp.c | 28 + src/xfns.c | 2 +- src/xterm.c | 2 +- 27 files changed, 2787 insertions(+), 506 deletions(-) delete mode 100644 java/org/gnu/emacs/EmacsEditable.java (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/configure.ac b/configure.ac index 44529260892..fc95bcd09b6 100644 --- a/configure.ac +++ b/configure.ac @@ -7217,7 +7217,7 @@ if test "$window_system" != "none"; then [Define if you poll periodically to detect C-g.]) WINDOW_SYSTEM_OBJ="fontset.o fringe.o image.o" - if test "$window_system" = "x11"; then + if test "$window_system" = "x11" || test "$REALLY_ANDROID" = "yes"; then AC_DEFINE([HAVE_TEXT_CONVERSION], [1], [Define if the window system has text conversion support.]) WINDOW_SYSTEM_OBJ="$WINDOW_SYSTEM_OBJ textconv.o" diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index b306a63b6cb..2463a75edcd 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -109,3 +109,20 @@ Emacs quitting. @xref{Quitting}. The exact button is used to do this varies by system: on X, it is defined in the variable @code{x-quit-keysym}, and on Android, it is always the volume down button. + +@cindex text conversion, keyboards + Most input methods designed to work with on-screen keyboards perform +buffer edits differently from desktop input methods. + + On a conventional desktop windowing system, an input method will +simply display the contents of any on going character compositions on +screen, and send the appropriate key events to Emacs after completion. + + However, on screen keyboard input methods directly perform edits to +the selected window of each frame; this is known as ``text +conversion'', or ``string conversion'' under the X Window System. + + Text conversion is performed asynchronously whenever Emacs receives +a request to perform the conversion from the input method. After the +conversion completes, a @code{text-conversion} event is sent. +@xref{Misc Events,,, elisp, the Emacs Reference Manual}. diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 2c0787521a5..2807d3d61b2 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2200,6 +2200,15 @@ the buffer in which the xwidget will be displayed, using A few other event types represent occurrences within the system. @table @code +@cindex @code{text-conversion} event +@item text-conversion +This kind of event is sent @strong{after} a system-wide input method +performs an edit to one or more buffers. + +Once the event is sent, the input method may already have made changes +to multiple frames. @c TODO: allow querying which frames to which +@c changes have been made. + @cindex @code{delete-frame} event @item (delete-frame (@var{frame})) This kind of event indicates that the user gave the window manager diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 92429410d03..184c611bf9d 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -279,20 +279,7 @@ public class EmacsContextMenu } }; - synchronized (runnable) - { - EmacsService.SERVICE.runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + EmacsService.syncRunnable (runnable); return rc.thing; } diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index bd5e9ba8ee7..bc0333e99b9 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -317,20 +317,7 @@ public class EmacsDialog implements DialogInterface.OnDismissListener } }; - synchronized (runnable) - { - EmacsService.SERVICE.runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + EmacsService.syncRunnable (runnable); return rc.thing; } diff --git a/java/org/gnu/emacs/EmacsEditable.java b/java/org/gnu/emacs/EmacsEditable.java deleted file mode 100644 index 79af65a6ccd..00000000000 --- a/java/org/gnu/emacs/EmacsEditable.java +++ /dev/null @@ -1,300 +0,0 @@ -/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- - -Copyright (C) 2023 Free Software Foundation, Inc. - -This file is part of GNU Emacs. - -GNU Emacs is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or (at -your option) any later version. - -GNU Emacs is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with GNU Emacs. If not, see . */ - -package org.gnu.emacs; - -import android.text.InputFilter; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.SpanWatcher; -import android.text.Selection; - -import android.content.Context; - -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; - -import android.text.Spannable; - -import android.util.Log; - -import android.os.Build; - -/* Android input methods insist on having access to buffer contents. - Since Emacs is not designed like ``any other Android text editor'', - that is not possible. - - This file provides a fake editing buffer that is designed to weasel - as much information as possible out of an input method, without - actually providing buffer contents to Emacs. - - The basic idea is to have the fake editing buffer be initially - empty. - - When the input method inserts composed text, it sets a flag. - Updates to the buffer while the flag is set are sent to Emacs to be - displayed as ``preedit text''. - - Once some heuristics decide that composition has been completed, - the composed text is sent to Emacs, and the text that was inserted - in this editing buffer is erased. */ - -public class EmacsEditable extends SpannableStringBuilder - implements SpanWatcher -{ - private static final String TAG = "EmacsEditable"; - - /* Whether or not composition is currently in progress. */ - private boolean isComposing; - - /* The associated input connection. */ - private EmacsInputConnection connection; - - /* The associated IM manager. */ - private InputMethodManager imManager; - - /* Any extracted text an input method may be monitoring. */ - private ExtractedText extractedText; - - /* The corresponding text request. */ - private ExtractedTextRequest extractRequest; - - /* The number of nested batch edits. */ - private int batchEditCount; - - /* Whether or not invalidateInput should be called upon batch edits - ending. */ - private boolean pendingInvalidate; - - /* The ``composing span'' indicating the bounds of an ongoing - character composition. */ - private Object composingSpan; - - public - EmacsEditable (EmacsInputConnection connection) - { - /* Initialize the editable with one initial space, so backspace - always works. */ - super (); - - Object tem; - Context context; - - this.connection = connection; - - context = connection.view.getContext (); - tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); - imManager = (InputMethodManager) tem; - - /* To watch for changes to text properties on Android, you - add... a text property. */ - setSpan (this, 0, 0, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } - - public void - endBatchEdit () - { - if (batchEditCount < 1) - return; - - if (--batchEditCount == 0 && pendingInvalidate) - invalidateInput (); - } - - public void - beginBatchEdit () - { - ++batchEditCount; - } - - public void - setExtractedTextAndRequest (ExtractedText text, - ExtractedTextRequest request, - boolean monitor) - { - /* Extract the text. If monitor is set, also record it as the - text that is currently being extracted. */ - - text.startOffset = 0; - text.selectionStart = Selection.getSelectionStart (this); - text.selectionEnd = Selection.getSelectionStart (this); - text.text = this; - - if (monitor) - { - extractedText = text; - extractRequest = request; - } - } - - public void - compositionStart () - { - isComposing = true; - } - - public void - compositionEnd () - { - isComposing = false; - sendComposingText (null); - } - - private void - sendComposingText (String string) - { - EmacsWindow window; - long time, serial; - - window = connection.view.window; - - if (window.isDestroyed ()) - return; - - time = System.currentTimeMillis (); - - /* A composition event is simply a special key event with a - keycode of -1. */ - - synchronized (window.eventStrings) - { - serial - = EmacsNative.sendKeyPress (window.handle, time, 0, -1, -1); - - /* Save the string so that android_lookup_string can find - it. */ - if (string != null) - window.saveUnicodeString ((int) serial, string); - } - } - - private void - invalidateInput () - { - int start, end, composingSpanStart, composingSpanEnd; - - if (batchEditCount > 0) - { - Log.d (TAG, "invalidateInput: deferring for batch edit"); - pendingInvalidate = true; - return; - } - - pendingInvalidate = false; - - start = Selection.getSelectionStart (this); - end = Selection.getSelectionEnd (this); - - if (composingSpan != null) - { - composingSpanStart = getSpanStart (composingSpan); - composingSpanEnd = getSpanEnd (composingSpan); - } - else - { - composingSpanStart = -1; - composingSpanEnd = -1; - } - - Log.d (TAG, "invalidateInput: now " + start + ", " + end); - - /* Tell the input method that the cursor changed. */ - imManager.updateSelection (connection.view, start, end, - composingSpanStart, - composingSpanEnd); - - /* If there is any extracted text, tell the IME that it has - changed. */ - if (extractedText != null) - imManager.updateExtractedText (connection.view, - extractRequest.token, - extractedText); - } - - public SpannableStringBuilder - replace (int start, int end, CharSequence tb, int tbstart, - int tbend) - { - super.replace (start, end, tb, tbstart, tbend); - - /* If a change happens during composition, perform the change and - then send the text being composed. */ - - if (isComposing) - sendComposingText (toString ()); - - return this; - } - - private boolean - isSelectionSpan (Object span) - { - return ((Selection.SELECTION_START == span - || Selection.SELECTION_END == span) - && (getSpanFlags (span) - & Spanned.SPAN_INTERMEDIATE) == 0); - } - - @Override - public void - onSpanAdded (Spannable text, Object what, int start, int end) - { - Log.d (TAG, "onSpanAdded: " + text + " " + what + " " - + start + " " + end); - - /* Try to find the composing span. This isn't a public API. */ - - if (what.getClass ().getName ().contains ("ComposingText")) - composingSpan = what; - - if (isSelectionSpan (what)) - invalidateInput (); - } - - @Override - public void - onSpanChanged (Spannable text, Object what, int ostart, - int oend, int nstart, int nend) - { - Log.d (TAG, "onSpanChanged: " + text + " " + what + " " - + nstart + " " + nend); - - if (isSelectionSpan (what)) - invalidateInput (); - } - - @Override - public void - onSpanRemoved (Spannable text, Object what, - int start, int end) - { - Log.d (TAG, "onSpanRemoved: " + text + " " + what + " " - + start + " " + end); - - if (isSelectionSpan (what)) - invalidateInput (); - } - - public boolean - isInBatchEdit () - { - return batchEditCount > 0; - } -} diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 897a393b984..3cf4419838b 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -25,6 +25,7 @@ import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextSnapshot; import android.view.KeyEvent; import android.text.Editable; @@ -38,138 +39,156 @@ import android.util.Log; public class EmacsInputConnection extends BaseInputConnection { private static final String TAG = "EmacsInputConnection"; - public EmacsView view; - private EmacsEditable editable; - - /* The length of the last string to be committed. */ - private int lastCommitLength; - - int currentLargeOffset; + private EmacsView view; + private short windowHandle; public EmacsInputConnection (EmacsView view) { - super (view, false); + super (view, true); + this.view = view; - this.editable = new EmacsEditable (this); + this.windowHandle = view.window.handle; } @Override - public Editable - getEditable () + public boolean + beginBatchEdit () { - return editable; + Log.d (TAG, "beginBatchEdit"); + EmacsNative.beginBatchEdit (windowHandle); + return true; } @Override public boolean - setComposingText (CharSequence text, int newCursorPosition) + endBatchEdit () { - editable.compositionStart (); - super.setComposingText (text, newCursorPosition); + Log.d (TAG, "endBatchEdit"); + EmacsNative.endBatchEdit (windowHandle); return true; } @Override public boolean - setComposingRegion (int start, int end) + commitCompletion (CompletionInfo info) { - int i; - - if (lastCommitLength != 0) - { - Log.d (TAG, "Restarting composition for: " + lastCommitLength); - - for (i = 0; i < lastCommitLength; ++i) - sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN, - KeyEvent.KEYCODE_DEL)); + Log.d (TAG, "commitCompletion: " + info); + EmacsNative.commitCompletion (windowHandle, + info.getText ().toString (), + info.getPosition ()); + return true; + } - lastCommitLength = 0; - } + @Override + public boolean + commitText (CharSequence text, int newCursorPosition) + { + Log.d (TAG, "commitText: " + text + " " + newCursorPosition); + EmacsNative.commitText (windowHandle, text.toString (), + newCursorPosition); + return true; + } - editable.compositionStart (); - super.setComposingRegion (start, end); + @Override + public boolean + deleteSurroundingText (int leftLength, int rightLength) + { + Log.d (TAG, ("deleteSurroundingText: " + + leftLength + " " + rightLength)); + EmacsNative.deleteSurroundingText (windowHandle, leftLength, + rightLength); return true; } - + @Override public boolean finishComposingText () { - editable.compositionEnd (); - return super.finishComposingText (); + Log.d (TAG, "finishComposingText"); + + EmacsNative.finishComposingText (windowHandle); + return true; + } + + @Override + public String + getSelectedText (int flags) + { + Log.d (TAG, "getSelectedText: " + flags); + + return EmacsNative.getSelectedText (windowHandle, flags); + } + + @Override + public String + getTextAfterCursor (int length, int flags) + { + Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); + + return EmacsNative.getTextAfterCursor (windowHandle, length, + flags); + } + + @Override + public String + getTextBeforeCursor (int length, int flags) + { + Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); + + return EmacsNative.getTextBeforeCursor (windowHandle, length, + flags); } @Override public boolean - beginBatchEdit () + setComposingText (CharSequence text, int newCursorPosition) { - editable.beginBatchEdit (); - return super.beginBatchEdit (); + Log.d (TAG, "setComposingText: " + newCursorPosition); + + EmacsNative.setComposingText (windowHandle, text.toString (), + newCursorPosition); + return true; } @Override public boolean - endBatchEdit () + setComposingRegion (int start, int end) { - editable.endBatchEdit (); - return super.endBatchEdit (); + Log.d (TAG, "setComposingRegion: " + start + " " + end); + + EmacsNative.setComposingRegion (windowHandle, start, end); + return true; } - + @Override public boolean - commitText (CharSequence text, int newCursorPosition) + performEditorAction (int editorAction) { - editable.compositionEnd (); - super.commitText (text, newCursorPosition); - - /* An observation is that input methods rarely recompose trailing - spaces. Avoid re-setting the commit length in that case. */ - - if (text.toString ().equals (" ")) - lastCommitLength += 1; - else - /* At this point, the editable is now empty. - - The input method may try to cancel the edit upon a subsequent - backspace by calling setComposingRegion with a region that is - the length of TEXT. - - Record this length in order to be able to send backspace - events to ``delete'' the text in that case. */ - lastCommitLength = text.length (); - - Log.d (TAG, "commitText: \"" + text + "\""); + Log.d (TAG, "performEditorAction: " + editorAction); + EmacsNative.performEditorAction (windowHandle, editorAction); return true; } - /* Return a large offset, cycling through 100000, 30000, 0. - The offset is typically used to force the input method to update - its notion of ``surrounding text'', bypassing any caching that - it might have in progress. + @Override + public ExtractedText + getExtractedText (ExtractedTextRequest request, int flags) + { + Log.d (TAG, "getExtractedText: " + request + " " + flags); + + return EmacsNative.getExtractedText (windowHandle, request, + flags); + } - There must be another way to do this, but I can't find it. */ + + /* Override functions which are not implemented. */ - public int - largeSelectionOffset () + @Override + public TextSnapshot + takeSnapshot () { - switch (currentLargeOffset) - { - case 0: - currentLargeOffset = 100000; - return 100000; - - case 100000: - currentLargeOffset = 30000; - return 30000; - - case 30000: - currentLargeOffset = 0; - return 0; - } - - currentLargeOffset = 0; - return -1; + Log.d (TAG, "takeSnapshot"); + return null; } } diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index f0219843d35..aaac9446510 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -22,6 +22,8 @@ package org.gnu.emacs; import java.lang.System; import android.content.res.AssetManager; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; public class EmacsNative { @@ -161,6 +163,50 @@ public class EmacsNative descriptor, or NULL if there is none. */ public static native byte[] getProcName (int fd); + /* Notice that the Emacs thread will now start waiting for the main + thread's looper to respond. */ + public static native void beginSynchronous (); + + /* Notice that the Emacs thread will has finished waiting for the + main thread's looper to respond. */ + public static native void endSynchronous (); + + + + /* Input connection functions. These mostly correspond to their + counterparts in Android's InputConnection. */ + + public static native void beginBatchEdit (short window); + public static native void endBatchEdit (short window); + public static native void commitCompletion (short window, String text, + int position); + public static native void commitText (short window, String text, + int position); + public static native void deleteSurroundingText (short window, + int leftLength, + int rightLength); + public static native void finishComposingText (short window); + public static native String getSelectedText (short window, int flags); + public static native String getTextAfterCursor (short window, int length, + int flags); + public static native String getTextBeforeCursor (short window, int length, + int flags); + public static native void setComposingText (short window, String text, + int newCursorPosition); + public static native void setComposingRegion (short window, int start, + int end); + public static native void setSelection (short window, int start, int end); + public static native void performEditorAction (short window, + int editorAction); + public static native ExtractedText getExtractedText (short window, + ExtractedTextRequest req, + int flags); + + + /* Return the current value of the selection, or -1 upon + failure. */ + public static native int getSelection (short window); + static { /* Older versions of Android cannot link correctly with shared diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 2ec2ddf9bda..855a738a30f 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -80,6 +80,11 @@ public class EmacsService extends Service private EmacsThread thread; private Handler handler; + /* Keep this in synch with androidgui.h. */ + public static final int IC_MODE_NULL = 0; + public static final int IC_MODE_ACTION = 1; + public static final int IC_MODE_TEXT = 2; + /* Display metrics used by font backends. */ public DisplayMetrics metrics; @@ -258,20 +263,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + syncRunnable (runnable); return view.thing; } @@ -292,19 +284,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } + syncRunnable (runnable); } public void @@ -502,19 +482,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } + syncRunnable (runnable); } @@ -594,20 +562,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + syncRunnable (runnable); return manager.thing; } @@ -622,4 +577,58 @@ public class EmacsService extends Service startActivity (intent); System.exit (0); } + + /* Wait synchronously for the specified RUNNABLE to complete in the + UI thread. Must be called from the Emacs thread. */ + + public static void + syncRunnable (Runnable runnable) + { + EmacsNative.beginSynchronous (); + + synchronized (runnable) + { + SERVICE.runOnUiThread (runnable); + + while (true) + { + try + { + runnable.wait (); + break; + } + catch (InterruptedException e) + { + continue; + } + } + } + + EmacsNative.endSynchronous (); + } + + public void + updateIC (EmacsWindow window, int newSelectionStart, + int newSelectionEnd, int composingRegionStart, + int composingRegionEnd) + { + Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart + + " " + newSelectionEnd + " " + + composingRegionStart + " " + + composingRegionEnd)); + window.view.imManager.updateSelection (window.view, + newSelectionStart, + newSelectionEnd, + composingRegionStart, + composingRegionEnd); + } + + public void + resetIC (EmacsWindow window, int icMode) + { + Log.d (TAG, "resetIC: " + window); + + window.view.setICMode (icMode); + window.view.imManager.restartInput (window.view); + } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index bc3716f6da8..52da6d41f7d 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -103,6 +103,13 @@ public class EmacsView extends ViewGroup displayed whenever possible. */ public boolean isCurrentlyTextEditor; + /* The associated input connection. */ + private EmacsInputConnection inputConnection; + + /* The current IC mode. See `android_reset_ic' for more + details. */ + private int icMode; + public EmacsView (EmacsWindow window) { @@ -554,14 +561,46 @@ public class EmacsView extends ViewGroup public InputConnection onCreateInputConnection (EditorInfo info) { + int selection, mode; + + /* Figure out what kind of IME behavior Emacs wants. */ + mode = getICMode (); + /* Make sure the input method never displays a full screen input box that obscures Emacs. */ info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; /* Set a reasonable inputType. */ - info.inputType = InputType.TYPE_NULL; + info.inputType = InputType.TYPE_CLASS_TEXT; + + /* Obtain the current position of point and set it as the + selection. */ + selection = EmacsNative.getSelection (window.handle); + + Log.d (TAG, "onCreateInputConnection: current selection is: " + selection); + + /* If this fails or ANDROID_IC_MODE_NULL was requested, then don't + initialize the input connection. */ + if (selection == -1 || mode == EmacsService.IC_MODE_NULL) + { + info.inputType = InputType.TYPE_NULL; + return null; + } + + if (mode == EmacsService.IC_MODE_ACTION) + info.imeOptions |= EditorInfo.IME_ACTION_DONE; - return null; + /* Set the initial selection fields. */ + info.initialSelStart = selection; + info.initialSelEnd = selection; + + /* Create the input connection if necessary. */ + + if (inputConnection == null) + inputConnection = new EmacsInputConnection (this); + + /* Return the input connection. */ + return inputConnection; } @Override @@ -572,4 +611,16 @@ public class EmacsView extends ViewGroup keyboard. */ return isCurrentlyTextEditor; } + + public synchronized void + setICMode (int icMode) + { + this.icMode = icMode; + } + + public synchronized int + getICMode () + { + return icMode; + } }; diff --git a/lisp/bindings.el b/lisp/bindings.el index c77b64c05da..057c870958d 100644 --- a/lisp/bindings.el +++ b/lisp/bindings.el @@ -1521,6 +1521,9 @@ if `inhibit-field-text-motion' is non-nil." (define-key special-event-map [sigusr1] 'ignore) (define-key special-event-map [sigusr2] 'ignore) +;; Text conversion +(define-key global-map [text-conversion] 'ignore) + ;; Don't look for autoload cookies in this file. ;; Local Variables: ;; no-update-autoloads: t diff --git a/src/alloc.c b/src/alloc.c index bc43f22005d..6d8658e7bb0 100644 --- a/src/alloc.c +++ b/src/alloc.c @@ -6939,6 +6939,11 @@ static void mark_frame (struct Lisp_Vector *ptr) { struct frame *f = (struct frame *) ptr; +#ifdef HAVE_TEXT_CONVERSION + struct text_conversion_action *tem; +#endif + + mark_vectorlike (&ptr->header); mark_face_cache (f->face_cache); #ifdef HAVE_WINDOW_SYSTEM @@ -6950,6 +6955,15 @@ mark_frame (struct Lisp_Vector *ptr) mark_vectorlike (&font->header); } #endif + +#ifdef HAVE_TEXT_CONVERSION + mark_object (f->conversion.compose_region_start); + mark_object (f->conversion.compose_region_end); + mark_object (f->conversion.compose_region_overlay); + + for (tem = f->conversion.actions; tem; tem = tem->next) + mark_object (tem->data); +#endif } static void diff --git a/src/android.c b/src/android.c index 479eb9b10d4..8f446224dab 100644 --- a/src/android.c +++ b/src/android.c @@ -98,6 +98,8 @@ struct android_emacs_service jmethodID sync; jmethodID browse_url; jmethodID restart_emacs; + jmethodID update_ic; + jmethodID reset_ic; }; struct android_emacs_pixmap @@ -207,7 +209,7 @@ static struct android_emacs_window window_class; /* The last event serial used. This is a 32 bit value, but it is stored in unsigned long to be consistent with X. */ -static unsigned int event_serial; +unsigned int event_serial; /* Unused pointer used to control compiler optimizations. */ void *unused_pointer; @@ -408,6 +410,10 @@ android_handle_sigusr1 (int sig, siginfo_t *siginfo, void *arg) #endif +/* Semaphore used to indicate completion of a query. + This should ideally be defined further down. */ +static sem_t android_query_sem; + /* Set up the global event queue by initializing the mutex and two condition variables, and the linked list of events. This must be called before starting the Emacs thread. Also, initialize the @@ -438,6 +444,7 @@ android_init_events (void) sem_init (&android_pselect_sem, 0, 0); sem_init (&android_pselect_start_sem, 0, 0); + sem_init (&android_query_sem, 0, 0); event_queue.events.next = &event_queue.events; event_queue.events.last = &event_queue.events; @@ -538,7 +545,7 @@ android_next_event (union android_event *event_return) pthread_mutex_unlock (&event_queue.mutex); } -static void +void android_write_event (union android_event *event) { struct android_event_container *container; @@ -576,6 +583,10 @@ android_select (int nfds, fd_set *readfds, fd_set *writefds, static char byte; #endif + /* Check for and run anything the UI thread wants to run on the main + thread. */ + android_check_query (); + pthread_mutex_lock (&event_queue.mutex); if (event_queue.num_events) @@ -634,6 +645,10 @@ android_select (int nfds, fd_set *readfds, fd_set *writefds, if (nfds_return < 0) errno = EINTR; + /* Now check for and run anything the UI thread wants to run in the + main thread. */ + android_check_query (); + return nfds_return; } @@ -1431,7 +1446,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, /* This may be called from multiple threads. setEmacsParams should only ever be called once. */ - if (__atomic_fetch_add (&emacs_initialized, -1, __ATOMIC_RELAXED)) + if (__atomic_fetch_add (&emacs_initialized, -1, __ATOMIC_SEQ_CST)) { ANDROID_THROW (env, "java/lang/IllegalArgumentException", "Emacs was already initialized!"); @@ -1705,6 +1720,10 @@ android_init_emacs_service (void) FIND_METHOD (browse_url, "browseUrl", "(Ljava/lang/String;)" "Ljava/lang/String;"); FIND_METHOD (restart_emacs, "restartEmacs", "()V"); + FIND_METHOD (update_ic, "updateIC", + "(Lorg/gnu/emacs/EmacsWindow;IIII)V"); + FIND_METHOD (reset_ic, "resetIC", + "(Lorg/gnu/emacs/EmacsWindow;I)V"); #undef FIND_METHOD } @@ -1834,7 +1853,7 @@ android_init_emacs_window (void) #undef FIND_METHOD } -extern JNIEXPORT void JNICALL +JNIEXPORT void JNICALL NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, jobject dump_file_object, jint api_level) { @@ -1928,19 +1947,19 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, emacs_abort (); } -extern JNIEXPORT void JNICALL +JNIEXPORT void JNICALL NATIVE_NAME (emacsAbort) (JNIEnv *env, jobject object) { emacs_abort (); } -extern JNIEXPORT void JNICALL +JNIEXPORT void JNICALL NATIVE_NAME (quit) (JNIEnv *env, jobject object) { Vquit_flag = Qt; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, jshort window, jlong time, jint x, jint y, jint width, @@ -1961,7 +1980,7 @@ NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -1981,7 +2000,7 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -2001,7 +2020,7 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, jshort window, jlong time) { @@ -2016,7 +2035,7 @@ NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, jshort window, jlong time) { @@ -2031,7 +2050,7 @@ NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, return ++event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, jshort window, jint action) { @@ -2046,7 +2065,7 @@ NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -2064,7 +2083,7 @@ NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -2082,7 +2101,7 @@ NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -2100,7 +2119,7 @@ NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -2121,7 +2140,7 @@ NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -2142,7 +2161,7 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -2161,7 +2180,7 @@ NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -2180,7 +2199,7 @@ NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -2199,7 +2218,7 @@ NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -2221,7 +2240,7 @@ NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, jshort window) { @@ -2235,7 +2254,7 @@ NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, jshort window) { @@ -2249,7 +2268,7 @@ NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, jshort window, jint menu_event_id) { @@ -2264,7 +2283,7 @@ NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jint width, jint height) @@ -2283,6 +2302,23 @@ NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, return event_serial; } +/* Forward declarations of deadlock prevention functions. */ + +static void android_begin_query (void); +static void android_end_query (void); + +JNIEXPORT void JNICALL +NATIVE_NAME (beginSynchronous) (JNIEnv *env, jobject object) +{ + android_begin_query (); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (endSynchronous) (JNIEnv *env, jobject object) +{ + android_end_query (); +} + #ifdef __clang__ #pragma clang diagnostic pop #else @@ -5155,6 +5191,215 @@ android_get_current_api_level (void) +/* Whether or not a query is currently being made. */ +static bool android_servicing_query; + +/* Function that is waiting to be run in the Emacs thread. */ +static void (*android_query_function) (void *); + +/* Context for that function. */ +static void *android_query_context; + +/* Deadlock protection. The UI thread and the Emacs thread must + sometimes make synchronous queries to each other, which are + normally answered inside each thread's respective event loop. + Deadlocks can happen when both threads simultaneously make such + synchronous queries and block waiting for each others responses. + + The Emacs thread can be interrupted to service any queries made by + the UI thread, but is not possible the other way around. + + To avoid such deadlocks, an atomic counter is provided. This + counter is incremented every time a query starts, and is set to + zerp every time one ends. If the UI thread tries to make a query + and sees that the counter is non-zero, it simply returns so that + its event loop can proceed to perform and respond to the query. If + the Emacs thread sees the same thing, then it stops to service all + queries being made by the input method, then proceeds to make its + query. */ + +/* Run any function that the UI thread has asked to run, and then + signal its completion. */ + +void +android_check_query (void) +{ + void (*proc) (void *); + void *closure; + + if (!__atomic_load_n (&android_servicing_query, __ATOMIC_SEQ_CST)) + return; + + /* First, load the procedure and closure. */ + __atomic_load (&android_query_context, &closure, __ATOMIC_SEQ_CST); + __atomic_load (&android_query_function, &proc, __ATOMIC_SEQ_CST); + + if (!proc) + return; + + proc (closure); + + /* Finish the query. */ + __atomic_store_n (&android_query_context, NULL, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, NULL, __ATOMIC_SEQ_CST); + __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); + + /* Signal completion. */ + sem_post (&android_query_sem); +} + +/* Notice that the Emacs thread will start blocking waiting for a + response from the UI thread. Process any pending queries from the + UI thread. + + This function may be called from Java. */ + +static void +android_begin_query (void) +{ + if (__atomic_test_and_set (&android_servicing_query, + __ATOMIC_SEQ_CST)) + { + /* Answer the query that is currently being made. */ + assert (android_query_function != NULL); + android_check_query (); + + /* Wait for that query to complete. */ + while (__atomic_load_n (&android_servicing_query, + __ATOMIC_SEQ_CST)) + ;; + } +} + +/* Notice that a query has stopped. This function may be called from + Java. */ + +static void +android_end_query (void) +{ + __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); +} + +/* Synchronously ask the Emacs thread to run the specified PROC with + the given CLOSURE. Return if this fails, or once PROC is run. + + PROC may be run from inside maybe_quit. + + It is not okay to run Lisp code which signals or performs non + trivial tasks inside PROC. + + Return 1 if the Emacs thread is currently waiting for the UI thread + to respond and PROC could not be run, or 0 otherwise. */ + +int +android_run_in_emacs_thread (void (*proc) (void *), void *closure) +{ + union android_event event; + + event.xaction.type = ANDROID_WINDOW_ACTION; + event.xaction.serial = ++event_serial; + event.xaction.window = 0; + event.xaction.action = 0; + + /* Set android_query_function and android_query_context. */ + __atomic_store_n (&android_query_context, closure, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, proc, __ATOMIC_SEQ_CST); + + /* Don't allow deadlocks to happen; make sure the Emacs thread is + not waiting for something to be done. */ + + if (__atomic_test_and_set (&android_servicing_query, + __ATOMIC_SEQ_CST)) + { + __atomic_store_n (&android_query_context, NULL, + __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, NULL, + __ATOMIC_SEQ_CST); + + return 1; + } + + /* Send a dummy event. `android_check_query' will be called inside + wait_reading_process_output after the event arrives. + + Otherwise, android_select will call android_check_thread the next + time it is entered. */ + android_write_event (&event); + + /* Start waiting for the function to be executed. */ + while (sem_wait (&android_query_sem) < 0) + ;; + + return 0; +} + + + +/* Input method related functions. */ + +/* Change WINDOW's active selection to the characters between + SELECTION_START and SELECTION_END. + + Also, update the composing region to COMPOSING_REGION_START and + COMPOSING_REGION_END. + + If any value cannot fit in jint, then the behavior of the input + method is undefined. */ + +void +android_update_ic (android_window window, ptrdiff_t selection_start, + ptrdiff_t selection_end, ptrdiff_t composing_region_start, + ptrdiff_t composing_region_end) +{ + jobject object; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.update_ic, + object, + (jint) selection_start, + (jint) selection_end, + (jint) composing_region_start, + (jint) composing_region_end); + android_exception_check (); +} + +/* Reinitialize any ongoing input method connection on WINDOW. + + Any input method that is connected to WINDOW will invalidate its + cache of the buffer contents. + + MODE controls certain aspects of the input method's behavior: + + - If MODE is ANDROID_IC_MODE_NULL, the input method will be + deactivated, and an ASCII only keyboard will be displayed + instead. + + - If MODE is ANDROID_IC_MODE_ACTION, the input method will + edit text normally, but send ``return'' as a key event. + This is useful inside the mini buffer. + + - If MODE is ANDROID_IC_MODE_TEXT, the input method is free + to behave however it wants. */ + +void +android_reset_ic (android_window window, enum android_ic_mode mode) +{ + jobject object; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.reset_ic, + object, (jint) mode); + android_exception_check (); +} + + + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index 9006f5f34c5..ec4fa33dfc3 100644 --- a/src/android.h +++ b/src/android.h @@ -108,6 +108,16 @@ extern void android_closedir (struct android_dir *); extern Lisp_Object android_browse_url (Lisp_Object); + + +/* Event loop functions. */ + +extern void android_check_query (void); +extern int android_run_in_emacs_thread (void (*) (void *), void *); +extern void android_write_event (union android_event *); + +extern unsigned int event_serial; + #endif /* JNI functions should not be built when Emacs is stubbed out for the diff --git a/src/androidgui.h b/src/androidgui.h index 1f0d34e67aa..25dc6754fff 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -235,6 +235,7 @@ enum android_event_type ANDROID_DEICONIFIED, ANDROID_CONTEXT_MENU, ANDROID_EXPOSE, + ANDROID_INPUT_METHOD, }; struct android_any_event @@ -419,6 +420,52 @@ struct android_menu_event int menu_event_id; }; +enum android_ime_operation + { + ANDROID_IME_COMMIT_TEXT, + ANDROID_IME_DELETE_SURROUNDING_TEXT, + ANDROID_IME_FINISH_COMPOSING_TEXT, + ANDROID_IME_SET_COMPOSING_TEXT, + ANDROID_IME_SET_COMPOSING_REGION, + ANDROID_IME_SET_POINT, + ANDROID_IME_START_BATCH_EDIT, + ANDROID_IME_END_BATCH_EDIT, + }; + +struct android_ime_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* The event serial. */ + unsigned long serial; + + /* The associated window. */ + android_window window; + + /* What operation is being performed. */ + enum android_ime_operation operation; + + /* The details of the operation. START and END provide buffer + indices, and may actually mean ``left'' and ``right''. */ + ptrdiff_t start, end, position; + + /* The number of characters in TEXT. */ + size_t length; + + /* TEXT is either NULL, or a pointer to LENGTH bytes of malloced + UTF-16 encoded text that must be decoded by Emacs. + + POSITION is where point should end up after the text is + committed, relative to TEXT. If POSITION is less than 0, it is + relative to TEXT's start; otherwise, it is relative to its + end. */ + unsigned short *text; + + /* Value to set the counter to after the operation completes. */ + unsigned long counter; +}; + union android_event { enum android_event_type type; @@ -447,6 +494,9 @@ union android_event /* This is only used to transmit selected menu items. */ struct android_menu_event menu; + + /* This is used to dispatch input method editing requests. */ + struct android_ime_event ime; }; enum @@ -463,6 +513,13 @@ enum android_lookup_status ANDROID_LOOKUP_BOTH, }; +enum android_ic_mode + { + ANDROID_IC_MODE_NULL = 0, + ANDROID_IC_MODE_ACTION = 1, + ANDROID_IC_MODE_TEXT = 2, + }; + extern int android_pending (void); extern void android_next_event (union android_event *); @@ -554,6 +611,9 @@ extern void android_sync (void); extern int android_wc_lookup_string (android_key_pressed_event *, wchar_t *, int, int *, enum android_lookup_status *); +extern void android_update_ic (android_window, ptrdiff_t, ptrdiff_t, + ptrdiff_t, ptrdiff_t); +extern void android_reset_ic (android_window, enum android_ic_mode); #endif diff --git a/src/androidterm.c b/src/androidterm.c index a57d5623c66..a44bae954da 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -20,6 +20,9 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include +#include +#include #include "lisp.h" #include "androidterm.h" @@ -28,6 +31,8 @@ along with GNU Emacs. If not, see . */ #include "android.h" #include "buffer.h" #include "window.h" +#include "textconv.h" +#include "coding.h" /* This is a chain of structures for all the X displays currently in use. */ @@ -59,6 +64,12 @@ enum ANDROID_EVENT_DROP, }; +/* Find the frame whose window has the identifier WDESC. + + This is like x_window_to_frame in xterm.c, except that DPYINFO may + be NULL, as there is only at most one Android display, and is only + specified in order to stay consistent with X. */ + static struct frame * android_window_to_frame (struct android_display_info *dpyinfo, android_window wdesc) @@ -73,7 +84,7 @@ android_window_to_frame (struct android_display_info *dpyinfo, { f = XFRAME (frame); - if (!FRAME_ANDROID_P (f) || FRAME_DISPLAY_INFO (f) != dpyinfo) + if (!FRAME_ANDROID_P (f)) continue; if (FRAME_ANDROID_WINDOW (f) == wdesc) @@ -527,6 +538,103 @@ android_find_tool (struct frame *f, int pointer_id) return NULL; } +/* Decode STRING, an array of N little endian UTF-16 characters, into + a Lisp string. Return Qnil if the string is too large, and the + encoded string otherwise. */ + +static Lisp_Object +android_decode_utf16 (unsigned short *utf16, size_t n) +{ + struct coding_system coding; + ptrdiff_t size; + + if (INT_MULTIPLY_WRAPV (n, sizeof *utf16, &size)) + return Qnil; + + /* Set up the coding system. Decoding a UTF-16 string (with no BOM) + should not signal. */ + + memset (&coding, 0, sizeof coding); + + setup_coding_system (Qutf_16le, &coding); + coding.source = (const unsigned char *) utf16; + decode_coding_object (&coding, Qnil, 0, 0, size, + size, Qt); + + return coding.dst_object; +} + +/* Handle a single input method event EVENT, delivered to the frame + F. + + Perform the text conversion action specified inside. */ + +static void +android_handle_ime_event (union android_event *event, struct frame *f) +{ + Lisp_Object text; + + /* First, decode the text if necessary. */ + + switch (event->ime.operation) + { + case ANDROID_IME_COMMIT_TEXT: + case ANDROID_IME_FINISH_COMPOSING_TEXT: + case ANDROID_IME_SET_COMPOSING_TEXT: + text = android_decode_utf16 (event->ime.text, + event->ime.length); + xfree (event->ime.text); + break; + + default: + break; + } + + /* Finally, perform the appropriate conversion action. */ + + switch (event->ime.operation) + { + case ANDROID_IME_COMMIT_TEXT: + commit_text (f, text, event->ime.position, + event->ime.counter); + break; + + case ANDROID_IME_DELETE_SURROUNDING_TEXT: + delete_surrounding_text (f, event->ime.start, + event->ime.end, + event->ime.counter); + break; + + case ANDROID_IME_FINISH_COMPOSING_TEXT: + finish_composing_text (f, event->ime.counter); + break; + + case ANDROID_IME_SET_COMPOSING_TEXT: + set_composing_text (f, text, event->ime.position, + event->ime.counter); + break; + + case ANDROID_IME_SET_COMPOSING_REGION: + set_composing_region (f, event->ime.start, + event->ime.end, + event->ime.counter); + break; + + case ANDROID_IME_SET_POINT: + textconv_set_point (f, event->ime.position, + event->ime.counter); + break; + + case ANDROID_IME_START_BATCH_EDIT: + start_batch_edit (f, event->ime.counter); + break; + + case ANDROID_IME_END_BATCH_EDIT: + end_batch_edit (f, event->ime.counter); + break; + } +} + static int handle_one_android_event (struct android_display_info *dpyinfo, union android_event *event, int *finish, @@ -745,6 +853,17 @@ handle_one_android_event (struct android_display_info *dpyinfo, case ANDROID_WINDOW_ACTION: + /* This is a special event sent by android_run_in_emacs_thread + used to make Android run stuff. */ + + if (!event->xaction.window && !event->xaction.action) + { + /* Check for and run anything the UI thread wants to run on the main + thread. */ + android_check_query (); + goto OTHER; + } + f = any; if (event->xaction.action == 0) @@ -1334,6 +1453,19 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto OTHER; + /* Input method events. textconv.c functions are called here to + queue events, which are then executed in a safe context + inside keyboard.c. */ + case ANDROID_INPUT_METHOD: + + if (!any) + /* Free any text allocated for this event. */ + xfree (event->ime.text); + else + android_handle_ime_event (event, any); + + goto OTHER; + default: goto OTHER; } @@ -4148,6 +4280,904 @@ android_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1) +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-prototypes" +#endif + +/* Input method related functions. Some of these are called from Java + within the UI thread. */ + +/* A counter used to decide when an editing request completes. */ +static unsigned long edit_counter; + +/* The last counter known to have completed. */ +static unsigned long last_edit_counter; + +/* Semaphore posted every time the counter increases. */ +static sem_t edit_sem; + +/* Try to synchronize with the UI thread, waiting a certain amount of + time for outstanding editing requests to complete. + + Every time one of the text retrieval functions is called and an + editing request is made, Emacs gives the main thread approximately + 50 ms to process it, in order to mostly keep the input method in + sync with the buffer contents. */ + +static void +android_sync_edit (void) +{ + struct timespec start, end, rem; + + if (__atomic_load_n (&last_edit_counter, + __ATOMIC_SEQ_CST) + == edit_counter) + return; + + start = current_timespec (); + end = timespec_add (start, make_timespec (0, 50000000)); + + while (true) + { + rem = timespec_sub (end, current_timespec ()); + + /* Timeout. */ + if (timespec_sign (rem) < 0) + break; + + if (__atomic_load_n (&last_edit_counter, + __ATOMIC_SEQ_CST) + == edit_counter) + break; + + sem_timedwait (&edit_sem, &end); + } +} + +/* Return a copy of the specified Java string and its length in + *LENGTH. Use the JNI environment ENV. Value is NULL if copying + *the string fails. */ + +static unsigned short * +android_copy_java_string (JNIEnv *env, jstring string, size_t *length) +{ + jsize size, i; + const jchar *java; + unsigned short *buffer; + + size = (*env)->GetStringLength (env, string); + buffer = malloc (size * sizeof *buffer); + + if (!buffer) + return NULL; + + java = (*env)->GetStringChars (env, string, NULL); + + if (!java) + { + free (buffer); + return NULL; + } + + for (i = 0; i < size; ++i) + buffer[i] = java[i]; + + *length = size; + (*env)->ReleaseStringChars (env, string, java); + return buffer; +} + +JNIEXPORT void JNICALL +NATIVE_NAME (beginBatchEdit) (JNIEnv *env, jobject object, jshort window) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_START_BATCH_EDIT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (endBatchEdit) (JNIEnv *env, jobject object, jshort window) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_END_BATCH_EDIT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (commitCompletion) (JNIEnv *env, jobject object, jshort window, + jstring completion_text, jint position) +{ + union android_event event; + unsigned short *text; + size_t length; + + /* First, obtain a copy of the Java string. */ + text = android_copy_java_string (env, completion_text, &length); + + if (!text) + return; + + /* Next, populate the event. Events will always eventually be + delivered on Android, so handle_one_android_event can be relied + on to free text. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_COMMIT_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = min (length, PTRDIFF_MAX); + event.ime.position = position; + event.ime.text = text; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (commitText) (JNIEnv *env, jobject object, jshort window, + jstring commit_text, jint position) +{ + union android_event event; + unsigned short *text; + size_t length; + + /* First, obtain a copy of the Java string. */ + text = android_copy_java_string (env, commit_text, &length); + + if (!text) + return; + + /* Next, populate the event. Events will always eventually be + delivered on Android, so handle_one_android_event can be relied + on to free text. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_COMMIT_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = min (length, PTRDIFF_MAX); + event.ime.position = position; + event.ime.text = text; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (deleteSurroundingText) (JNIEnv *env, jobject object, + jshort window, jint left_length, + jint right_length) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_DELETE_SURROUNDING_TEXT; + event.ime.start = left_length; + event.ime.end = right_length; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (finishComposingText) (JNIEnv *env, jobject object, + jshort window) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_FINISH_COMPOSING_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +/* Structure describing the context used for a text query. */ + +struct android_conversion_query_context +{ + /* The conversion request. */ + struct textconv_callback_struct query; + + /* The window the request is being made on. */ + android_window window; + + /* Whether or not the request was successful. */ + bool success; +}; + +/* Obtain the text from the frame whose window is that specified in + DATA using the text conversion query specified there. + + Adjust the query position to skip over any active composing region. + + Set ((struct android_conversion_query_context *) DATA)->success on + success. */ + +static void +android_perform_conversion_query (void *data) +{ + struct android_conversion_query_context *context; + struct frame *f; + + context = data; + + /* Find the frame associated with the window. */ + f = android_window_to_frame (NULL, context->window); + + if (!f) + return; + + textconv_query (f, &context->query, + TEXTCONV_SKIP_CONVERSION_REGION); + + /* context->query.text will have been set even if textconv_query + returns 1. */ + + context->success = true; +} + +/* Convert a string BUFFERS containing N characters in Emacs's + internal multibyte encoding to a Java string utilizing the + specified JNI environment. + + If N is equal to BYTES, then BUFFER is a single byte buffer. + Otherwise, BUFFER is a multibyte buffer. + + Make sure N and BYTES are absolutely correct, or you are asking for + trouble. + + Value is the string upon success, NULL otherwise. Any exceptions + generated are not cleared. */ + +static jstring +android_text_to_string (JNIEnv *env, char *buffer, ptrdiff_t n, + ptrdiff_t bytes) +{ + jchar *utf16; + size_t size, index; + jstring string; + int encoded; + + if (n == bytes) + { + /* This buffer holds no multibyte characters. */ + + if (INT_MULTIPLY_WRAPV (n, sizeof *utf16, &size)) + return NULL; + + utf16 = malloc (size); + index = 0; + + if (!utf16) + return NULL; + + while (n--) + { + utf16[index] = buffer[index]; + index++; + } + + string = (*env)->NewString (env, utf16, bytes); + free (utf16); + + return string; + } + + /* Allocate enough to hold N characters. */ + + if (INT_MULTIPLY_WRAPV (n, sizeof *utf16, &size)) + return NULL; + + utf16 = malloc (size); + index = 0; + + if (!utf16) + return NULL; + + while (n--) + { + eassert (CHAR_HEAD_P (*buffer)); + encoded = STRING_CHAR ((unsigned char *) buffer); + + /* Now figure out how to save ENCODED into the string. + Emacs operates on multibyte characters, not UTF-16 + characters with surrogate pairs as Android does. + + However, character positions in Java are represented in 2 + byte units, meaning that the text position reported to + Android can become out of sync if characters are found in a + buffer that require surrogate pairs. + + The hack used by Emacs is to simply replace each multibyte + character that doesn't fit in a jchar with the NULL + character. */ + + if (encoded >= 65536) + encoded = 0; + + utf16[index++] = encoded; + buffer += BYTES_BY_CHAR_HEAD (*buffer); + } + + /* Create the string. */ + string = (*env)->NewString (env, utf16, index); + free (utf16); + return string; +} + +JNIEXPORT jstring JNICALL +NATIVE_NAME (getSelectedText) (JNIEnv *env, jobject object, + jshort window) +{ + return NULL; +} + +JNIEXPORT jstring JNICALL +NATIVE_NAME (getTextAfterCursor) (JNIEnv *env, jobject object, jshort window, + jint length, jint flags) +{ + struct android_conversion_query_context context; + jstring string; + + /* First, set up the conversion query. */ + context.query.position = 0; + context.query.direction = TEXTCONV_FORWARD_CHAR; + context.query.factor = min (length, 65535); + context.query.operation = TEXTCONV_RETRIEVAL; + + /* Next, set the rest of the context. */ + context.window = window; + context.success = false; + + /* Now try to perform the query. */ + android_sync_edit (); + if (android_run_in_emacs_thread (android_perform_conversion_query, + &context)) + return NULL; + + if (!context.success) + return NULL; + + /* context->query.text now contains the text in Emacs's internal + UTF-8 based encoding. + + Convert it to Java's UTF-16 encoding, which is the same as + UTF-16, except that NULL bytes are encoded as surrogate pairs. + + This assumes that `free' can free data allocated with xmalloc. */ + + string = android_text_to_string (env, context.query.text.text, + context.query.text.length, + context.query.text.bytes); + free (context.query.text.text); + + return string; +} + +JNIEXPORT jstring JNICALL +NATIVE_NAME (getTextBeforeCursor) (JNIEnv *env, jobject object, jshort window, + jint length, jint flags) +{ + struct android_conversion_query_context context; + jstring string; + + /* First, set up the conversion query. */ + context.query.position = 0; + context.query.direction = TEXTCONV_BACKWARD_CHAR; + context.query.factor = min (length, 65535); + context.query.operation = TEXTCONV_RETRIEVAL; + + /* Next, set the rest of the context. */ + context.window = window; + context.success = false; + + /* Now try to perform the query. */ + android_sync_edit (); + if (android_run_in_emacs_thread (android_perform_conversion_query, + &context)) + return NULL; + + if (!context.success) + return NULL; + + /* context->query.text now contains the text in Emacs's internal + UTF-8 based encoding. + + Convert it to Java's UTF-16 encoding, which is the same as + UTF-16, except that NULL bytes are encoded as surrogate pairs. + + This assumes that `free' can free data allocated with xmalloc. */ + + string = android_text_to_string (env, context.query.text.text, + context.query.text.length, + context.query.text.bytes); + free (context.query.text.text); + + return string; +} + +JNIEXPORT void JNICALL +NATIVE_NAME (setComposingText) (JNIEnv *env, jobject object, jshort window, + jstring composing_text, + jint new_cursor_position) +{ + union android_event event; + unsigned short *text; + size_t length; + + /* First, obtain a copy of the Java string. */ + text = android_copy_java_string (env, composing_text, &length); + + if (!text) + return; + + /* Next, populate the event. Events will always eventually be + delivered on Android, so handle_one_android_event can be relied + on to free text. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_SET_COMPOSING_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = min (length, PTRDIFF_MAX); + event.ime.position = new_cursor_position; + event.ime.text = text; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (setComposingRegion) (JNIEnv *env, jobject object, jshort window, + jint start, jint end) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_SET_COMPOSING_REGION; + event.ime.start = start; + event.ime.end = end; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (setSelection) (JNIEnv *env, jobject object, jshort window, + jint start, jint end) +{ + union android_event event; + + /* While IMEs want access to the entire selection, Emacs only + supports setting the point. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_SET_POINT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = start; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; +} + +/* Structure describing the context for `getSelection'. */ + +struct android_get_selection_context +{ + /* The window in question. */ + android_window window; + + /* The position of the window's point when it was last + redisplayed. */ + ptrdiff_t point; +}; + +/* Function run on the main thread by `getSelection'. + Place the character position of point in PT. */ + +static void +android_get_selection (void *data) +{ + struct android_get_selection_context *context; + struct frame *f; + struct window *w; + + context = data; + + /* Look up the associated frame and its selected window. */ + f = android_window_to_frame (NULL, context->window); + + if (!f) + context->point = -1; + else + { + w = XWINDOW (f->selected_window); + + /* Return W's point at the time of the last redisplay. This is + rather important to keep the input method consistent with the + contents of the display. */ + context->point = w->last_point; + } +} + +JNIEXPORT jint JNICALL +NATIVE_NAME (getSelection) (JNIEnv *env, jobject object, jshort window) +{ + struct android_get_selection_context context; + + context.window = window; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_selection, + &context)) + return -1; + + if (context.point == -1) + return -1; + + return min (context.point, TYPE_MAXIMUM (jint)); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (performEditorAction) (JNIEnv *env, jobject object, + jshort window, int action) +{ + union android_event event; + + /* Undocumented behavior: performEditorAction is apparently expected + to finish composing any text. */ + + NATIVE_NAME (finishComposingText) (env, object, window); + + event.xkey.type = ANDROID_KEY_PRESS; + event.xkey.serial = ++event_serial; + event.xkey.window = window; + event.xkey.time = 0; + event.xkey.state = 0; + event.xkey.keycode = 66; + event.xkey.unicode_char = 0; + + android_write_event (&event); +} + +struct android_get_extracted_text_context +{ + /* The parameters of the request. */ + int hint_max_chars; + + /* Token for the request. */ + int token; + + /* The returned text, or NULL. */ + char *text; + + /* The size of that text in characters and bytes. */ + ptrdiff_t length, bytes; + + /* Offsets into that text. */ + ptrdiff_t start, offset; + + /* The window. */ + android_window window; +}; + +/* Return the extracted text in the extracted text context specified + by DATA. */ + +static void +android_get_extracted_text (void *data) +{ + struct android_get_extracted_text_context *request; + struct frame *f; + + request = data; + + /* Find the frame associated with the window. */ + f = android_window_to_frame (NULL, request->window); + + if (!f) + return; + + /* Now get the extracted text. */ + request->text + = get_extracted_text (f, min (request->hint_max_chars, 600), + &request->start, &request->offset, + &request->length, &request->bytes); +} + +/* Structure describing the `ExtractedTextRequest' class. + Valid only on the UI thread. */ + +struct android_extracted_text_request_class +{ + bool initialized; + jfieldID hint_max_chars; + jfieldID token; +}; + +/* Structure describing the `ExtractedText' class. + Valid only on the UI thread. */ + +struct android_extracted_text_class +{ + jclass class; + jmethodID constructor; + jfieldID partial_start_offset; + jfieldID partial_end_offset; + jfieldID selection_start; + jfieldID selection_end; + jfieldID start_offset; + jfieldID text; +}; + +JNIEXPORT jobject JNICALL +NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, + jshort window, jobject request, + jint flags) +{ + struct android_get_extracted_text_context context; + static struct android_extracted_text_request_class request_class; + static struct android_extracted_text_class text_class; + jstring string; + jclass class; + jobject object; + + /* TODO: report changes to extracted text. */ + + /* Initialize both classes if necessary. */ + + if (!request_class.initialized) + { + class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/ExtractedTextRequest")); + assert (class); + + request_class.hint_max_chars + = (*env)->GetFieldID (env, class, "hintMaxChars", "I"); + assert (request_class.hint_max_chars); + + request_class.token + = (*env)->GetFieldID (env, class, "token", "I"); + assert (request_class.token); + + request_class.initialized = true; + } + + if (!text_class.class) + { + text_class.class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/ExtractedText")); + assert (text_class.class); + + class + = text_class.class + = (*env)->NewGlobalRef (env, text_class.class); + assert (text_class.class); + + text_class.partial_start_offset + = (*env)->GetFieldID (env, class, "partialStartOffset", "I"); + text_class.partial_end_offset + = (*env)->GetFieldID (env, class, "partialEndOffset", "I"); + text_class.selection_start + = (*env)->GetFieldID (env, class, "selectionStart", "I"); + text_class.selection_end + = (*env)->GetFieldID (env, class, "selectionEnd", "I"); + text_class.start_offset + = (*env)->GetFieldID (env, class, "startOffset", "I"); + text_class.text + = (*env)->GetFieldID (env, class, "text", "Ljava/lang/CharSequence;"); + text_class.constructor + = (*env)->GetMethodID (env, class, "", "()V"); + } + + context.hint_max_chars + = (*env)->GetIntField (env, request, request_class.hint_max_chars); + context.token + = (*env)->GetIntField (env, request, request_class.token); + context.text = NULL; + context.window = window; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_extracted_text, + &context)) + return NULL; + + if (!context.text) + return NULL; + + /* Encode the returned text. */ + string = android_text_to_string (env, context.text, context.length, + context.bytes); + free (context.text); + + if (!string) + return NULL; + + /* Create an ExtractedText object containing this information. */ + object = (*android_java_env)->NewObject (env, text_class.class, + text_class.constructor); + if (!object) + return NULL; + + (*env)->SetIntField (env, object, text_class.partial_start_offset, -1); + (*env)->SetIntField (env, object, text_class.partial_end_offset, -1); + (*env)->SetIntField (env, object, text_class.selection_start, + min (context.offset, TYPE_MAXIMUM (jint))); + (*env)->SetIntField (env, object, text_class.selection_end, + min (context.offset, TYPE_MAXIMUM (jint))); + (*env)->SetIntField (env, object, text_class.start_offset, + min (context.start, TYPE_MAXIMUM (jint))); + (*env)->SetObjectField (env, object, text_class.text, string); + return object; +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#else +#pragma GCC diagnostic pop +#endif + + + +/* Tell the input method where the composing region and selection of + F's selected window is located. W should be F's selected window; + if it is NULL, then F->selected_window is used in its place. */ + +static void +android_update_selection (struct frame *f, struct window *w) +{ + ptrdiff_t start, end, point; + + if (MARKERP (f->conversion.compose_region_start)) + { + eassert (MARKERP (f->conversion.compose_region_end)); + + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + } + else + start = -1, end = -1; + + /* Now constrain START and END to the maximium size of a Java + integer. */ + start = min (start, TYPE_MAXIMUM (jint)); + end = min (end, TYPE_MAXIMUM (jint)); + + if (!w) + w = XWINDOW (f->selected_window); + + /* Figure out where the point is. */ + point = min (w->last_point, TYPE_MAXIMUM (jint)); + + /* Send the update. */ + android_update_ic (FRAME_ANDROID_WINDOW (f), point, point, + start, end); +} + +/* Notice that the input method connection to F should be reset as a + result of a change to its contents. */ + +static void +android_reset_conversion (struct frame *f) +{ + /* Reset the input method. + + Pick an appropriate ``input mode'' based on whether or not the + minibuffer window is selected; this controls whether or not + ``RET'' inserts a newline or sends an actual key event. */ + android_reset_ic (FRAME_ANDROID_WINDOW (f), + (EQ (f->selected_window, + f->minibuffer_window) + ? ANDROID_IC_MODE_ACTION + : ANDROID_IC_MODE_TEXT)); + + /* Move its selection to the specified position. */ + android_update_selection (f, NULL); +} + +/* Notice that point has moved in the F's selected window's selected + buffer. W is the window, and BUFFER is that buffer. */ + +static void +android_set_point (struct frame *f, struct window *w, + struct buffer *buffer) +{ + android_update_selection (f, w); +} + +/* Notice that the composition region on F's old selected window has + changed. */ + +static void +android_compose_region_changed (struct frame *f) +{ + android_update_selection (f, XWINDOW (f->old_selected_window)); +} + +/* Notice that the text conversion has completed. */ + +static void +android_notify_conversion (unsigned long counter) +{ + int sval; + + if (last_edit_counter < counter) + __atomic_store_n (&last_edit_counter, counter, + __ATOMIC_SEQ_CST); + + sem_getvalue (&edit_sem, &sval); + + if (sval <= 0) + sem_post (&edit_sem); +} + +/* Android text conversion interface. */ + +static struct textconv_interface text_conversion_interface = + { + android_reset_conversion, + android_set_point, + android_compose_region_changed, + android_notify_conversion, + }; + + + extern frame_parm_handler android_frame_parm_handlers[]; #endif /* !ANDROID_STUBIFY */ @@ -4327,6 +5357,11 @@ android_term_init (void) /* Set the baud rate to the same value it gets set to on X. */ baud_rate = 19200; + +#ifndef ANDROID_STUBIFY + sem_init (&edit_sem, false, 0); + register_textconv_interface (&text_conversion_interface); +#endif } diff --git a/src/coding.c b/src/coding.c index be5a0252ede..3ac4ada2939 100644 --- a/src/coding.c +++ b/src/coding.c @@ -11759,7 +11759,7 @@ syms_of_coding (void) DEFSYM (Qutf_8_unix, "utf-8-unix"); DEFSYM (Qutf_8_emacs, "utf-8-emacs"); -#if defined (WINDOWSNT) || defined (CYGWIN) +#if defined (WINDOWSNT) || defined (CYGWIN) || defined HAVE_ANDROID /* No, not utf-16-le: that one has a BOM. */ DEFSYM (Qutf_16le, "utf-16le"); #endif diff --git a/src/frame.c b/src/frame.c index a1d73d0799d..874e8c4cac1 100644 --- a/src/frame.c +++ b/src/frame.c @@ -997,6 +997,16 @@ make_frame (bool mini_p) f->select_mini_window_flag = false; /* This one should never be zero. */ f->change_stamp = 1; + +#ifdef HAVE_TEXT_CONVERSION + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + f->conversion.compose_region_overlay = Qnil; + f->conversion.batch_edit_count = 0; + f->conversion.batch_edit_flags = 0; + f->conversion.actions = NULL; +#endif + root_window = make_window (); rw = XWINDOW (root_window); if (mini_p) @@ -2264,6 +2274,13 @@ delete_frame (Lisp_Object frame, Lisp_Object force) f->terminal = 0; /* Now the frame is dead. */ unblock_input (); + /* Clear markers and overlays set by F on behalf of an input + method. */ +#ifdef HAVE_TEXT_CONVERSION + if (FRAME_WINDOW_P (f)) + reset_frame_state (f); +#endif + /* If needed, delete the terminal that this frame was on. (This must be done after the frame is killed.) */ terminal->reference_count--; diff --git a/src/frame.h b/src/frame.h index 4405c2df860..27484936ff1 100644 --- a/src/frame.h +++ b/src/frame.h @@ -76,6 +76,63 @@ enum ns_appearance_type #endif #endif /* HAVE_WINDOW_SYSTEM */ +#ifdef HAVE_TEXT_CONVERSION + +enum text_conversion_operation + { + TEXTCONV_START_BATCH_EDIT, + TEXTCONV_END_BATCH_EDIT, + TEXTCONV_COMMIT_TEXT, + TEXTCONV_FINISH_COMPOSING_TEXT, + TEXTCONV_SET_COMPOSING_TEXT, + TEXTCONV_SET_COMPOSING_REGION, + TEXTCONV_SET_POINT, + TEXTCONV_DELETE_SURROUNDING_TEXT, + }; + +/* Structure describing a single edit being performed by the input + method that should be executed in the context of + kbd_buffer_get_event. */ + +struct text_conversion_action +{ + /* The next text conversion action. */ + struct text_conversion_action *next; + + /* Any associated data. */ + Lisp_Object data; + + /* The operation being performed. */ + enum text_conversion_operation operation; + + /* Counter value. */ + unsigned long counter; +}; + +/* Structure describing the text conversion state associated with a + frame. */ + +struct text_conversion_state +{ + /* List of text conversion actions associated with this frame. */ + struct text_conversion_action *actions; + + /* Markers representing the composing region. */ + Lisp_Object compose_region_start, compose_region_end; + + /* Overlay representing the composing region. */ + Lisp_Object compose_region_overlay; + + /* The number of ongoing ``batch edits'' that are causing point + reporting to be delayed. */ + int batch_edit_count; + + /* Mask containing what must be updated after batch edits end. */ + int batch_edit_flags; +}; + +#endif + /* The structure representing a frame. */ struct frame @@ -664,6 +721,11 @@ struct frame enum ns_appearance_type ns_appearance; bool_bf ns_transparent_titlebar; #endif + +#ifdef HAVE_TEXT_CONVERSION + /* Text conversion state used by certain input methods. */ + struct text_conversion_state conversion; +#endif } GCALIGNED_STRUCT; /* Most code should use these functions to set Lisp fields in struct frame. */ diff --git a/src/keyboard.c b/src/keyboard.c index 11fa1e62c89..538fdffc199 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -44,6 +44,11 @@ along with GNU Emacs. If not, see . */ #include "atimer.h" #include "process.h" #include "menu.h" + +#ifdef HAVE_TEXT_CONVERSION +#include "textconv.h" +#endif + #include #ifdef HAVE_PTHREAD @@ -3020,6 +3025,10 @@ read_char (int commandflag, Lisp_Object map, { struct buffer *prev_buffer = current_buffer; last_input_event = c; + + /* All a `text-conversion' event does is prevent Emacs from + staying idle. It is not useful. */ + call4 (Qcommand_execute, tem, Qnil, Fvector (1, &last_input_event), Qt); if (CONSP (c) && !NILP (Fmemq (XCAR (c), Vwhile_no_input_ignore_events)) @@ -3582,6 +3591,11 @@ readable_events (int flags) return 1; #endif +#ifdef HAVE_TEXT_CONVERSION + if (detect_conversion_events ()) + return 1; +#endif + if (!(flags & READABLE_EVENTS_IGNORE_SQUEEZABLES) && some_mouse_moved ()) return 1; if (single_kboard) @@ -3914,6 +3928,11 @@ kbd_buffer_get_event (KBOARD **kbp, had_pending_selection_requests = false; #endif +#ifdef HAVE_TEXT_CONVERSION + bool had_pending_conversion_events; + + had_pending_conversion_events = false; +#endif #ifdef subprocesses if (kbd_on_hold_p () && kbd_buffer_nr_stored () < KBD_BUFFER_SIZE / 4) @@ -3977,6 +3996,13 @@ kbd_buffer_get_event (KBOARD **kbp, had_pending_selection_requests = true; break; } +#endif +#ifdef HAVE_TEXT_CONVERSION + if (detect_conversion_events ()) + { + had_pending_conversion_events = true; + break; + } #endif if (end_time) { @@ -4024,6 +4050,14 @@ kbd_buffer_get_event (KBOARD **kbp, x_handle_pending_selection_requests (); #endif +#ifdef HAVE_TEXT_CONVERSION + /* Handle pending ``text conversion'' requests from an input + method. */ + + if (had_pending_conversion_events) + handle_pending_conversion_events (); +#endif + if (CONSP (Vunread_command_events)) { Lisp_Object first; @@ -4380,12 +4414,25 @@ kbd_buffer_get_event (KBOARD **kbp, #ifdef HAVE_X_WINDOWS else if (had_pending_selection_requests) obj = Qnil; +#endif +#ifdef HAVE_TEXT_CONVERSION + /* This is an internal event used to prevent Emacs from becoming + idle immediately after a text conversion operation. */ + else if (had_pending_conversion_events) + obj = Qtext_conversion; #endif else /* We were promised by the above while loop that there was something for us to read! */ emacs_abort (); +#ifdef HAVE_TEXT_CONVERSION + /* While not implemented as keyboard commands, changes made by the + input method still mean that Emacs is no longer idle. */ + if (had_pending_conversion_events) + timer_stop_idle (); +#endif + input_pending = readable_events (0); Vlast_event_frame = internal_last_event_frame; @@ -12902,6 +12949,9 @@ See also `pre-command-hook'. */); DEFSYM (Qcoding, "coding"); DEFSYM (Qtouchscreen, "touchscreen"); +#ifdef HAVE_TEXT_CONVERSION + DEFSYM (Qtext_conversion, "text-conversion"); +#endif Fset (Qecho_area_clear_hook, Qnil); diff --git a/src/lisp.h b/src/lisp.h index 894b939b7b9..2dc51d32481 100644 --- a/src/lisp.h +++ b/src/lisp.h @@ -5230,7 +5230,10 @@ extern char *emacs_root_dir (void); #ifdef HAVE_TEXT_CONVERSION /* Defined in textconv.c. */ +extern void reset_frame_state (struct frame *); extern void report_selected_window_change (struct frame *); +extern void report_point_change (struct frame *, struct window *, + struct buffer *); #endif #ifdef HAVE_NATIVE_COMP diff --git a/src/process.c b/src/process.c index 9a8b0d7fd85..e7ccb2c604e 100644 --- a/src/process.c +++ b/src/process.c @@ -121,6 +121,7 @@ static struct rlimit nofile_limit; #ifdef HAVE_ANDROID #include "android.h" +#include "androidterm.h" #endif #ifdef HAVE_WINDOW_SYSTEM diff --git a/src/textconv.c b/src/textconv.c index d5db6d11717..b62ed7d1365 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -25,7 +25,10 @@ along with GNU Emacs. If not, see . */ ability to ``undo'' or ``edit'' previously composed text. This is most commonly seen in input methods for CJK laguages for X Windows, and is extensively used throughout Android by input methods for all - kinds of scripts. */ + kinds of scripts. + + In addition, these input methods may also need to make detailed + edits to the content of a buffer. That is also handled here. */ #include @@ -44,6 +47,15 @@ along with GNU Emacs. If not, see . */ static struct textconv_interface *text_interface; +/* Flags used to determine what must be sent after a batch edit + ends. */ + +enum textconv_batch_edit_flags + { + PENDING_POINT_CHANGE = 1, + PENDING_COMPOSE_CHANGE = 2, + }; + /* Copy the portion of the current buffer described by BEG, BEG_BYTE, @@ -94,14 +106,18 @@ copy_buffer (ptrdiff_t beg, ptrdiff_t beg_byte, Then, either delete that text from the buffer if QUERY->operation is TEXTCONV_SUBSTITUTION, or return 0. + If FLAGS & TEXTCONV_SKIP_CONVERSION_REGION, then first move PT past + the conversion region in the specified direction if it is inside. + Value is 0 if QUERY->operation was not TEXTCONV_SUBSTITUTION or if deleting the text was successful, and 1 otherwise. */ int -textconv_query (struct frame *f, struct textconv_callback_struct *query) +textconv_query (struct frame *f, struct textconv_callback_struct *query, + int flags) { specpdl_ref count; - ptrdiff_t pos, pos_byte, end, end_byte; + ptrdiff_t pos, pos_byte, end, end_byte, start; ptrdiff_t temp, temp1; char *buffer; @@ -113,14 +129,46 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query) /* Inhibit quitting. */ specbind (Qinhibit_quit, Qt); - /* Temporarily switch to F's selected window. */ - Fselect_window (f->selected_window, Qt); + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window ((WINDOW_LIVE_P (f->old_selected_window) + ? f->old_selected_window + : f->selected_window), Qt); /* Now find the appropriate text bounds for QUERY. First, move point QUERY->position steps forward or backwards. */ pos = PT; + /* Next, if POS lies within the conversion region and the caller + asked for it to be moved away, move it away from the conversion + region. */ + + if (flags & TEXTCONV_SKIP_CONVERSION_REGION + && MARKERP (f->conversion.compose_region_start)) + { + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + + if (pos >= start && pos < end) + { + switch (query->direction) + { + case TEXTCONV_FORWARD_CHAR: + case TEXTCONV_FORWARD_WORD: + case TEXTCONV_CARET_DOWN: + case TEXTCONV_NEXT_LINE: + case TEXTCONV_LINE_START: + pos = end; + break; + + default: + pos = max (BEGV, start - 1); + break; + } + } + } + /* If pos is outside the accessible part of the buffer or if it overflows, move back to point or to the extremes of the accessible region. */ @@ -287,6 +335,828 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query) return 0; } +/* Reset F's text conversion state. Delete any overlays or + markers inside. */ + +void +reset_frame_state (struct frame *f) +{ + struct text_conversion_action *last, *next; + + /* Make the composition region markers point elsewhere. */ + + if (!NILP (f->conversion.compose_region_start)) + { + Fset_marker (f->conversion.compose_region_start, Qnil, Qnil); + Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + } + + /* Delete the composition region overlay. */ + + if (!NILP (f->conversion.compose_region_overlay)) + Fdelete_overlay (f->conversion.compose_region_overlay); + + /* Delete each text conversion action queued up. */ + + next = f->conversion.actions; + while (next) + { + last = next; + next = next->next; + + /* Say that the conversion is finished. */ + if (text_interface && text_interface->notify_conversion) + text_interface->notify_conversion (last->counter); + + xfree (last); + } + f->conversion.actions = NULL; + + /* Clear batch edit state. */ + f->conversion.batch_edit_count = 0; + f->conversion.batch_edit_flags = 0; +} + +/* Return whether or not there are pending edits from an input method + on any frame. */ + +bool +detect_conversion_events (void) +{ + Lisp_Object tail, frame; + + FOR_EACH_FRAME (tail, frame) + { + if (XFRAME (frame)->conversion.actions) + return true; + } + + return false; +} + +/* Restore the selected window WINDOW. */ + +static void +restore_selected_window (Lisp_Object window) +{ + /* FIXME: not sure what to do if WINDOW has been deleted. */ + Fselect_window (window, Qt); +} + +/* Commit the given text in the composing region. If there is no + composing region, then insert the text after F's selected window's + last point instead. Finally, remove the composing region. */ + +static void +really_commit_text (struct frame *f, EMACS_INT position, + Lisp_Object text) +{ + specpdl_ref count; + ptrdiff_t wanted, start, end; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + /* Now detect whether or not there is a composing region. + If there is, then replace it with TEXT. Don't do that + otherwise. */ + + if (MARKERP (f->conversion.compose_region_start)) + { + /* Replace its contents. */ + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + safe_del_range (start, end); + Finsert (1, &text); + + /* Move to a the position specified in POSITION. */ + + if (position < 0) + { + wanted + = marker_position (f->conversion.compose_region_start); + + if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + || wanted < BEGV) + wanted = BEGV; + + if (wanted > ZV) + wanted = ZV; + + set_point (wanted); + } + else + { + wanted + = marker_position (f->conversion.compose_region_end); + + if (INT_ADD_WRAPV (wanted, position - 1, &wanted) + || wanted > ZV) + wanted = ZV; + + if (wanted < BEGV) + wanted = BEGV; + + set_point (wanted); + } + + /* Make the composition region markers point elsewhere. */ + + if (!NILP (f->conversion.compose_region_start)) + { + Fset_marker (f->conversion.compose_region_start, Qnil, Qnil); + Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + } + + /* Delete the composition region overlay. */ + + if (!NILP (f->conversion.compose_region_overlay)) + Fdelete_overlay (f->conversion.compose_region_overlay); + } + else + { + /* Otherwise, move the text and point to an appropriate + location. */ + wanted = PT; + Finsert (1, &text); + + if (position < 0) + { + if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + || wanted < BEGV) + wanted = BEGV; + + if (wanted > ZV) + wanted = ZV; + + set_point (wanted); + } + else + { + wanted = PT; + + if (INT_ADD_WRAPV (wanted, position - 1, &wanted) + || wanted > ZV) + wanted = ZV; + + if (wanted < BEGV) + wanted = BEGV; + + set_point (wanted); + } + } + + unbind_to (count, Qnil); +} + +/* Remove the composition region on the frame F, while leaving its + contents intact. */ + +static void +really_finish_composing_text (struct frame *f) +{ + if (!NILP (f->conversion.compose_region_start)) + { + Fset_marker (f->conversion.compose_region_start, Qnil, Qnil); + Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + } + + /* Delete the composition region overlay. */ + + if (!NILP (f->conversion.compose_region_overlay)) + Fdelete_overlay (f->conversion.compose_region_overlay); +} + +/* Set the composing text on F to TEXT. Then, move point to an + appropriate position relative to POSITION, and call + `compose_region_changed' in the text conversion interface should + point not have been changed relative to F's old selected window's + last point. */ + +static void +really_set_composing_text (struct frame *f, ptrdiff_t position, + Lisp_Object text) +{ + specpdl_ref count; + ptrdiff_t start, wanted, end; + struct window *w; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + w = XWINDOW (f->old_selected_window); + Fselect_window (f->old_selected_window, Qt); + + /* Now set up the composition region if necessary. */ + + if (!MARKERP (f->conversion.compose_region_start)) + { + f->conversion.compose_region_start = Fmake_marker (); + f->conversion.compose_region_end = Fmake_marker (); + Fset_marker (f->conversion.compose_region_start, + Fpoint (), Qnil); + Fset_marker (f->conversion.compose_region_end, + Fpoint (), Qnil); + Fset_marker_insertion_type (f->conversion.compose_region_end, + Qt); + } + else + { + /* Delete the text between the start of the composition region + and its end. TODO: avoid this widening. */ + record_unwind_protect (save_restriction_restore, + save_restriction_save ()); + Fwiden (); + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + safe_del_range (start, end); + set_point (start); + } + + /* Insert the new text. */ + Finsert (1, &text); + + /* Now move point to an appropriate location. */ + if (position < 0) + { + wanted = start; + + if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + || wanted < BEGV) + wanted = BEGV; + + if (wanted > ZV) + wanted = ZV; + } + else + { + end = marker_position (f->conversion.compose_region_end); + wanted = end; + + /* end should be PT after the edit. */ + eassert (end == PT); + + if (INT_ADD_WRAPV (wanted, position - 1, &wanted) + || wanted > ZV) + wanted = ZV; + + if (wanted < BEGV) + wanted = BEGV; + } + + set_point (wanted); + + /* If PT hasn't changed, the conversion region definitely has. + Otherwise, redisplay will update the input method instead. */ + + if (PT == w->last_point + && text_interface + && text_interface->compose_region_changed) + { + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_COMPOSE_CHANGE; + else + text_interface->compose_region_changed (f); + } + + unbind_to (count, Qnil); +} + +/* Set the composing region to START by END. Make it that it is not + already set. */ + +static void +really_set_composing_region (struct frame *f, ptrdiff_t start, + ptrdiff_t end) +{ + specpdl_ref count; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + /* If MAX (0, start) == end, then this should behave the same as + really_finish_composing_text. */ + + if (max (0, start) == max (0, end)) + { + really_finish_composing_text (f); + return; + } + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + /* Now set up the composition region if necessary. */ + + if (!MARKERP (f->conversion.compose_region_start)) + { + f->conversion.compose_region_start = Fmake_marker (); + f->conversion.compose_region_end = Fmake_marker (); + Fset_marker_insertion_type (f->conversion.compose_region_end, + Qt); + } + + Fset_marker (f->conversion.compose_region_start, + make_fixnum (start), Qnil); + Fset_marker (f->conversion.compose_region_end, + make_fixnum (end), Qnil); + + unbind_to (count, Qnil); +} + +/* Delete LEFT and RIGHT chars around point. */ + +static void +really_delete_surrounding_text (struct frame *f, ptrdiff_t left, + ptrdiff_t right) +{ + specpdl_ref count; + ptrdiff_t start, end; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + start = max (BEGV, PT - left); + end = min (ZV, PT + right); + + safe_del_range (start, end); + unbind_to (count, Qnil); +} + +/* Set point in F to POSITION. + + If it has not changed, signal an update through the text input + interface, which is necessary for the IME to acknowledge that the + change has completed. */ + +static void +really_set_point (struct frame *f, ptrdiff_t point) +{ + specpdl_ref count; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + if (point == PT) + { + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_POINT_CHANGE; + else + text_interface->point_changed (f, + XWINDOW (f->old_selected_window), + current_buffer); + } + else + /* Set the point. */ + Fgoto_char (make_fixnum (point)); + + unbind_to (count, Qnil); +} + +/* Complete the edit specified by the counter value inside *TOKEN. */ + +static void +complete_edit (void *token) +{ + if (text_interface && text_interface->notify_conversion) + text_interface->notify_conversion (*(unsigned long *) token); +} + +/* Process and free the text conversion ACTION. F must be the frame + on which ACTION will be performed. */ + +static void +handle_pending_conversion_events_1 (struct frame *f, + struct text_conversion_action *action) +{ + Lisp_Object data; + enum text_conversion_operation operation; + struct buffer *buffer; + struct window *w; + specpdl_ref count; + unsigned long token; + + /* Next, process this action and free it. */ + + data = action->data; + operation = action->operation; + token = action->counter; + xfree (action); + + /* Make sure completion is signalled. */ + count = SPECPDL_INDEX (); + record_unwind_protect_ptr (complete_edit, &token); + + switch (operation) + { + case TEXTCONV_START_BATCH_EDIT: + f->conversion.batch_edit_count++; + break; + + case TEXTCONV_END_BATCH_EDIT: + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_count--; + + if (!WINDOW_LIVE_P (f->old_selected_window)) + break; + + if (f->conversion.batch_edit_flags & PENDING_POINT_CHANGE) + { + w = XWINDOW (f->old_selected_window); + buffer = XBUFFER (WINDOW_BUFFER (w)); + + text_interface->point_changed (f, w, buffer); + } + + if (f->conversion.batch_edit_flags & PENDING_COMPOSE_CHANGE) + text_interface->compose_region_changed (f); + + f->conversion.batch_edit_flags = 0; + break; + + case TEXTCONV_COMMIT_TEXT: + really_commit_text (f, XFIXNUM (XCAR (data)), XCDR (data)); + break; + + case TEXTCONV_FINISH_COMPOSING_TEXT: + really_finish_composing_text (f); + break; + + case TEXTCONV_SET_COMPOSING_TEXT: + really_set_composing_text (f, XFIXNUM (XCAR (data)), + XCDR (data)); + break; + + case TEXTCONV_SET_COMPOSING_REGION: + really_set_composing_region (f, XFIXNUM (XCAR (data)), + XFIXNUM (XCDR (data))); + break; + + case TEXTCONV_SET_POINT: + really_set_point (f, XFIXNUM (data)); + break; + + case TEXTCONV_DELETE_SURROUNDING_TEXT: + really_delete_surrounding_text (f, XFIXNUM (XCAR (data)), + XFIXNUM (XCDR (data))); + break; + } + + unbind_to (count, Qnil); +} + +/* Process any outstanding text conversion events. + This may run Lisp or signal. */ + +void +handle_pending_conversion_events (void) +{ + struct frame *f; + Lisp_Object tail, frame; + struct text_conversion_action *action, *next; + bool handled; + + handled = false; + + FOR_EACH_FRAME (tail, frame) + { + f = XFRAME (frame); + + /* Test if F has any outstanding conversion events. Then + process them in bottom to up order. */ + for (action = f->conversion.actions; action; action = next) + { + /* Redisplay in between if there is more than one + action. */ + + if (handled) + redisplay (); + + /* Unlink this action. */ + next = action->next; + f->conversion.actions = next; + + /* Handle and free the action. */ + handle_pending_conversion_events_1 (f, action); + handled = true; + } + } +} + +/* Start a ``batch edit'' in F. During a batch edit, point_changed + will not be called until the batch edit ends. + + Process the actual operation in the event loop in keyboard.c; then, + call `notify_conversion' in the text conversion interface with + COUNTER. */ + +void +start_batch_edit (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_START_BATCH_EDIT; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* End a ``batch edit''. It is ok to call this function even if a + batch edit has not yet started, in which case it does nothing. + + COUNTER means the same as in `start_batch_edit'. */ + +void +end_batch_edit (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_END_BATCH_EDIT; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Insert the specified STRING into F's current buffer's composition + region, and set point to POSITION relative to STRING. + + COUNTER means the same as in `start_batch_edit'. */ + +void +commit_text (struct frame *f, Lisp_Object string, + ptrdiff_t position, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_COMMIT_TEXT; + action->data = Fcons (make_fixnum (position), string); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Remove the composition region and its overlay from F's current + buffer. Leave the text being composed intact. + + COUNTER means the same as in `start_batch_edit'. */ + +void +finish_composing_text (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_FINISH_COMPOSING_TEXT; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Insert the given STRING and make it the currently active + composition. + + If there is currently no composing region, then the new value of + point is used as the composing region. + + Then, the composing region is replaced with the text in the + specified string. + + Finally, move point to new_point, which is relative to either the + start or the end of OBJECT depending on whether or not it is less + than zero. + + COUNTER means the same as in `start_batch_edit'. */ + +void +set_composing_text (struct frame *f, Lisp_Object object, + ptrdiff_t new_point, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_SET_COMPOSING_TEXT; + action->data = Fcons (make_fixnum (new_point), + object); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Make the region between START and END the currently active + ``composing region''. + + The ``composing region'' is a region of text in the buffer that is + about to undergo editing by the input method. */ + +void +set_composing_region (struct frame *f, ptrdiff_t start, + ptrdiff_t end, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + if (start > MOST_POSITIVE_FIXNUM) + start = MOST_POSITIVE_FIXNUM; + + if (end > MOST_POSITIVE_FIXNUM) + end = MOST_POSITIVE_FIXNUM; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_SET_COMPOSING_REGION; + action->data = Fcons (make_fixnum (start), + make_fixnum (end)); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Move point in F's selected buffer to POINT. + + COUNTER means the same as in `start_batch_edit'. */ + +void +textconv_set_point (struct frame *f, ptrdiff_t point, + unsigned long counter) +{ + struct text_conversion_action *action, **last; + + if (point > MOST_POSITIVE_FIXNUM) + point = MOST_POSITIVE_FIXNUM; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_SET_POINT; + action->data = make_fixnum (point); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Delete LEFT and RIGHT characters around point in F's old selected + window. */ + +void +delete_surrounding_text (struct frame *f, ptrdiff_t left, + ptrdiff_t right, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_DELETE_SURROUNDING_TEXT; + action->data = Fcons (make_fixnum (left), + make_fixnum (right)); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Return N characters of text around point in F's old selected + window. + + Set *N to the actual number of characters returned, *START_RETURN + to the position of the first character returned, *OFFSET to the + offset of point within that text, *LENGTH to the actual number of + characters returned, and *BYTES to the actual number of bytes + returned. + + Value is NULL upon failure, and a malloced string upon success. */ + +char * +get_extracted_text (struct frame *f, ptrdiff_t n, + ptrdiff_t *start_return, + ptrdiff_t *offset, ptrdiff_t *length, + ptrdiff_t *bytes) +{ + specpdl_ref count; + ptrdiff_t start, end, start_byte, end_byte; + char *buffer; + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return NULL; + + /* Save the excursion, as there will be extensive changes to the + selected window. */ + count = SPECPDL_INDEX (); + record_unwind_protect_excursion (); + + /* Inhibit quitting. */ + specbind (Qinhibit_quit, Qt); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + /* Figure out the bounds of the text to return. */ + start = PT - n / 2; + end = PT + n - n / 2; + start = max (start, BEGV); + end = min (end, ZV); + buffer = NULL; + + /* Detect overflow. */ + + if (!(start <= PT <= end)) + goto finish; + + /* Convert the character positions to byte positions. */ + start_byte = CHAR_TO_BYTE (start); + end_byte = CHAR_TO_BYTE (end); + + /* Extract the text from the buffer. */ + buffer = xmalloc (end_byte - start_byte); + copy_buffer (start, start_byte, end, end_byte, + buffer); + + /* Return the offsets. */ + *start_return = start; + *offset = PT - start; + *length = end - start; + *bytes = end_byte - start_byte; + + finish: + unbind_to (count, Qnil); + return buffer; +} + /* Window system interface. These are called from the rest of @@ -298,16 +1168,40 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query) void report_selected_window_change (struct frame *f) { + reset_frame_state (f); + if (!text_interface) return; text_interface->reset (f); } +/* Notice that the point in F's selected window's current buffer has + changed. + + F is the frame whose selected window was changed, W is the window + in question, and BUFFER is that window's current buffer. + + Tell the text conversion interface about the change; it will likely + pass the information on to the system input method. */ + +void +report_point_change (struct frame *f, struct window *window, + struct buffer *buffer) +{ + if (!text_interface || !text_interface->point_changed) + return; + + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_POINT_CHANGE; + else + text_interface->point_changed (f, window, buffer); +} + /* Register INTERFACE as the text conversion interface. */ void -register_texconv_interface (struct textconv_interface *interface) +register_textconv_interface (struct textconv_interface *interface) { text_interface = interface; } diff --git a/src/textconv.h b/src/textconv.h index f6e7eb7925f..ce003847637 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -34,6 +34,20 @@ struct textconv_interface happen if the window is deleted or switches buffers, or an unexpected buffer change occurs.) */ void (*reset) (struct frame *); + + /* Notice that point has moved in the specified frame's selected + window's selected buffer. The second argument is the window + whose point changed, and the third argument is the buffer. */ + void (*point_changed) (struct frame *, struct window *, + struct buffer *); + + /* Notice that the preconversion region has changed without point + being moved. */ + void (*compose_region_changed) (struct frame *); + + /* Notice that an asynch conversion identified by COUNTER has + completed. */ + void (*notify_conversion) (unsigned long); }; @@ -103,7 +117,27 @@ struct textconv_callback_struct struct textconv_conversion_text text; }; -extern int textconv_query (struct frame *, struct textconv_callback_struct *); -extern void register_texconv_interface (struct textconv_interface *); +#define TEXTCONV_SKIP_CONVERSION_REGION (1 << 0) + +extern int textconv_query (struct frame *, struct textconv_callback_struct *, + int); +extern bool detect_conversion_events (void); +extern void handle_pending_conversion_events (void); +extern void start_batch_edit (struct frame *, unsigned long); +extern void end_batch_edit (struct frame *, unsigned long); +extern void commit_text (struct frame *, Lisp_Object, ptrdiff_t, + unsigned long); +extern void finish_composing_text (struct frame *, unsigned long); +extern void set_composing_text (struct frame *, Lisp_Object, + ptrdiff_t, unsigned long); +extern void set_composing_region (struct frame *, ptrdiff_t, ptrdiff_t, + unsigned long); +extern void textconv_set_point (struct frame *, ptrdiff_t, unsigned long); +extern void delete_surrounding_text (struct frame *, ptrdiff_t, + ptrdiff_t, unsigned long); +extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, + ptrdiff_t *, ptrdiff_t *, ptrdiff_t *); + +extern void register_textconv_interface (struct textconv_interface *); #endif /* _TEXTCONV_H_ */ diff --git a/src/xdisp.c b/src/xdisp.c index 8bbb80b1c8c..4b91fcf31ca 100644 --- a/src/xdisp.c +++ b/src/xdisp.c @@ -17267,6 +17267,9 @@ static void mark_window_display_accurate_1 (struct window *w, bool accurate_p) { struct buffer *b = XBUFFER (w->contents); +#ifdef HAVE_TEXT_CONVERSION + ptrdiff_t prev_point; +#endif w->last_modified = accurate_p ? BUF_MODIFF (b) : 0; w->last_overlay_modified = accurate_p ? BUF_OVERLAY_MODIFF (b) : 0; @@ -17296,11 +17299,36 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) w->last_cursor_vpos = w->cursor.vpos; w->last_cursor_off_p = w->cursor_off_p; +#ifdef HAVE_TEXT_CONVERSION + prev_point = w->last_point; +#endif + if (w == XWINDOW (selected_window)) w->last_point = BUF_PT (b); else w->last_point = marker_position (w->pointm); +#ifdef HAVE_TEXT_CONVERSION + /* Point motion is only propagated to the input method for use + in text conversion during a redisplay. While this can lead + to inconsistencies when point has moved but the change has + not yet been displayed, it leads to better results most of + the time, as point often changes within calls to + `save-excursion', and the only way to detect such calls is to + observe that the next redisplay never ends with those changes + applied. + + Changes to buffer text are immediately propagated to the + input method, and the position of point is also updated + during such a change, so the consequences are not that + severe. */ + + if (prev_point != w->last_point + && FRAME_WINDOW_P (WINDOW_XFRAME (w)) + && w == XWINDOW (WINDOW_XFRAME (w)->selected_window)) + report_point_change (WINDOW_XFRAME (w), w, b); +#endif + w->window_end_valid = true; w->update_mode_line = false; w->preserve_vscroll_p = false; diff --git a/src/xfns.c b/src/xfns.c index 9e004f6a678..0e4a25de04a 100644 --- a/src/xfns.c +++ b/src/xfns.c @@ -3861,7 +3861,7 @@ xic_string_conversion_callback (XIC ic, XPointer client_data, request.operation = TEXTCONV_RETRIEVAL; /* Now perform the string conversion. */ - rc = textconv_query (f, &request); + rc = textconv_query (f, &request, 0); if (rc) { diff --git a/src/xterm.c b/src/xterm.c index ccaacec2c57..49b88de2759 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -31615,7 +31615,7 @@ init_xterm (void) #endif #ifdef HAVE_X_I18N - register_texconv_interface (&text_conversion_interface); + register_textconv_interface (&text_conversion_interface); #endif } -- cgit v1.3 From 0998ab3ade7caf2b163cd99a44b8a3f17be763d5 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 19 Feb 2023 19:56:51 +0800 Subject: Report both sides of the region to the input method upon setup * java/org/gnu/emacs/EmacsNative.java (getSelection): Return array of ints. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Adjust accordingly. * src/androidterm.c (struct android_get_selection_context): New field `mark'. (android_get_selection): Set the mark field as appropriate. (getSelection): Adjust accordingly. --- java/org/gnu/emacs/EmacsNative.java | 2 +- java/org/gnu/emacs/EmacsView.java | 9 +++++---- src/androidterm.c | 38 +++++++++++++++++++++++++++++++------ 3 files changed, 38 insertions(+), 11 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index aaac9446510..ef1a0e75a5c 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -205,7 +205,7 @@ public class EmacsNative /* Return the current value of the selection, or -1 upon failure. */ - public static native int getSelection (short window); + public static native int[] getSelection (short window); static { diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 5ea8b0dcc0e..89f526853b2 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -555,7 +555,8 @@ public class EmacsView extends ViewGroup public InputConnection onCreateInputConnection (EditorInfo info) { - int selection, mode; + int mode; + int[] selection; /* Figure out what kind of IME behavior Emacs wants. */ mode = getICMode (); @@ -575,7 +576,7 @@ public class EmacsView extends ViewGroup /* If this fails or ANDROID_IC_MODE_NULL was requested, then don't initialize the input connection. */ - if (selection == -1 || mode == EmacsService.IC_MODE_NULL) + if (mode == EmacsService.IC_MODE_NULL || selection == null) { info.inputType = InputType.TYPE_NULL; return null; @@ -585,8 +586,8 @@ public class EmacsView extends ViewGroup info.imeOptions |= EditorInfo.IME_ACTION_DONE; /* Set the initial selection fields. */ - info.initialSelStart = selection; - info.initialSelEnd = selection; + info.initialSelStart = selection[0]; + info.initialSelEnd = selection[1]; /* Create the input connection if necessary. */ diff --git a/src/androidterm.c b/src/androidterm.c index 62a8d5d12b3..0701ef44436 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -4831,8 +4831,8 @@ struct android_get_selection_context android_window window; /* The position of the window's point when it was last - redisplayed. */ - ptrdiff_t point; + redisplayed, and its last mark if active. */ + ptrdiff_t point, mark; }; /* Function run on the main thread by `getSelection'. @@ -4844,6 +4844,7 @@ android_get_selection (void *data) struct android_get_selection_context *context; struct frame *f; struct window *w; + struct buffer *b; context = data; @@ -4860,25 +4861,50 @@ android_get_selection (void *data) rather important to keep the input method consistent with the contents of the display. */ context->point = w->ephemeral_last_point; + + /* Default context->mark to w->last_point too. */ + context->mark = context->point; + + /* If the mark is active, then set it properly. */ + b = XBUFFER (w->contents); + if (!NILP (BVAR (b, mark_active)) && w->last_mark != -1) + context->mark = w->last_mark; } } -JNIEXPORT jint JNICALL +JNIEXPORT jintArray JNICALL NATIVE_NAME (getSelection) (JNIEnv *env, jobject object, jshort window) { struct android_get_selection_context context; + jintArray array; + jint contents[2]; context.window = window; android_sync_edit (); if (android_run_in_emacs_thread (android_get_selection, &context)) - return -1; + return NULL; if (context.point == -1) - return -1; + return NULL; + + /* Wraparound actually makes more sense than truncation; at least + editing will sort of work. */ + contents[0] = (unsigned int) min (context.point, + context.mark); + contents[1] = (unsigned int) max (context.point, + context.mark); + + /* Now create the array. */ + array = (*env)->NewIntArray (env, 2); + + if (!array) + return NULL; - return min (context.point, TYPE_MAXIMUM (jint)); + /* Set its contents. */ + (*env)->SetIntArrayRegion (env, array, 0, 2, contents); + return array; } JNIEXPORT void JNICALL -- cgit v1.3 From 7aa4ffddd842e495d1ae388afff12075317ecb07 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 21 Feb 2023 15:28:06 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Startup): Document `content' special directory. * java/debug.sh (is_root): Improve /bin/tee detection. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function `dup'. * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity) (checkReadableOrCopy, onCreate): Create content directory names when the file is not readable. * java/org/gnu/emacs/EmacsService.java (EmacsService) (openContentUri, checkContentUri): New functions. * src/android.c (struct android_emacs_service): New methods. (android_content_name_p, android_get_content_name) (android_check_content_access): New function. (android_fstatat, android_open): Implement opening content URIs. (dup): Export to Java. (android_init_emacs_service): Initialize new methods. (android_faccessat): Implement content file names. --- doc/emacs/android.texi | 12 +- java/debug.sh | 2 +- java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsOpenActivity.java | 42 +++++- java/org/gnu/emacs/EmacsService.java | 119 +++++++++++++++++ src/android.c | 213 +++++++++++++++++++++++++++++- 6 files changed, 379 insertions(+), 13 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index d070199d325..f176d68ae67 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -129,9 +129,15 @@ file, it invokes @command{emacsclient} with the options and the name of the file being opened. Then, upon success, the focus is transferred to any open Emacs frame. -It is sadly impossible to open certain kinds of files which are -provided by a ``content provider''. When that is the case, a dialog -is displayed with an explanation of the error. +@cindex /content directory, android + Some files are given to Emacs as ``content identifiers'', which the +system provides access to outside the normal filesystem APIs. Emacs +internally supports a temporary @file{/content} directory which is +used to access those files. Do not make any assumptions about the +contents of this directory, or try to open files in it yourself. + + This feature is not provided on Android 4.3 and earlier, in which +case the file is copied to a temporary directory instead. @node Android File System @section What files Emacs can access under Android diff --git a/java/debug.sh b/java/debug.sh index 30e5a94eee5..f07bb98ed7d 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -281,7 +281,7 @@ else # Upload the specified gdbserver binary to the device. adb -s $device push "$gdbserver" "$gdbserver_bin" - if (adb -s $device pull /system/bin/tee /dev/null &> /dev/null); then + if adb -s $device shell ls /system/bin/tee; then # Copy it to the user directory. adb -s $device shell "$gdbserver_cat" adb -s $device shell "run-as $package chmod +x gdbserver" diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index ef1a0e75a5c..f0917a68120 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -31,6 +31,10 @@ public class EmacsNative initialization. */ private static final String[] libraryDeps; + + /* Like `dup' in C. */ + public static native int dup (int fd); + /* Obtain the fingerprint of this build of Emacs. The fingerprint can be used to determine the dump file name. */ public static native String getFingerprint (); diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index c8501d91025..87ce454a816 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -57,6 +57,8 @@ import android.os.Build; import android.os.Bundle; import android.os.ParcelFileDescriptor; +import android.util.Log; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -69,6 +71,8 @@ import java.io.UnsupportedEncodingException; public class EmacsOpenActivity extends Activity implements DialogInterface.OnClickListener { + private static final String TAG = "EmacsOpenActivity"; + private class EmacsClientThread extends Thread { private ProcessBuilder builder; @@ -178,12 +182,16 @@ public class EmacsOpenActivity extends Activity dialog.show (); } - /* Check that the specified FILE is readable. If it is not, then - copy the file in FD to a location in the system cache - directory and return the name of that file. */ + /* Check that the specified FILE is readable. If Android 4.4 or + later is being used, return URI formatted into a `/content/' file + name. + + If it is not, then copy the file in FD to a location in the + system cache directory and return the name of that file. */ private String - checkReadableOrCopy (String file, ParcelFileDescriptor fd) + checkReadableOrCopy (String file, ParcelFileDescriptor fd, + Uri uri) throws IOException, FileNotFoundException { File inFile; @@ -191,12 +199,34 @@ public class EmacsOpenActivity extends Activity InputStream stream; byte buffer[]; int read; + String content; + + Log.d (TAG, "checkReadableOrCopy: " + file); inFile = new File (file); - if (inFile.setReadable (true)) + if (inFile.canRead ()) return file; + Log.d (TAG, "checkReadableOrCopy: NO"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + { + content = "/content/" + uri.getEncodedAuthority (); + + for (String segment : uri.getPathSegments ()) + content += "/" + Uri.encode (segment); + + /* Append the URI query. */ + + if (uri.getEncodedQuery () != null) + content += "?" + uri.getEncodedQuery (); + + Log.d (TAG, "checkReadableOrCopy: " + content); + + return content; + } + /* inFile is now the file being written to. */ inFile = new File (getCacheDir (), inFile.getName ()); buffer = new byte[4098]; @@ -398,7 +428,7 @@ public class EmacsOpenActivity extends Activity if (names != null) fileName = new String (names, "UTF-8"); - fileName = checkReadableOrCopy (fileName, fd); + fileName = checkReadableOrCopy (fileName, fd, uri); } catch (FileNotFoundException exception) { diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index ba6ec485d62..c9701ff2990 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -19,7 +19,10 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.UnsupportedEncodingException; + import java.util.List; import java.util.ArrayList; @@ -41,22 +44,31 @@ import android.app.Service; import android.content.ClipboardManager; import android.content.Context; +import android.content.ContentResolver; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.database.Cursor; +import android.database.MatrixCursor; + + import android.net.Uri; import android.os.Build; import android.os.Looper; import android.os.IBinder; import android.os.Handler; +import android.os.ParcelFileDescriptor; import android.os.Vibrator; import android.os.VibratorManager; import android.os.VibrationEffect; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; + import android.util.Log; import android.util.DisplayMetrics; @@ -79,6 +91,7 @@ public class EmacsService extends Service private EmacsThread thread; private Handler handler; + private ContentResolver resolver; /* Keep this in synch with androidgui.h. */ public static final int IC_MODE_NULL = 0; @@ -193,6 +206,7 @@ public class EmacsService extends Service metrics = getResources ().getDisplayMetrics (); pixelDensityX = metrics.xdpi; pixelDensityY = metrics.ydpi; + resolver = getContentResolver (); try { @@ -643,4 +657,109 @@ public class EmacsService extends Service window.view.setICMode (icMode); window.view.imManager.restartInput (window.view); } + + /* Open a content URI described by the bytes BYTES, a non-terminated + string; make it writable if WRITABLE, and readable if READABLE. + Truncate the file if TRUNCATE. + + Value is the resulting file descriptor or -1 upon failure. */ + + public int + openContentUri (byte[] bytes, boolean writable, boolean readable, + boolean truncate) + { + String name, mode; + ParcelFileDescriptor fd; + int i; + + /* Figure out the file access mode. */ + + mode = ""; + + if (readable) + mode += "r"; + + if (writable) + mode += "w"; + + if (truncate) + mode += "t"; + + /* Try to open an associated ParcelFileDescriptor. */ + + try + { + /* The usual file name encoding question rears its ugly head + again. */ + name = new String (bytes, "UTF-8"); + Log.d (TAG, "openContentUri: " + Uri.parse (name)); + + fd = resolver.openFileDescriptor (Uri.parse (name), mode); + + /* Use detachFd on newer versions of Android or plain old + dup. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) + { + i = fd.detachFd (); + fd.close (); + + return i; + } + else + { + i = EmacsNative.dup (fd.getFd ()); + fd.close (); + + return i; + } + } + catch (Exception exception) + { + return -1; + } + } + + public boolean + checkContentUri (byte[] string, boolean readable, boolean writable) + { + String mode, name; + ParcelFileDescriptor fd; + + /* Decode this into a URI. */ + + try + { + /* The usual file name encoding question rears its ugly head + again. */ + name = new String (string, "UTF-8"); + Log.d (TAG, "checkContentUri: " + Uri.parse (name)); + } + catch (UnsupportedEncodingException exception) + { + name = null; + throw new RuntimeException (exception); + } + + mode = "r"; + + if (writable) + mode += "w"; + + try + { + fd = resolver.openFileDescriptor (Uri.parse (name), mode); + fd.close (); + + Log.d (TAG, "checkContentUri: YES"); + + return true; + } + catch (Exception exception) + { + Log.d (TAG, "checkContentUri: NO"); + Log.d (TAG, exception.toString ()); + return false; + } + } }; diff --git a/src/android.c b/src/android.c index fc5e6d278ed..75a97f9db33 100644 --- a/src/android.c +++ b/src/android.c @@ -108,6 +108,8 @@ struct android_emacs_service jmethodID restart_emacs; jmethodID update_ic; jmethodID reset_ic; + jmethodID open_content_uri; + jmethodID check_content_uri; }; struct android_emacs_pixmap @@ -941,6 +943,102 @@ android_get_asset_name (const char *filename) return NULL; } +/* Return whether or not the specified FILENAME actually resolves to a + content resolver URI. */ + +static bool +android_content_name_p (const char *filename) +{ + return (!strcmp (filename, "/content") + || !strncmp (filename, "/content/", + sizeof "/content/" - 1)); +} + +/* Return the content URI corresponding to a `/content' file name, + or NULL if it is not a content URI. + + This function is not reentrant. */ + +static const char * +android_get_content_name (const char *filename) +{ + static char buffer[PATH_MAX + 1]; + char *head, *token, *saveptr, *copy; + size_t n; + + n = PATH_MAX; + + /* First handle content ``URIs'' without a provider. */ + + if (!strcmp (filename, "/content") + || !strcmp (filename, "/content/")) + return "content://"; + + /* Next handle ordinary file names. */ + + if (strncmp (filename, "/content/", sizeof "/content/" - 1)) + return NULL; + + /* Forward past the first directory specifying the schema. */ + + copy = xstrdup (filename + sizeof "/content"); + token = saveptr = NULL; + head = stpcpy (buffer, "content:/"); + + /* Split FILENAME by slashes. */ + + while ((token = strtok_r (!token ? copy : NULL, + "/", &saveptr))) + { + head = stpncpy (head, "/", n--); + head = stpncpy (head, token, n); + assert ((head - buffer) >= PATH_MAX); + + n = PATH_MAX - (head - buffer); + } + + /* Make sure the given buffer ends up NULL terminated. */ + buffer[PATH_MAX] = '\0'; + xfree (copy); + + return buffer; +} + +/* Return whether or not the specified FILENAME is an accessible + content URI. MODE specifies what to check. */ + +static bool +android_check_content_access (const char *filename, int mode) +{ + const char *name; + jobject string; + size_t length; + jboolean rc; + + name = android_get_content_name (filename); + length = strlen (name); + + string = (*android_java_env)->NewByteArray (android_java_env, + length); + android_exception_check (); + + (*android_java_env)->SetByteArrayRegion (android_java_env, + string, 0, length, + (jbyte *) name); + rc = (*android_java_env)->CallBooleanMethod (android_java_env, + emacs_service, + service_class.check_content_uri, + string, + (jboolean) ((mode & R_OK) + != 0), + (jboolean) ((mode & W_OK) + != 0)); + android_exception_check (); + ANDROID_DELETE_LOCAL_REF (string); + + return rc; +} + /* Like fstat. However, look up the asset corresponding to the file descriptor. If it exists, return the right information. */ @@ -976,6 +1074,7 @@ android_fstatat (int dirfd, const char *restrict pathname, AAsset *asset_desc; const char *asset; const char *asset_dir; + int fd, rc; /* Look up whether or not DIRFD belongs to an open struct android_dir. */ @@ -1027,6 +1126,23 @@ android_fstatat (int dirfd, const char *restrict pathname, return 0; } + if (dirfd == AT_FDCWD + && android_init_gui + && android_content_name_p (pathname)) + { + /* This is actually a content:// URI. Open that file and call + stat on it. */ + + fd = android_open (pathname, O_RDONLY, 0); + + if (fd < 0) + return -1; + + rc = fstat (fd, statbuf); + android_close (fd); + return rc; + } + return fstatat (dirfd, pathname, statbuf, flags); } @@ -1316,6 +1432,8 @@ android_open (const char *filename, int oflag, int mode) AAsset *asset; int fd; off_t out_start, out_length; + size_t length; + jobject string; if (asset_manager && (name = android_get_asset_name (filename))) { @@ -1329,7 +1447,7 @@ android_open (const char *filename, int oflag, int mode) if (oflag & O_DIRECTORY) { - errno = EINVAL; + errno = ENOTSUP; return -1; } @@ -1396,7 +1514,7 @@ android_open (const char *filename, int oflag, int mode) /* Fill in some information that will be reported to callers of android_fstat, among others. */ android_table[fd].statb.st_mode - = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;; + = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; /* Owned by root. */ android_table[fd].statb.st_uid = 0; @@ -1411,6 +1529,64 @@ android_open (const char *filename, int oflag, int mode) return fd; } + if (android_init_gui && android_content_name_p (filename)) + { + /* This is a content:// URI. Ask the system for a descriptor to + that file. */ + + name = android_get_content_name (filename); + length = strlen (name); + + /* Check if the mode is valid. */ + + if (oflag & O_DIRECTORY) + { + errno = ENOTSUP; + return -1; + } + + /* Allocate a buffer to hold the file name. */ + string = (*android_java_env)->NewByteArray (android_java_env, + length); + if (!string) + { + (*android_java_env)->ExceptionClear (android_java_env); + errno = ENOMEM; + return -1; + } + (*android_java_env)->SetByteArrayRegion (android_java_env, + string, 0, length, + (jbyte *) name); + + /* Try to open the file descriptor. */ + + fd + = (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.open_content_uri, + string, + (jboolean) ((mode & O_WRONLY + || mode & O_RDWR) + != 0), + (jboolean) !(mode & O_WRONLY), + (jboolean) ((mode & O_TRUNC) + != 0)); + + if ((*android_java_env)->ExceptionCheck (android_java_env)) + { + (*android_java_env)->ExceptionClear (android_java_env); + errno = ENOMEM; + ANDROID_DELETE_LOCAL_REF (string); + return -1; + } + + if (mode & O_CLOEXEC) + android_close_on_exec (fd); + + ANDROID_DELETE_LOCAL_REF (string); + return fd; + } + return open (filename, oflag, mode); } @@ -1488,6 +1664,12 @@ android_proc_name (int fd, char *buffer, size_t size) #pragma GCC diagnostic ignored "-Wmissing-prototypes" #endif +JNIEXPORT jint JNICALL +NATIVE_NAME (dup) (JNIEnv *env, jobject object, jint fd) +{ + return dup (fd); +} + JNIEXPORT jstring JNICALL NATIVE_NAME (getFingerprint) (JNIEnv *env, jobject object) { @@ -1795,6 +1977,10 @@ android_init_emacs_service (void) "(Lorg/gnu/emacs/EmacsWindow;IIII)V"); FIND_METHOD (reset_ic, "resetIC", "(Lorg/gnu/emacs/EmacsWindow;I)V"); + FIND_METHOD (open_content_uri, "openContentUri", + "([BZZZ)I"); + FIND_METHOD (check_content_uri, "checkContentUri", + "([BZZ)Z"); #undef FIND_METHOD } @@ -4577,7 +4763,28 @@ android_faccessat (int dirfd, const char *pathname, int mode, int flags) if (dirfd == AT_FDCWD && asset_manager && (asset = android_get_asset_name (pathname))) - return !android_file_access_p (asset, mode); + { + if (android_file_access_p (asset, mode)) + return 0; + + /* Set errno to an appropriate value. */ + errno = ENOENT; + return 1; + } + + /* Check if pathname is actually a content resolver URI. */ + + if (dirfd == AT_FDCWD + && android_init_gui + && android_content_name_p (pathname)) + { + if (android_check_content_access (pathname, mode)) + return 0; + + /* Set errno to an appropriate value. */ + errno = ENOENT; + return 1; + } #if __ANDROID_API__ >= 16 return faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); -- cgit v1.3 From 15bcb446be2f2f5b85a1b9585ec3abaabcbf04d9 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 1 Mar 2023 12:00:46 +0800 Subject: Update Android port * java/Makefile.in (ETAGS, clean): New rules to generate tags. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): * java/org/gnu/emacs/EmacsApplication.java (EmacsApplication): * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu): * java/org/gnu/emacs/EmacsCopyArea.java (EmacsCopyArea): * java/org/gnu/emacs/EmacsDialog.java (EmacsDialog)::(dialog. Then): * java/org/gnu/emacs/EmacsDocumentsProvider.java (EmacsDocumentsProvider): * java/org/gnu/emacs/EmacsDrawLine.java (EmacsDrawLine): * java/org/gnu/emacs/EmacsDrawPoint.java (EmacsDrawPoint): * java/org/gnu/emacs/EmacsDrawRectangle.java (EmacsDrawRectangle): * java/org/gnu/emacs/EmacsFillPolygon.java (EmacsFillPolygon): * java/org/gnu/emacs/EmacsFillRectangle.java (EmacsFillRectangle): * java/org/gnu/emacs/EmacsGC.java (EmacsGC): * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsNoninteractive.java (EmacsNoninteractive): * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity): * java/org/gnu/emacs/EmacsPixmap.java (EmacsPixmap): * java/org/gnu/emacs/EmacsPreferencesActivity.java (EmacsPreferencesActivity): * java/org/gnu/emacs/EmacsSdk11Clipboard.java (EmacsSdk11Clipboard): * java/org/gnu/emacs/EmacsSdk23FontDriver.java (EmacsSdk23FontDriver): * java/org/gnu/emacs/EmacsSdk8Clipboard.java (EmacsSdk8Clipboard): * java/org/gnu/emacs/EmacsService.java (EmacsService): * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView) (buffers): * java/org/gnu/emacs/EmacsView.java (EmacsView, ViewGroup): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, drawables): * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager): Make classes final where appropriate. --- java/Makefile.in | 16 +++++++++++++++- java/org/gnu/emacs/EmacsActivity.java | 22 +++++++++++----------- java/org/gnu/emacs/EmacsApplication.java | 2 +- java/org/gnu/emacs/EmacsContextMenu.java | 2 +- java/org/gnu/emacs/EmacsCopyArea.java | 2 +- java/org/gnu/emacs/EmacsDialog.java | 2 +- java/org/gnu/emacs/EmacsDocumentsProvider.java | 2 +- java/org/gnu/emacs/EmacsDrawLine.java | 2 +- java/org/gnu/emacs/EmacsDrawPoint.java | 2 +- java/org/gnu/emacs/EmacsDrawRectangle.java | 2 +- java/org/gnu/emacs/EmacsFillPolygon.java | 2 +- java/org/gnu/emacs/EmacsFillRectangle.java | 2 +- java/org/gnu/emacs/EmacsGC.java | 2 +- java/org/gnu/emacs/EmacsInputConnection.java | 11 ++++++++++- java/org/gnu/emacs/EmacsNative.java | 2 +- java/org/gnu/emacs/EmacsNoninteractive.java | 2 +- java/org/gnu/emacs/EmacsOpenActivity.java | 2 +- java/org/gnu/emacs/EmacsPixmap.java | 2 +- java/org/gnu/emacs/EmacsPreferencesActivity.java | 2 +- java/org/gnu/emacs/EmacsSdk11Clipboard.java | 2 +- java/org/gnu/emacs/EmacsSdk23FontDriver.java | 2 +- java/org/gnu/emacs/EmacsSdk8Clipboard.java | 2 +- java/org/gnu/emacs/EmacsService.java | 2 +- java/org/gnu/emacs/EmacsSurfaceView.java | 2 +- java/org/gnu/emacs/EmacsView.java | 2 +- java/org/gnu/emacs/EmacsWindow.java | 2 +- .../gnu/emacs/EmacsWindowAttachmentManager.java | 2 +- 27 files changed, 60 insertions(+), 37 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/Makefile.in b/java/Makefile.in index 7ba05f6c9a3..bff021752c7 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -281,9 +281,23 @@ $(APK_NAME): classes.dex emacs.apk-in emacs.keystore $(AM_V_SILENT) $(APKSIGNER) $(SIGN_EMACS_V2) $@ $(AM_V_SILENT) rm -f $@.unaligned *.idsig +# TAGS generation. + +ETAGS = $(top_builddir)/lib-src/etags + +$(ETAGS): FORCE + $(MAKE) -C ../lib-src $(notdir $@) + +tagsfiles = $(JAVA_FILES) $(RESOURCE_FILE) + +.PHONY: tags FORCE +tags: TAGS +TAGS: $(ETAGS) $(tagsfiles) + $(AM_V_GEN) $(ETAGS) $(tagsfiles) + clean: rm -f *.apk emacs.apk-in *.dex *.unaligned *.class *.idsig - rm -rf install-temp $(RESOURCE_FILE) + rm -rf install-temp $(RESOURCE_FILE) TAGS find . -name '*.class' -delete maintainer-clean distclean bootstrap-clean: clean diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 7e09e608984..0ee8c239899 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -108,7 +108,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void detachWindow () { syncFullscreenWith (null); @@ -131,7 +131,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void attachWindow (EmacsWindow child) { Log.d (TAG, "attachWindow: " + child); @@ -161,21 +161,21 @@ public class EmacsActivity extends Activity } @Override - public void + public final void destroy () { finish (); } @Override - public EmacsWindow + public final EmacsWindow getAttachedWindow () { return window; } @Override - public void + public final void onCreate (Bundle savedInstanceState) { FrameLayout.LayoutParams params; @@ -238,7 +238,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onWindowFocusChanged (boolean isFocused) { Log.d (TAG, ("onWindowFocusChanged: " @@ -256,7 +256,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onPause () { isPaused = true; @@ -266,7 +266,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onResume () { isPaused = false; @@ -279,7 +279,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onContextMenuClosed (Menu menu) { Log.d (TAG, "onContextMenuClosed: " + menu); @@ -298,7 +298,7 @@ public class EmacsActivity extends Activity } @SuppressWarnings ("deprecation") - public void + public final void syncFullscreenWith (EmacsWindow emacsWindow) { WindowInsetsController controller; @@ -372,7 +372,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onAttachedToWindow () { super.onAttachedToWindow (); diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index 6a065165eb1..10099721744 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -27,7 +27,7 @@ import android.content.Context; import android.app.Application; import android.util.Log; -public class EmacsApplication extends Application +public final class EmacsApplication extends Application { private static final String TAG = "EmacsApplication"; diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 6b3ae0c6de9..0de292af21a 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -42,7 +42,7 @@ import android.widget.PopupMenu; Android menu, which can be turned into a popup (or other kind of) menu. */ -public class EmacsContextMenu +public final class EmacsContextMenu { private static final String TAG = "EmacsContextMenu"; diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 1daa2190542..f69b0cde866 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -27,7 +27,7 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsCopyArea +public final class EmacsCopyArea { private static Xfermode overAlu; diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 9f9124ce99c..80a5e5f7369 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -38,7 +38,7 @@ import android.view.ViewGroup; describes a single alert dialog. Then, `inflate' turns it into AlertDialog. */ -public class EmacsDialog implements DialogInterface.OnDismissListener +public final class EmacsDialog implements DialogInterface.OnDismissListener { private static final String TAG = "EmacsDialog"; diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index 3c3c7ead3c5..901c3b909e0 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -48,7 +48,7 @@ import java.io.IOException; This functionality is only available on Android 19 and later. */ -public class EmacsDocumentsProvider extends DocumentsProvider +public final class EmacsDocumentsProvider extends DocumentsProvider { /* Home directory. This is the directory whose contents are initially returned to requesting applications. */ diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index c6e5123bfca..0b23138a36c 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -29,7 +29,7 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsDrawLine +public final class EmacsDrawLine { public static void perform (EmacsDrawable drawable, EmacsGC gc, diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java index 3bc7be17961..de8ddf09cc4 100644 --- a/java/org/gnu/emacs/EmacsDrawPoint.java +++ b/java/org/gnu/emacs/EmacsDrawPoint.java @@ -19,7 +19,7 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -public class EmacsDrawPoint +public final class EmacsDrawPoint { public static void perform (EmacsDrawable drawable, diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index 695a8c6ea44..ce5e94e4a76 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -27,7 +27,7 @@ import android.graphics.RectF; import android.util.Log; -public class EmacsDrawRectangle +public final class EmacsDrawRectangle { public static void perform (EmacsDrawable drawable, EmacsGC gc, diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java index 22e2dd0d8a9..d55a0b3aca8 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -29,7 +29,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; -public class EmacsFillPolygon +public final class EmacsFillPolygon { public static void perform (EmacsDrawable drawable, EmacsGC gc, Point points[]) diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java index aed0a540c8f..4a0478b446f 100644 --- a/java/org/gnu/emacs/EmacsFillRectangle.java +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -26,7 +26,7 @@ import android.graphics.Rect; import android.util.Log; -public class EmacsFillRectangle +public final class EmacsFillRectangle { public static void perform (EmacsDrawable drawable, EmacsGC gc, diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java index bdc27a1ca5b..a7467cb9bd0 100644 --- a/java/org/gnu/emacs/EmacsGC.java +++ b/java/org/gnu/emacs/EmacsGC.java @@ -29,7 +29,7 @@ import android.graphics.Xfermode; /* X like graphics context structures. Keep the enums in synch with androidgui.h! */ -public class EmacsGC extends EmacsHandleObject +public final class EmacsGC extends EmacsHandleObject { public static final int GC_COPY = 0; public static final int GC_XOR = 1; diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 834c2226c82..ed64c368857 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -36,7 +36,7 @@ import android.util.Log; See EmacsEditable for more details. */ -public class EmacsInputConnection extends BaseInputConnection +public final class EmacsInputConnection extends BaseInputConnection { private static final String TAG = "EmacsInputConnection"; private EmacsView view; @@ -243,6 +243,15 @@ public class EmacsInputConnection extends BaseInputConnection return super.sendKeyEvent (key); } + @Override + public boolean + deleteSurroundingTextInCodePoints (int beforeLength, int afterLength) + { + /* This can be implemented the same way as + deleteSurroundingText. */ + return this.deleteSurroundingText (beforeLength, afterLength); + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index f0917a68120..b1205353090 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,7 +25,7 @@ import android.content.res.AssetManager; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; -public class EmacsNative +public final class EmacsNative { /* List of native libraries that must be loaded during class initialization. */ diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index 30901edb75f..f365037b311 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -44,7 +44,7 @@ import java.lang.reflect.Method; initializes Emacs. */ @SuppressWarnings ("unchecked") -public class EmacsNoninteractive +public final class EmacsNoninteractive { private static String getLibraryDirectory (Context context) diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index 87ce454a816..fddd5331d2f 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -68,7 +68,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; -public class EmacsOpenActivity extends Activity +public final class EmacsOpenActivity extends Activity implements DialogInterface.OnClickListener { private static final String TAG = "EmacsOpenActivity"; diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index a83d8f25542..eb011bc5e65 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -29,7 +29,7 @@ import android.os.Build; /* Drawable backed by bitmap. */ -public class EmacsPixmap extends EmacsHandleObject +public final class EmacsPixmap extends EmacsHandleObject implements EmacsDrawable { /* The depth of the bitmap. This is not actually used, just defined diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index 85639fe9236..70934fa4bd4 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -42,7 +42,7 @@ import android.preference.*; Unfortunately, there is no alternative that looks the same way. */ @SuppressWarnings ("deprecation") -public class EmacsPreferencesActivity extends PreferenceActivity +public final class EmacsPreferencesActivity extends PreferenceActivity { /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now, and tell the system to EmacsActivity with some parameters later. */ diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java index ea35a463299..a05184513cd 100644 --- a/java/org/gnu/emacs/EmacsSdk11Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java @@ -32,7 +32,7 @@ import java.io.UnsupportedEncodingException; /* This class implements EmacsClipboard for Android 3.0 and later systems. */ -public class EmacsSdk11Clipboard extends EmacsClipboard +public final class EmacsSdk11Clipboard extends EmacsClipboard implements ClipboardManager.OnPrimaryClipChangedListener { private static final String TAG = "EmacsSdk11Clipboard"; diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java index 11e128d5769..aaba8dbd166 100644 --- a/java/org/gnu/emacs/EmacsSdk23FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java @@ -22,7 +22,7 @@ package org.gnu.emacs; import android.graphics.Paint; import android.graphics.Rect; -public class EmacsSdk23FontDriver extends EmacsSdk7FontDriver +public final class EmacsSdk23FontDriver extends EmacsSdk7FontDriver { private void textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics, diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java index 34e66912562..818a722a908 100644 --- a/java/org/gnu/emacs/EmacsSdk8Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java @@ -31,7 +31,7 @@ import java.io.UnsupportedEncodingException; similarly old systems. */ @SuppressWarnings ("deprecation") -public class EmacsSdk8Clipboard extends EmacsClipboard +public final class EmacsSdk8Clipboard extends EmacsClipboard { private static final String TAG = "EmacsSdk8Clipboard"; private ClipboardManager manager; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 7f4f75b5147..e61d9487375 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -82,7 +82,7 @@ class Holder /* EmacsService is the service that starts the thread running Emacs and handles requests by that Emacs instance. */ -public class EmacsService extends Service +public final class EmacsService extends Service { public static final String TAG = "EmacsService"; public static final int MAX_PENDING_REQUESTS = 256; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 62e927094e4..e0411f7f8b3 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -35,7 +35,7 @@ import java.lang.ref.WeakReference; own back buffers, which use too much memory (up to 96 MB for a single frame.) */ -public class EmacsSurfaceView extends View +public final class EmacsSurfaceView extends View { private static final String TAG = "EmacsSurfaceView"; private EmacsView view; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 89f526853b2..d2330494bc7 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -51,7 +51,7 @@ import android.util.Log; It is also a ViewGroup, as it also lays out children. */ -public class EmacsView extends ViewGroup +public final class EmacsView extends ViewGroup { public static final String TAG = "EmacsView"; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 90fc4c44198..007a2a86e68 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -59,7 +59,7 @@ import android.os.Build; Views are also drawables, meaning they can accept drawing requests. */ -public class EmacsWindow extends EmacsHandleObject +public final class EmacsWindow extends EmacsHandleObject implements EmacsDrawable { private static final String TAG = "EmacsWindow"; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 510300571b8..1548bf28087 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -50,7 +50,7 @@ import android.util.Log; Finally, every time a window is removed, the consumer is destroyed. */ -public class EmacsWindowAttachmentManager +public final class EmacsWindowAttachmentManager { public static EmacsWindowAttachmentManager MANAGER; private final static String TAG = "EmacsWindowAttachmentManager"; -- cgit v1.3 From 1cae464859341bfd5fc7d7ed4b503ef959af81dd Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 5 Mar 2023 19:58:28 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsActivity.java (onCreate): * java/org/gnu/emacs/EmacsContextMenu.java: * java/org/gnu/emacs/EmacsDocumentsProvider.java (getMimeType): * java/org/gnu/emacs/EmacsDrawLine.java (perform): * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): * java/org/gnu/emacs/EmacsFillPolygon.java: * java/org/gnu/emacs/EmacsFontDriver.java: * java/org/gnu/emacs/EmacsHandleObject.java: * java/org/gnu/emacs/EmacsInputConnection.java: * java/org/gnu/emacs/EmacsMultitaskActivity.java (EmacsMultitaskActivity): * java/org/gnu/emacs/EmacsNative.java: * java/org/gnu/emacs/EmacsNoninteractive.java (EmacsNoninteractive, main): * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity) (startEmacsClient): * java/org/gnu/emacs/EmacsSdk7FontDriver.java: * java/org/gnu/emacs/EmacsSdk8Clipboard.java: * java/org/gnu/emacs/EmacsService.java (EmacsService, onCreate): * java/org/gnu/emacs/EmacsView.java (EmacsView, onLayout): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow): * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager): Remove redundant includes. Reorganize some functions around, remove duplicate `getLibDir' functions, and remove unused local variables. --- java/org/gnu/emacs/EmacsActivity.java | 13 ++++-- java/org/gnu/emacs/EmacsContextMenu.java | 3 -- java/org/gnu/emacs/EmacsDocumentsProvider.java | 4 +- java/org/gnu/emacs/EmacsDrawLine.java | 5 --- java/org/gnu/emacs/EmacsDrawRectangle.java | 1 - java/org/gnu/emacs/EmacsFillPolygon.java | 1 - java/org/gnu/emacs/EmacsFontDriver.java | 2 - java/org/gnu/emacs/EmacsHandleObject.java | 3 -- java/org/gnu/emacs/EmacsInputConnection.java | 9 +--- java/org/gnu/emacs/EmacsMultitaskActivity.java | 6 ++- java/org/gnu/emacs/EmacsNative.java | 2 - java/org/gnu/emacs/EmacsNoninteractive.java | 18 +------- java/org/gnu/emacs/EmacsOpenActivity.java | 20 +-------- java/org/gnu/emacs/EmacsSdk7FontDriver.java | 3 -- java/org/gnu/emacs/EmacsSdk8Clipboard.java | 4 +- java/org/gnu/emacs/EmacsService.java | 49 ++++++++-------------- java/org/gnu/emacs/EmacsView.java | 7 ++-- java/org/gnu/emacs/EmacsWindow.java | 15 +++---- .../gnu/emacs/EmacsWindowAttachmentManager.java | 5 +-- 19 files changed, 53 insertions(+), 117 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 13002d5d0e5..692d8a14e22 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -24,18 +24,22 @@ import java.util.List; import java.util.ArrayList; import android.app.Activity; + import android.content.Context; import android.content.Intent; + import android.os.Build; import android.os.Bundle; + import android.util.Log; + import android.view.Menu; import android.view.View; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController; -import android.widget.FrameLayout.LayoutParams; + import android.widget.FrameLayout; public class EmacsActivity extends Activity @@ -187,6 +191,7 @@ public class EmacsActivity extends Activity Intent intent; View decorView; ViewTreeObserver observer; + int matchParent; /* See if Emacs should be started with -Q. */ intent = getIntent (); @@ -194,8 +199,10 @@ public class EmacsActivity extends Activity = intent.getBooleanExtra ("org.gnu.emacs.START_DASH_Q", false); - params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); + matchParent = FrameLayout.LayoutParams.MATCH_PARENT; + params + = new FrameLayout.LayoutParams (matchParent, + matchParent); /* Make the frame layout. */ layout = new FrameLayout (this); diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index d1a624e68d9..dec5e148a8e 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -25,7 +25,6 @@ import java.util.ArrayList; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.os.Build; import android.view.Menu; @@ -35,8 +34,6 @@ import android.view.SubMenu; import android.util.Log; -import android.widget.PopupMenu; - /* Context menu implementation. This object is built from JNI and describes a menu hiearchy. Then, `inflate' can turn it into an Android menu, which can be turned into a popup (or other kind of) diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index 901c3b909e0..f70da6040ff 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -160,6 +160,7 @@ public final class EmacsDocumentsProvider extends DocumentsProvider { String name, extension, mime; int extensionSeparator; + MimeTypeMap singleton; if (file.isDirectory ()) return Document.MIME_TYPE_DIR; @@ -170,8 +171,9 @@ public final class EmacsDocumentsProvider extends DocumentsProvider if (extensionSeparator > 0) { + singleton = MimeTypeMap.getSingleton (); extension = name.substring (extensionSeparator + 1); - mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension (extension); + mime = singleton.getMimeTypeFromExtension (extension); if (mime != null) return mime; diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 0b23138a36c..92e03c48e26 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -21,13 +21,9 @@ package org.gnu.emacs; import java.lang.Math; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Xfermode; public final class EmacsDrawLine { @@ -38,7 +34,6 @@ public final class EmacsDrawLine Rect rect; Canvas canvas; Paint paint; - int i; /* TODO implement stippling. */ if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index ce5e94e4a76..3bd5779c54e 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -33,7 +33,6 @@ public final class EmacsDrawRectangle perform (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { - int i; Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java index d55a0b3aca8..ea8324c543c 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -21,7 +21,6 @@ package org.gnu.emacs; import java.lang.Math; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java index e142a3121d3..ff52899a897 100644 --- a/java/org/gnu/emacs/EmacsFontDriver.java +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -19,8 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.util.List; - import android.os.Build; /* This code is mostly unused. See sfntfont-android.c for the code diff --git a/java/org/gnu/emacs/EmacsHandleObject.java b/java/org/gnu/emacs/EmacsHandleObject.java index a57a3bbdfa9..5b889895337 100644 --- a/java/org/gnu/emacs/EmacsHandleObject.java +++ b/java/org/gnu/emacs/EmacsHandleObject.java @@ -19,9 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.util.List; -import java.util.ArrayList; -import java.lang.Object; import java.lang.IllegalStateException; /* This defines something that is a so-called ``handle''. Handles diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index ed64c368857..7b40fcff99e 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -23,18 +23,13 @@ import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.SurroundingText; import android.view.inputmethod.TextSnapshot; import android.view.KeyEvent; -import android.text.Editable; - import android.util.Log; -/* Android input methods, take number six. - - See EmacsEditable for more details. */ +/* Android input methods, take number six. See textconv.c for more + details; this is more-or-less a thin wrapper around that file. */ public final class EmacsInputConnection extends BaseInputConnection { diff --git a/java/org/gnu/emacs/EmacsMultitaskActivity.java b/java/org/gnu/emacs/EmacsMultitaskActivity.java index dbdc99a3559..b1c48f03fba 100644 --- a/java/org/gnu/emacs/EmacsMultitaskActivity.java +++ b/java/org/gnu/emacs/EmacsMultitaskActivity.java @@ -19,7 +19,11 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -public class EmacsMultitaskActivity extends EmacsActivity +/* This class only exists because EmacsActivity is already defined as + an activity, and the system wants a new class in order to define a + new activity. */ + +public final class EmacsMultitaskActivity extends EmacsActivity { } diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index b1205353090..38370d60727 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -19,8 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.lang.System; - import android.content.res.AssetManager; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index f365037b311..aaba74d877c 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -46,21 +46,6 @@ import java.lang.reflect.Method; @SuppressWarnings ("unchecked") public final class EmacsNoninteractive { - private static String - getLibraryDirectory (Context context) - { - int apiLevel; - - apiLevel = Build.VERSION.SDK_INT; - - if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) - return context.getApplicationInfo().nativeLibraryDir; - else if (apiLevel >= Build.VERSION_CODES.DONUT) - return context.getApplicationInfo().dataDir + "/lib"; - - return "/data/data/" + context.getPackageName() + "/lib"; - } - public static void main (String[] args) { @@ -188,7 +173,7 @@ public final class EmacsNoninteractive /* Now configure Emacs. The class path should already be set. */ filesDir = context.getFilesDir ().getCanonicalPath (); - libDir = getLibraryDirectory (context); + libDir = EmacsService.getLibraryDirectory (context); cacheDir = context.getCacheDir ().getCanonicalPath (); } catch (Exception e) @@ -198,6 +183,7 @@ public final class EmacsNoninteractive System.err.println ("and that Emacs needs adjustments in order to"); System.err.println ("obtain required system internal resources."); System.err.println ("Please report this bug to bug-gnu-emacs@gnu.org."); + e.printStackTrace (); System.exit (1); } diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index ac643ae8a13..51335ddb2dd 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -46,7 +46,6 @@ package org.gnu.emacs; import android.app.AlertDialog; import android.app.Activity; -import android.content.Context; import android.content.ContentResolver; import android.content.DialogInterface; import android.content.Intent; @@ -301,23 +300,6 @@ public final class EmacsOpenActivity extends Activity }); } - public String - getLibraryDirectory () - { - int apiLevel; - Context context; - - context = getApplicationContext (); - apiLevel = Build.VERSION.SDK_INT; - - if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) - return context.getApplicationInfo().nativeLibraryDir; - else if (apiLevel >= Build.VERSION_CODES.DONUT) - return context.getApplicationInfo().dataDir + "/lib"; - - return "/data/data/" + context.getPackageName() + "/lib"; - } - public void startEmacsClient (String fileName) { @@ -327,7 +309,7 @@ public final class EmacsOpenActivity extends Activity EmacsClientThread thread; File file; - libDir = getLibraryDirectory (); + libDir = EmacsService.getLibraryDirectory (this); builder = new ProcessBuilder (libDir + "/libemacsclient.so", fileName, "--reuse-frame", "--timeout=10", "--no-wait"); diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index ae91c299de8..6df102f18a2 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -20,7 +20,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import java.io.File; -import java.io.IOException; import java.util.LinkedList; import java.util.List; @@ -32,8 +31,6 @@ import android.graphics.Canvas; import android.util.Log; -import android.os.Build; - public class EmacsSdk7FontDriver extends EmacsFontDriver { private static final String TOFU_STRING = "\uDB3F\uDFFD"; diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java index 818a722a908..5a40128b0ac 100644 --- a/java/org/gnu/emacs/EmacsSdk8Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java @@ -19,7 +19,9 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -/* Importing the entire package avoids the deprecation warning. */ +/* Importing the entire package instead of just the legacy + ClipboardManager class avoids the deprecation warning. */ + import android.text.*; import android.content.Context; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index d9cb25f3e9c..d05ebce75dc 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -24,22 +24,15 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.List; -import java.util.ArrayList; -import android.graphics.Canvas; -import android.graphics.Bitmap; import android.graphics.Point; -import android.view.View; import android.view.InputDevice; import android.view.KeyEvent; -import android.annotation.TargetApi; - import android.app.Notification; import android.app.NotificationManager; import android.app.NotificationChannel; -import android.app.PendingIntent; import android.app.Service; import android.content.ClipboardManager; @@ -51,9 +44,6 @@ import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager; import android.content.res.AssetManager; -import android.database.Cursor; -import android.database.MatrixCursor; - import android.hardware.input.InputManager; import android.net.Uri; @@ -68,9 +58,6 @@ import android.os.Vibrator; import android.os.VibratorManager; import android.os.VibrationEffect; -import android.provider.DocumentsContract; -import android.provider.DocumentsContract.Document; - import android.util.Log; import android.util.DisplayMetrics; @@ -87,7 +74,6 @@ class Holder public final class EmacsService extends Service { public static final String TAG = "EmacsService"; - public static final int MAX_PENDING_REQUESTS = 256; public static volatile EmacsService SERVICE; public static boolean needDashQ; @@ -107,6 +93,22 @@ public final class EmacsService extends Service information. */ public static final boolean DEBUG_IC = false; + /* Return the directory leading to the directory in which native + library files are stored on behalf of CONTEXT. */ + + public static String + getLibraryDirectory (Context context) + { + int apiLevel; + + apiLevel = Build.VERSION.SDK_INT; + + if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) + return context.getApplicationInfo ().nativeLibraryDir; + + return context.getApplicationInfo ().dataDir + "/lib"; + } + @Override public int onStartCommand (Intent intent, int flags, int startId) @@ -178,23 +180,6 @@ public final class EmacsService extends Service } } - private String - getLibraryDirectory () - { - int apiLevel; - Context context; - - context = getApplicationContext (); - apiLevel = Build.VERSION.SDK_INT; - - if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) - return context.getApplicationInfo().nativeLibraryDir; - else if (apiLevel >= Build.VERSION_CODES.DONUT) - return context.getApplicationInfo().dataDir + "/lib"; - - return "/data/data/" + context.getPackageName() + "/lib"; - } - @Override public void onCreate () @@ -219,7 +204,7 @@ public final class EmacsService extends Service /* Configure Emacs with the asset manager and other necessary parameters. */ filesDir = app_context.getFilesDir ().getCanonicalPath (); - libDir = getLibraryDirectory (); + libDir = getLibraryDirectory (this); cacheDir = app_context.getCacheDir ().getCanonicalPath (); /* Now provide this application's apk file, so a recursive diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 617836d8811..aefc79c4fb7 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -20,7 +20,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import android.content.Context; -import android.content.res.ColorStateList; import android.text.InputType; @@ -116,6 +115,7 @@ public final class EmacsView extends ViewGroup super (EmacsService.SERVICE); Object tem; + Context context; this.window = window; this.damageRegion = new Region (); @@ -133,7 +133,8 @@ public final class EmacsView extends ViewGroup setDefaultFocusHighlightEnabled (false); /* Obtain the input method manager. */ - tem = getContext ().getSystemService (Context.INPUT_METHOD_SERVICE); + context = getContext (); + tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); imManager = (InputMethodManager) tem; } @@ -281,7 +282,6 @@ public final class EmacsView extends ViewGroup int count, i; View child; Rect windowRect; - int wantedWidth, wantedHeight; count = getChildCount (); @@ -422,7 +422,6 @@ public final class EmacsView extends ViewGroup } } - /* The following two functions must not be called if the view has no parent, or is parented to an activity. */ diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index ea4cf48090d..ffc7476f010 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -31,19 +31,16 @@ import android.content.Context; import android.graphics.Rect; import android.graphics.Canvas; import android.graphics.Bitmap; -import android.graphics.Point; import android.graphics.PixelFormat; import android.view.View; import android.view.ViewManager; -import android.view.ViewGroup; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.InputDevice; import android.view.WindowManager; -import android.content.Intent; import android.util.Log; import android.os.Build; @@ -87,7 +84,7 @@ public final class EmacsWindow extends EmacsHandleObject public EmacsWindow parent; /* List of all children in stacking order. This must be kept - consistent! */ + consistent with their Z order! */ public ArrayList children; /* Map between pointer identifiers and last known position. Used to @@ -105,9 +102,8 @@ public final class EmacsWindow extends EmacsHandleObject last button press or release event. */ public int lastButtonState, lastModifiers; - /* Whether or not the window is mapped, and whether or not it is - deiconified. */ - private boolean isMapped, isIconified; + /* Whether or not the window is mapped. */ + private boolean isMapped; /* Whether or not to ask for focus upon being mapped, and whether or not the window should be focusable. */ @@ -122,7 +118,8 @@ public final class EmacsWindow extends EmacsHandleObject private WindowManager windowManager; /* The time of the last KEYCODE_VOLUME_DOWN release. This is used - to quit Emacs. */ + to quit Emacs upon two rapid clicks of the volume down + button. */ private long lastVolumeButtonRelease; /* Linked list of character strings which were recently sent as @@ -1103,14 +1100,12 @@ public final class EmacsWindow extends EmacsHandleObject public void noticeIconified () { - isIconified = true; EmacsNative.sendIconified (this.handle); } public void noticeDeiconified () { - isIconified = false; EmacsNative.sendDeiconified (this.handle); } diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 1548bf28087..30f29250970 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -20,7 +20,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import android.content.Intent; @@ -74,8 +73,8 @@ public final class EmacsWindowAttachmentManager public EmacsWindowAttachmentManager () { - consumers = new LinkedList (); - windows = new LinkedList (); + consumers = new ArrayList (); + windows = new ArrayList (); } public void -- cgit v1.3 From c0a6f14f4a5069c28b7c90247546f1c5889a6d21 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 6 Mar 2023 15:30:29 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function requestSelectionUpdate. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Call it instead of losing if getting the current selection fails. * src/android-asset.h (AAsset_seek): Define stub. * src/android.c (android_open): Take mode_t. (android_open_asset, android_close_asset, android_asset_read_quit) (android_asset_read, android_asset_lseek, android_asset_fstat): New functions. * src/android.h (struct android_fd_or_asset): Update prototypes. * src/androidgui.h (enum android_ime_operation): Add new operation to update the selection position. * src/androidterm.c (android_handle_ime_event): Handle new operation. (requestSelectionUpdate): New function. * src/fileio.c (close_file_unwind_emacs_fd): New function. (Fcopy_file, union read_non_regular, read_non_regular) (Finsert_file_contents): Use optimized codepath to insert Android asset files. * src/frame.h (enum text_conversion_operation): New operation. * src/textconv.c (really_request_point_update) (handle_pending_conversion_events_1, request_point_update): New functions. * src/textconv.h: Update prototypes. --- java/org/gnu/emacs/EmacsNative.java | 1 + java/org/gnu/emacs/EmacsView.java | 29 ++++-- src/android-asset.h | 8 ++ src/android.c | 150 ++++++++++++++++++++++++++++++- src/android.h | 26 +++++- src/androidgui.h | 1 + src/androidterm.c | 26 ++++++ src/fileio.c | 174 ++++++++++++++++++++++++------------ src/frame.h | 1 + src/textconv.c | 43 +++++++++ src/textconv.h | 1 + 11 files changed, 392 insertions(+), 68 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 38370d60727..8e626b9534b 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -203,6 +203,7 @@ public final class EmacsNative public static native ExtractedText getExtractedText (short window, ExtractedTextRequest req, int flags); + public static native void requestSelectionUpdate (short window); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index f751eaaa3e4..90a2c912a5a 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -573,20 +573,35 @@ public final class EmacsView extends ViewGroup /* Set a reasonable inputType. */ info.inputType = InputType.TYPE_CLASS_TEXT; - /* Obtain the current position of point and set it as the - selection. */ - selection = EmacsNative.getSelection (window.handle); - - Log.d (TAG, "onCreateInputConnection: current selection is: " + selection); - /* If this fails or ANDROID_IC_MODE_NULL was requested, then don't initialize the input connection. */ - if (mode == EmacsService.IC_MODE_NULL || selection == null) + + if (mode == EmacsService.IC_MODE_NULL) { info.inputType = InputType.TYPE_NULL; return null; } + /* Obtain the current position of point and set it as the + selection. */ + selection = EmacsNative.getSelection (window.handle); + + if (selection != null) + Log.d (TAG, "onCreateInputConnection: current selection is: " + + selection[0] + ", by " + selection[1]); + else + { + Log.d (TAG, "onCreateInputConnection: current selection could" + + " not be retrieved."); + + /* If the selection could not be obtained, return 0 by 0. + However, ask for the selection position to be updated as + soon as possible. */ + + selection = new int[] { 0, 0, }; + EmacsNative.requestSelectionUpdate (window.handle); + } + if (mode == EmacsService.IC_MODE_ACTION) info.imeOptions |= EditorInfo.IME_ACTION_DONE; diff --git a/src/android-asset.h b/src/android-asset.h index 3b2f8105865..4fb309f1645 100644 --- a/src/android-asset.h +++ b/src/android-asset.h @@ -412,3 +412,11 @@ AAsset_read (AAsset *asset, void *buffer, size_t size) return android_asset_read_internal (asset, MIN (size, INT_MAX), buffer); } + +static off_t +AAsset_seek (AAsset *asset, off_t offset, int whence) +{ + /* Java InputStreams don't support seeking at all. */ + errno = ESPIPE; + return -1; +} diff --git a/src/android.c b/src/android.c index 9fc4143602c..f49249de09f 100644 --- a/src/android.c +++ b/src/android.c @@ -1568,7 +1568,7 @@ android_close_on_exec (int fd) contain all assets in the application package. */ int -android_open (const char *filename, int oflag, int mode) +android_open (const char *filename, int oflag, mode_t mode) { const char *name; AAsset *asset; @@ -5971,6 +5971,154 @@ android_set_fullscreen (android_window window, bool fullscreen) +/* External asset management interface. By using functions here + to read and write from files, Emacs can avoid opening a + shared memory file descriptor for each ``asset'' file. */ + +/* Like android_open. However, return a structure that can + either directly hold an AAsset or a file descriptor. + + Value is the structure upon success. Upon failure, value + consists of an uninitialized file descriptor, but its asset + field is set to -1, and errno is set accordingly. */ + +struct android_fd_or_asset +android_open_asset (const char *filename, int oflag, mode_t mode) +{ + const char *name; + struct android_fd_or_asset fd; + AAsset *asset; + + /* Initialize FD by setting its asset to an invalid + pointer. */ + fd.asset = (void *) -1; + + /* See if this is an asset. */ + + if (asset_manager && (name = android_get_asset_name (filename))) + { + /* Return failure for unsupported flags. */ + + if (oflag & O_WRONLY || oflag & O_RDWR) + { + errno = EROFS; + return fd; + } + + if (oflag & O_DIRECTORY) + { + errno = ENOTSUP; + return fd; + } + + /* Now try to open the asset. */ + asset = AAssetManager_open (asset_manager, name, + AASSET_MODE_STREAMING); + + if (!asset) + { + errno = ENOENT; + return fd; + } + + /* Return the asset. */ + fd.asset = asset; + return fd; + } + + /* If the file is not an asset, fall back to android_open and + get a regular file descriptor. */ + + fd.fd = android_open (filename, oflag, mode); + if (fd.fd < 1) + return fd; + + /* Set fd.asset to NULL, signifying that it is a file + descriptor. */ + fd.asset = NULL; + return fd; +} + +/* Like android_close. However, it takes a ``file descriptor'' + opened using android_open_asset. */ + +int +android_close_asset (struct android_fd_or_asset asset) +{ + if (!asset.asset) + return android_close (asset.fd); + + AAsset_close (asset.asset); + return 0; +} + +/* Like `emacs_read_quit'. However, it handles file descriptors + opened using `android_open_asset' as well. */ + +ssize_t +android_asset_read_quit (struct android_fd_or_asset asset, + void *buffer, size_t size) +{ + if (!asset.asset) + return emacs_read_quit (asset.fd, buffer, size); + + /* It doesn't seem possible to quit from inside AAsset_read, + sadly. */ + return AAsset_read (asset.asset, buffer, size); +} + +/* Like `read'. However, it handles file descriptors opened + using `android_open_asset' as well. */ + +ssize_t +android_asset_read (struct android_fd_or_asset asset, + void *buffer, size_t size) +{ + if (!asset.asset) + return read (asset.fd, buffer, size); + + /* It doesn't seem possible to quit from inside AAsset_read, + sadly. */ + return AAsset_read (asset.asset, buffer, size); +} + +/* Like `lseek', but it handles ``file descriptors'' opened with + android_open_asset. */ + +off_t +android_asset_lseek (struct android_fd_or_asset asset, off_t off, + int whence) +{ + if (!asset.asset) + return lseek (asset.fd, off, whence); + + return AAsset_seek (asset.asset, off, whence); +} + +/* Like `fstat'. */ + +int +android_asset_fstat (struct android_fd_or_asset asset, + struct stat *statb) +{ + if (!asset.asset) + return fstat (asset.fd, statb); + + /* Clear statb. */ + memset (statb, 0, sizeof *statb); + + /* Set the mode. */ + statb->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + + /* Owned by root. */ + statb->st_uid = 0; + statb->st_gid = 0; + + /* Size of the file. */ + statb->st_size = AAsset_getLength (asset.asset); + return 0; +} + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index 95206b77979..ed0089ad94e 100644 --- a/src/android.h +++ b/src/android.h @@ -46,7 +46,7 @@ extern int android_emacs_init (int, char **, char *); extern int android_select (int, fd_set *, fd_set *, fd_set *, struct timespec *); -extern int android_open (const char *, int, int); +extern int android_open (const char *, int, mode_t); extern char *android_user_full_name (struct passwd *); extern int android_fstat (int, struct stat *); extern int android_fstatat (int, const char *restrict, @@ -107,6 +107,30 @@ extern void android_closedir (struct android_dir *); +/* External asset manager interface. */ + +struct android_fd_or_asset +{ + /* The file descriptor. */ + int fd; + + /* The asset. If set, FD is not a real file descriptor. */ + void *asset; +}; + +extern struct android_fd_or_asset android_open_asset (const char *, + int, mode_t); +extern int android_close_asset (struct android_fd_or_asset); +extern ssize_t android_asset_read_quit (struct android_fd_or_asset, + void *, size_t); +extern ssize_t android_asset_read (struct android_fd_or_asset, + void *, size_t); +extern off_t android_asset_lseek (struct android_fd_or_asset, off_t, int); +extern int android_asset_fstat (struct android_fd_or_asset, + struct stat *); + + + /* Very miscellaneous functions. */ struct android_battery_state diff --git a/src/androidgui.h b/src/androidgui.h index 6514c78d289..e1c80a71a59 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -430,6 +430,7 @@ enum android_ime_operation ANDROID_IME_SET_POINT, ANDROID_IME_START_BATCH_EDIT, ANDROID_IME_END_BATCH_EDIT, + ANDROID_IME_REQUEST_SELECTION_UPDATE, }; struct android_ime_event diff --git a/src/androidterm.c b/src/androidterm.c index a6709ac8169..0cc2b35099c 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -630,6 +630,10 @@ android_handle_ime_event (union android_event *event, struct frame *f) case ANDROID_IME_END_BATCH_EDIT: end_batch_edit (f, event->ime.counter); break; + + case ANDROID_IME_REQUEST_SELECTION_UPDATE: + request_point_update (f, event->ime.counter); + break; } } @@ -5169,6 +5173,28 @@ NATIVE_NAME (getSelectedText) (JNIEnv *env, jobject object, return string; } +JNIEXPORT void JNICALL +NATIVE_NAME (requestSelectionUpdate) (JNIEnv *env, jobject object, + jshort window) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_REQUEST_SELECTION_UPDATE; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + #ifdef __clang__ #pragma clang diagnostic pop #else diff --git a/src/fileio.c b/src/fileio.c index 7bc9800aaf8..ae244b8f85a 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -113,6 +113,37 @@ along with GNU Emacs. If not, see . */ #include "commands.h" +#if !defined HAVE_ANDROID || defined ANDROID_STUBIFY + +/* Type describing a file descriptor used by functions such as + `insert-file-contents'. */ + +typedef int emacs_fd; + +/* Function used to read and open from such a file descriptor. */ + +#define emacs_fd_open emacs_open +#define emacs_fd_close emacs_close +#define emacs_fd_read emacs_read_quit +#define emacs_fd_lseek lseek +#define emacs_fd_fstat sys_fstat +#define emacs_fd_valid_p(fd) ((fd) >= 0) +#define emacs_fd_to_int(fds) (fds) + +#else /* HAVE_ANDROID && !defined ANDROID_STUBIFY */ + +typedef struct android_fd_or_asset emacs_fd; + +#define emacs_fd_open android_open_asset +#define emacs_fd_close android_close_asset +#define emacs_fd_read android_asset_read_quit +#define emacs_fd_lseek android_asset_lseek +#define emacs_fd_fstat android_asset_fstat +#define emacs_fd_valid_p(fd) ((fd).asset != ((void *) -1)) +#define emacs_fd_to_int(fds) ((fds).asset ? -1 : (fds).fd) + +#endif /* !defined HAVE_ANDROID || defined ANDROID_STUBIFY */ + /* True during writing of auto-save files. */ static bool auto_saving; @@ -299,6 +330,15 @@ close_file_unwind (int fd) emacs_close (fd); } +static void +close_file_unwind_emacs_fd (void *ptr) +{ + emacs_fd *fd; + + fd = ptr; + emacs_fd_close (*fd); +} + void fclose_unwind (void *arg) { @@ -2221,7 +2261,8 @@ permissions. */) #else bool already_exists = false; mode_t new_mask; - int ifd, ofd; + emacs_fd ifd; + int ofd; struct stat st; #endif @@ -2265,22 +2306,24 @@ permissions. */) report_file_error ("Copying permissions to", newname); } #else /* not WINDOWSNT */ - ifd = emacs_open (SSDATA (encoded_file), O_RDONLY | O_NONBLOCK, 0); + ifd = emacs_fd_open (SSDATA (encoded_file), O_RDONLY | O_NONBLOCK, 0); - if (ifd < 0) + if (!emacs_fd_valid_p (ifd)) report_file_error ("Opening input file", file); - record_unwind_protect_int (close_file_unwind, ifd); + record_unwind_protect_ptr (close_file_unwind_emacs_fd, &ifd); - if (sys_fstat (ifd, &st) != 0) + if (emacs_fd_fstat (ifd, &st) != 0) report_file_error ("Input file status", file); if (!NILP (preserve_permissions)) { #if HAVE_LIBSELINUX - if (is_selinux_enabled ()) + if (is_selinux_enabled () + && emacs_fd_to_int (ifd) != -1) { - conlength = fgetfilecon (ifd, &con); + conlength = fgetfilecon (emacs_fd_to_int (ifd), + &con); if (conlength == -1) report_file_error ("Doing fgetfilecon", file); } @@ -2329,7 +2372,8 @@ permissions. */) maybe_quit (); - if (clone_file (ofd, ifd)) + if (emacs_fd_to_int (ifd) != -1 + && clone_file (ofd, emacs_fd_to_int (ifd))) newsize = st.st_size; else { @@ -2337,30 +2381,38 @@ permissions. */) ssize_t copied; #ifndef MSDOS - for (newsize = 0; newsize < insize; newsize += copied) + newsize = 0; + + if (emacs_fd_to_int (ifd) != -1) { - /* Copy at most COPY_MAX bytes at a time; this is min - (PTRDIFF_MAX, SIZE_MAX) truncated to a value that is - surely aligned well. */ - ssize_t ssize_max = TYPE_MAXIMUM (ssize_t); - ptrdiff_t copy_max = min (ssize_max, SIZE_MAX) >> 30 << 30; - off_t intail = insize - newsize; - ptrdiff_t len = min (intail, copy_max); - copied = copy_file_range (ifd, NULL, ofd, NULL, len, 0); - if (copied <= 0) - break; - maybe_quit (); + for (; newsize < insize; newsize += copied) + { + /* Copy at most COPY_MAX bytes at a time; this is min + (PTRDIFF_MAX, SIZE_MAX) truncated to a value that is + surely aligned well. */ + ssize_t ssize_max = TYPE_MAXIMUM (ssize_t); + ptrdiff_t copy_max = min (ssize_max, SIZE_MAX) >> 30 << 30; + off_t intail = insize - newsize; + ptrdiff_t len = min (intail, copy_max); + copied = copy_file_range (emacs_fd_to_int (ifd), NULL, + ofd, NULL, len, 0); + if (copied <= 0) + break; + maybe_quit (); + } } #endif /* MSDOS */ /* Fall back on read+write if copy_file_range failed, or if the - input is empty and so could be a /proc file. read+write will - either succeed, or report an error more precisely than - copy_file_range would. */ + input is empty and so could be a /proc file, or if ifd is an + invention of android.c. read+write will either succeed, or + report an error more precisely than copy_file_range + would. */ if (newsize != insize || insize == 0) { char buf[MAX_ALLOCA]; - for (; (copied = emacs_read_quit (ifd, buf, sizeof buf)); + + for (; (copied = emacs_fd_read (ifd, buf, sizeof buf)); newsize += copied) { if (copied < 0) @@ -2408,8 +2460,10 @@ permissions. */) } } - switch (!NILP (preserve_permissions) - ? qcopy_acl (SSDATA (encoded_file), ifd, + switch ((!NILP (preserve_permissions) + && emacs_fd_to_int (ifd) != -1) + ? qcopy_acl (SSDATA (encoded_file), + emacs_fd_to_int (ifd), SSDATA (encoded_newname), ofd, preserved_permissions) : (already_exists @@ -2449,7 +2503,9 @@ permissions. */) if (emacs_close (ofd) < 0) report_file_error ("Write error", newname); - emacs_close (ifd); + /* Note that ifd is not closed twice because unwind_protects are + discarded at the end of this function. */ + emacs_fd_close (ifd); #ifdef MSDOS /* In DJGPP v2.0 and later, fstat usually returns true file mode bits, @@ -3826,7 +3882,7 @@ union read_non_regular { struct { - int fd; + emacs_fd fd; ptrdiff_t inserted, trytry; } s; GCALIGNED_UNION_MEMBER @@ -3837,10 +3893,10 @@ static Lisp_Object read_non_regular (Lisp_Object state) { union read_non_regular *data = XFIXNUMPTR (state); - int nbytes = emacs_read_quit (data->s.fd, - ((char *) BEG_ADDR + PT_BYTE - BEG_BYTE - + data->s.inserted), - data->s.trytry); + int nbytes = emacs_fd_read (data->s.fd, + ((char *) BEG_ADDR + PT_BYTE - BEG_BYTE + + data->s.inserted), + data->s.trytry); return make_fixnum (nbytes); } @@ -3989,7 +4045,7 @@ by calling `format-decode', which see. */) { struct stat st; struct timespec mtime; - int fd; + emacs_fd fd; ptrdiff_t inserted = 0; int unprocessed; specpdl_ref count = SPECPDL_INDEX (); @@ -4067,8 +4123,8 @@ by calling `format-decode', which see. */) orig_filename = filename; filename = ENCODE_FILE (filename); - fd = emacs_open (SSDATA (filename), O_RDONLY, 0); - if (fd < 0) + fd = emacs_fd_open (SSDATA (filename), O_RDONLY, 0); + if (!emacs_fd_valid_p (fd)) { save_errno = errno; if (NILP (visit)) @@ -4086,7 +4142,7 @@ by calling `format-decode', which see. */) } specpdl_ref fd_index = SPECPDL_INDEX (); - record_unwind_protect_int (close_file_unwind, fd); + record_unwind_protect_ptr (close_file_unwind_emacs_fd, &fd); /* Replacement should preserve point as it preserves markers. */ if (!NILP (replace)) @@ -4096,7 +4152,7 @@ by calling `format-decode', which see. */) XCAR (XCAR (window_markers))); } - if (sys_fstat (fd, &st) != 0) + if (emacs_fd_fstat (fd, &st) != 0) report_file_error ("Input file status", orig_filename); mtime = get_stat_mtime (&st); @@ -4117,7 +4173,7 @@ by calling `format-decode', which see. */) xsignal2 (Qfile_error, build_string ("not a regular file"), orig_filename); - seekable = lseek (fd, 0, SEEK_CUR) < 0; + seekable = emacs_fd_lseek (fd, 0, SEEK_CUR) < 0; if (!NILP (beg) && !seekable) xsignal2 (Qfile_error, build_string ("cannot use a start position in a non-seekable file/device"), @@ -4196,17 +4252,17 @@ by calling `format-decode', which see. */) int nread; if (st.st_size <= (1024 * 4)) - nread = emacs_read_quit (fd, read_buf, 1024 * 4); + nread = emacs_fd_read (fd, read_buf, 1024 * 4); else { - nread = emacs_read_quit (fd, read_buf, 1024); + nread = emacs_fd_read (fd, read_buf, 1024); if (nread == 1024) { int ntail; - if (lseek (fd, st.st_size - 1024 * 3, SEEK_CUR) < 0) + if (emacs_fd_lseek (fd, st.st_size - 1024 * 3, SEEK_CUR) < 0) report_file_error ("Setting file position", orig_filename); - ntail = emacs_read_quit (fd, read_buf + nread, 1024 * 3); + ntail = emacs_fd_read (fd, read_buf + nread, 1024 * 3); nread = ntail < 0 ? ntail : nread + ntail; } } @@ -4247,7 +4303,7 @@ by calling `format-decode', which see. */) specpdl_ptr--; /* Rewind the file for the actual read done later. */ - if (lseek (fd, 0, SEEK_SET) < 0) + if (emacs_fd_lseek (fd, 0, SEEK_SET) < 0) report_file_error ("Setting file position", orig_filename); } } @@ -4306,7 +4362,7 @@ by calling `format-decode', which see. */) if (beg_offset != 0) { - if (lseek (fd, beg_offset, SEEK_SET) < 0) + if (emacs_fd_lseek (fd, beg_offset, SEEK_SET) < 0) report_file_error ("Setting file position", orig_filename); } @@ -4314,7 +4370,7 @@ by calling `format-decode', which see. */) match the text at the beginning of the buffer. */ while (true) { - int nread = emacs_read_quit (fd, read_buf, sizeof read_buf); + int nread = emacs_fd_read (fd, read_buf, sizeof read_buf); if (nread < 0) report_file_error ("Read error", orig_filename); else if (nread == 0) @@ -4349,7 +4405,7 @@ by calling `format-decode', which see. */) there's no need to replace anything. */ if (same_at_start - BEGV_BYTE == end_offset - beg_offset) { - emacs_close (fd); + emacs_fd_close (fd); clear_unwind_protect (fd_index); /* Truncate the buffer to the size of the file. */ @@ -4372,14 +4428,14 @@ by calling `format-decode', which see. */) break; /* How much can we scan in the next step? */ trial = min (curpos, sizeof read_buf); - if (lseek (fd, curpos - trial, SEEK_SET) < 0) + if (emacs_fd_lseek (fd, curpos - trial, SEEK_SET) < 0) report_file_error ("Setting file position", orig_filename); total_read = nread = 0; while (total_read < trial) { - nread = emacs_read_quit (fd, read_buf + total_read, - trial - total_read); + nread = emacs_fd_read (fd, read_buf + total_read, + trial - total_read); if (nread < 0) report_file_error ("Read error", orig_filename); else if (nread == 0) @@ -4499,7 +4555,7 @@ by calling `format-decode', which see. */) /* First read the whole file, performing code conversion into CONVERSION_BUFFER. */ - if (lseek (fd, beg_offset, SEEK_SET) < 0) + if (emacs_fd_lseek (fd, beg_offset, SEEK_SET) < 0) report_file_error ("Setting file position", orig_filename); inserted = 0; /* Bytes put into CONVERSION_BUFFER so far. */ @@ -4510,8 +4566,8 @@ by calling `format-decode', which see. */) /* Read at most READ_BUF_SIZE bytes at a time, to allow quitting while reading a huge file. */ - this = emacs_read_quit (fd, read_buf + unprocessed, - READ_BUF_SIZE - unprocessed); + this = emacs_fd_read (fd, read_buf + unprocessed, + READ_BUF_SIZE - unprocessed); if (this <= 0) break; @@ -4526,7 +4582,7 @@ by calling `format-decode', which see. */) if (this < 0) report_file_error ("Read error", orig_filename); - emacs_close (fd); + emacs_fd_close (fd); clear_unwind_protect (fd_index); if (unprocessed > 0) @@ -4673,7 +4729,7 @@ by calling `format-decode', which see. */) if (beg_offset != 0 || !NILP (replace)) { - if (lseek (fd, beg_offset, SEEK_SET) < 0) + if (emacs_fd_lseek (fd, beg_offset, SEEK_SET) < 0) report_file_error ("Setting file position", orig_filename); } @@ -4726,10 +4782,10 @@ by calling `format-decode', which see. */) /* Allow quitting out of the actual I/O. We don't make text part of the buffer until all the reading is done, so a C-g here doesn't do any harm. */ - this = emacs_read_quit (fd, - ((char *) BEG_ADDR + PT_BYTE - BEG_BYTE - + inserted), - trytry); + this = emacs_fd_read (fd, + ((char *) BEG_ADDR + PT_BYTE - BEG_BYTE + + inserted), + trytry); } if (this <= 0) @@ -4756,7 +4812,7 @@ by calling `format-decode', which see. */) else Fset (Qdeactivate_mark, Qt); - emacs_close (fd); + emacs_fd_close (fd); clear_unwind_protect (fd_index); if (read_quit < 0) diff --git a/src/frame.h b/src/frame.h index ca4cca17d74..e2900d1c15b 100644 --- a/src/frame.h +++ b/src/frame.h @@ -88,6 +88,7 @@ enum text_conversion_operation TEXTCONV_SET_COMPOSING_REGION, TEXTCONV_SET_POINT_AND_MARK, TEXTCONV_DELETE_SURROUNDING_TEXT, + TEXTCONV_REQUEST_POINT_UPDATE, }; /* Structure describing a single edit being performed by the input diff --git a/src/textconv.c b/src/textconv.c index 91d386d4c61..c1ce83b1d7d 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -957,6 +957,26 @@ really_delete_surrounding_text (struct frame *f, ptrdiff_t left, unbind_to (count, Qnil); } +/* Update the interface with F's new point and mark. If a batch edit + is in progress, schedule the update for when it finishes + instead. */ + +static void +really_request_point_update (struct frame *f) +{ + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_POINT_CHANGE; + else if (text_interface && text_interface->point_changed) + text_interface->point_changed (f, + XWINDOW (f->old_selected_window), + current_buffer); +} + /* Set point in F to POSITION. If MARK is not POSITION, activate the mark and set MARK to that as well. @@ -1163,6 +1183,10 @@ handle_pending_conversion_events_1 (struct frame *f, really_delete_surrounding_text (f, XFIXNUM (XCAR (data)), XFIXNUM (XCDR (data))); break; + + case TEXTCONV_REQUEST_POINT_UPDATE: + really_request_point_update (f); + break; } /* Signal success. */ @@ -1457,6 +1481,25 @@ delete_surrounding_text (struct frame *f, ptrdiff_t left, input_pending = true; } +/* Request an immediate call to INTERFACE->point_changed with the new + details of F's region unless a batch edit is in progress. */ + +void +request_point_update (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_REQUEST_POINT_UPDATE; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + /* Return N characters of text around point in F's old selected window. diff --git a/src/textconv.h b/src/textconv.h index 16d13deb092..9bc9e7d9bd1 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -138,6 +138,7 @@ extern void textconv_set_point_and_mark (struct frame *, ptrdiff_t, ptrdiff_t, unsigned long); extern void delete_surrounding_text (struct frame *, ptrdiff_t, ptrdiff_t, unsigned long); +extern void request_point_update (struct frame *, unsigned long); extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *); extern bool conversion_disabled_p (void); -- cgit v1.3 From e859a14bee7a84a3aaed45770c89ef60c68b3e08 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 9 Mar 2023 16:30:02 +0800 Subject: Fix menu and popup race conditions on Android * java/org/gnu/emacs/EmacsActivity.java (onContextMenuClosed): * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu) (onMenuItemClick, run): * java/org/gnu/emacs/EmacsDialog.java (EmacsDialog, onClick) (createDialog, onDismiss): Take menu event serial, and pass it along in context menu events. * java/org/gnu/emacs/EmacsNative.java (sendContextMenu): New argument. * src/android.c (sendContextMenu): Pass serial number in event. * src/androidgui.h (struct android_menu_event): New field `menu_event_serial'. * src/androidmenu.c (FIND_METHOD_STATIC) (android_init_emacs_context_menu): Adjust method declarations. (android_menu_show, android_dialog_show): * src/androidterm.c (handle_one_android_event): Expect serial in context menu events. * src/androidterm.h: Update prototypes. --- java/org/gnu/emacs/EmacsActivity.java | 8 +++++++- java/org/gnu/emacs/EmacsContextMenu.java | 14 ++++++++++---- java/org/gnu/emacs/EmacsDialog.java | 15 ++++++++++----- java/org/gnu/emacs/EmacsNative.java | 3 ++- src/android.c | 4 +++- src/androidgui.h | 3 +++ src/androidmenu.c | 24 +++++++++++++++++++----- src/androidterm.c | 7 ++++++- src/androidterm.h | 6 ++++++ 9 files changed, 66 insertions(+), 18 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 692d8a14e22..735a464be8e 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -315,6 +315,8 @@ public class EmacsActivity extends Activity public final void onContextMenuClosed (Menu menu) { + int serial; + Log.d (TAG, "onContextMenuClosed: " + menu); /* See the comment inside onMenuItemClick. */ @@ -335,7 +337,11 @@ public class EmacsActivity extends Activity /* Send a context menu event given that no menu item has already been selected. */ if (!EmacsContextMenu.itemAlreadySelected) - EmacsNative.sendContextMenu ((short) 0, 0); + { + serial = EmacsContextMenu.lastMenuEventSerial; + EmacsNative.sendContextMenu ((short) 0, 0, + serial); + } super.onContextMenuClosed (menu); } diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 0553ff9d4a3..abc1869ac6a 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -49,6 +49,9 @@ public final class EmacsContextMenu /* Whether or not a submenu was selected. */ public static boolean wasSubmenuSelected; + /* The serial ID of the last context menu to be displayed. */ + public static int lastMenuEventSerial; + private static class Item implements MenuItem.OnMenuItemClickListener { public int itemID; @@ -106,7 +109,8 @@ public final class EmacsContextMenu } /* Send a context menu event. */ - EmacsNative.sendContextMenu ((short) 0, itemID); + EmacsNative.sendContextMenu ((short) 0, itemID, + lastMenuEventSerial); /* Say that an item has already been selected. */ itemAlreadySelected = true; @@ -293,12 +297,13 @@ public final class EmacsContextMenu false); } - /* Display this context menu on WINDOW, at xPosition and - yPosition. */ + /* Display this context menu on WINDOW, at xPosition and yPosition. + SERIAL is a number that will be returned in any menu event + generated to identify this context menu. */ public boolean display (final EmacsWindow window, final int xPosition, - final int yPosition) + final int yPosition, final int serial) { Runnable runnable; final Holder rc; @@ -312,6 +317,7 @@ public final class EmacsContextMenu { synchronized (this) { + lastMenuEventSerial = serial; rc.thing = display1 (window, xPosition, yPosition); notify (); } diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 80a5e5f7369..aed84de29e5 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -57,6 +57,9 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener /* Dialog to dismiss after click. */ private AlertDialog dismissDialog; + /* The menu serial associated with this dialog box. */ + private int menuEventSerial; + private class EmacsButton implements View.OnClickListener, DialogInterface.OnClickListener { @@ -76,7 +79,7 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener Log.d (TAG, "onClicked " + this); wasButtonClicked = true; - EmacsNative.sendContextMenu ((short) 0, id); + EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial); dismissDialog.dismiss (); } @@ -87,15 +90,16 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener Log.d (TAG, "onClicked " + this); wasButtonClicked = true; - EmacsNative.sendContextMenu ((short) 0, id); + EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial); } }; /* Create a popup dialog with the title TITLE and the text TEXT. - TITLE may be NULL. */ + TITLE may be NULL. MENUEVENTSERIAL is a number which will + identify this popup dialog inside events it sends. */ public static EmacsDialog - createDialog (String title, String text) + createDialog (String title, String text, int menuEventSerial) { EmacsDialog dialog; @@ -103,6 +107,7 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener dialog.buttons = new ArrayList (); dialog.title = title; dialog.text = text; + dialog.menuEventSerial = menuEventSerial; return dialog; } @@ -330,6 +335,6 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener if (wasButtonClicked) return; - EmacsNative.sendContextMenu ((short) 0, 0); + EmacsNative.sendContextMenu ((short) 0, 0, menuEventSerial); } }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 8e626b9534b..11da5db8746 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -155,7 +155,8 @@ public final class EmacsNative public static native long sendDeiconified (short window); /* Send an ANDROID_CONTEXT_MENU event. */ - public static native long sendContextMenu (short window, int menuEventID); + public static native long sendContextMenu (short window, int menuEventID, + int menuEventSerial); /* Send an ANDROID_EXPOSE event. */ public static native long sendExpose (short window, int x, int y, diff --git a/src/android.c b/src/android.c index b14d2845544..e2ae77e30d0 100644 --- a/src/android.c +++ b/src/android.c @@ -2744,7 +2744,8 @@ NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, JNIEXPORT jlong JNICALL NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, - jshort window, jint menu_event_id) + jshort window, jint menu_event_id, + jint menu_event_serial) { JNI_STACK_ALIGNMENT_PROLOGUE; @@ -2754,6 +2755,7 @@ NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, event.menu.serial = ++event_serial; event.menu.window = window; event.menu.menu_event_id = menu_event_id; + event.menu.menu_event_serial = menu_event_serial; android_write_event (&event); return event_serial; diff --git a/src/androidgui.h b/src/androidgui.h index afcaed98cae..5858a168080 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -418,6 +418,9 @@ struct android_menu_event /* Menu event ID. */ int menu_event_id; + + /* Menu event serial; this counter identifies the context menu. */ + int menu_event_serial; }; enum android_ime_operation diff --git a/src/androidmenu.c b/src/androidmenu.c index 540b25cf602..7d9c33e28b1 100644 --- a/src/androidmenu.c +++ b/src/androidmenu.c @@ -35,6 +35,11 @@ along with GNU Emacs. If not, see . */ static int popup_activated_flag; +/* Serial number used to identify which context menu events are + associated with the context menu currently being displayed. */ + +unsigned int current_menu_serial; + int popup_activated (void) { @@ -96,7 +101,8 @@ android_init_emacs_context_menu (void) eassert (menu_class.c_name); FIND_METHOD_STATIC (create_context_menu, "createContextMenu", - "(Ljava/lang/String;)Lorg/gnu/emacs/EmacsContextMenu;"); + "(Ljava/lang/String;)" + "Lorg/gnu/emacs/EmacsContextMenu;"); FIND_METHOD (add_item, "addItem", "(ILjava/lang/String;ZZZ" "Ljava/lang/String;)V"); @@ -105,7 +111,7 @@ android_init_emacs_context_menu (void) "Lorg/gnu/emacs/EmacsContextMenu;"); FIND_METHOD (add_pane, "addPane", "(Ljava/lang/String;)V"); FIND_METHOD (parent, "parent", "()Lorg/gnu/emacs/EmacsContextMenu;"); - FIND_METHOD (display, "display", "(Lorg/gnu/emacs/EmacsWindow;II)Z"); + FIND_METHOD (display, "display", "(Lorg/gnu/emacs/EmacsWindow;III)Z"); FIND_METHOD (dismiss, "dismiss", "(Lorg/gnu/emacs/EmacsWindow;)V"); #undef FIND_METHOD @@ -254,8 +260,10 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, struct android_menu_subprefix *subprefix, *temp_subprefix; struct android_menu_subprefix *subprefix_1; bool checkmark; + unsigned int serial; count = SPECPDL_INDEX (); + serial = ++current_menu_serial; block_input (); @@ -458,7 +466,8 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, context_menu, menu_class.display, window, (jint) x, - (jint) y); + (jint) y, + (jint) serial); android_exception_check (); if (!rc) @@ -606,7 +615,7 @@ android_init_emacs_dialog (void) name, signature); \ FIND_METHOD_STATIC (create_dialog, "createDialog", "(Ljava/lang/String;" - "Ljava/lang/String;)Lorg/gnu/emacs/EmacsDialog;"); + "Ljava/lang/String;I)Lorg/gnu/emacs/EmacsDialog;"); FIND_METHOD (add_button, "addButton", "(Ljava/lang/String;IZ)V"); FIND_METHOD (display, "display", "()Z"); @@ -625,6 +634,10 @@ android_dialog_show (struct frame *f, Lisp_Object title, bool rc; int id; jmethodID method; + unsigned int serial; + + /* Generate a unique ID for events from this dialog box. */ + serial = ++current_menu_serial; if (menu_items_n_panes > 1) { @@ -651,7 +664,8 @@ android_dialog_show (struct frame *f, Lisp_Object title, dialog = (*android_java_env)->CallStaticObjectMethod (android_java_env, dialog_class.class, method, java_header, - java_title); + java_title, + (jint) serial); android_exception_check (); /* Delete now unused local references. */ diff --git a/src/androidterm.c b/src/androidterm.c index b502b1c6de5..2e2bf86706c 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -1457,7 +1457,12 @@ handle_one_android_event (struct android_display_info *dpyinfo, /* Context menu handling. */ case ANDROID_CONTEXT_MENU: - if (dpyinfo->menu_event_id == -1) + if (dpyinfo->menu_event_id == -1 + /* Previously displayed popup menus might generate events + after dismissal, which might interfere. + `current_menu_serial' is always set to an identifier + identifying the last context menu to be displayed. */ + && event->menu.menu_event_serial == current_menu_serial) dpyinfo->menu_event_id = event->menu.menu_event_id; goto OTHER; diff --git a/src/androidterm.h b/src/androidterm.h index 9bd11bb7853..9964eb54880 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -421,6 +421,12 @@ extern void android_finalize_font_entity (struct font_entity *); /* Defined in androidmenu.c. */ +#ifndef ANDROID_STUBIFY + +extern unsigned int current_menu_serial; + +#endif + extern Lisp_Object android_menu_show (struct frame *, int, int, int, Lisp_Object, const char **); extern Lisp_Object android_popup_dialog (struct frame *, Lisp_Object, -- cgit v1.3 From a7c8ae7d675c402923e2ff8359b9cc286792bbb0 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Mar 2023 11:39:30 +0800 Subject: ; * java/org/gnu/emacs/EmacsNative.java: Add missing dependency. --- java/org/gnu/emacs/EmacsNative.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 11da5db8746..d96c93a83a1 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -229,7 +229,8 @@ public final class EmacsNative "tasn1_emacs", "hogweed_emacs", "jansson_emacs", "jpeg_emacs", "tiff_emacs", "xml2_emacs", - "icuuc_emacs", }; + "icuuc_emacs", + "tree-sitter_emacs", }; for (String dependency : libraryDeps) { -- cgit v1.3 From 1eb546309b24f41b124a0f94aee4009c6dbd8580 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Mar 2023 19:13:22 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Windowing): Document how to pass multimedia keys to the system. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function. * java/org/gnu/emacs/EmacsView.java (onKeyDown, onKeyMultiple) (onKeyUp): Check that function. * java/org/gnu/emacs/EmacsWindow.java (defineCursor): Handle cases where cursor is NULL. * src/android.c (NATIVE_NAME): New function. * src/androidfns.c (syms_of_androidfns): New variable. * src/keyboard.c (lispy_function_keys): Add volume keys. --- doc/emacs/android.texi | 10 ++++++++++ java/org/gnu/emacs/EmacsNative.java | 4 ++++ java/org/gnu/emacs/EmacsView.java | 18 ++++++++++++++++++ java/org/gnu/emacs/EmacsWindow.java | 5 ++++- src/android.c | 9 +++++++++ src/androidfns.c | 11 +++++++++++ src/keyboard.c | 5 +++++ 7 files changed, 61 insertions(+), 1 deletion(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 8e98b92314a..d50acda7710 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -498,6 +498,16 @@ selection, Emacs provides an emulation instead. This means there is no way to transfer the contents of the primary selection to another application via cut-and-paste. +@vindex android-pass-multimedia-buttons-to-system +@cindex volume/multimedia buttons, Android + The volume keys are normally reserved by Emacs and used to provide +the ability to quit Emacs without a physical keyboard +(@pxref{On-Screen-Keyboards}.) However, if you want them to adjust +the volume instead, you can set the variable +@code{android-pass-multimedia-buttons-to-system} to a non-@code{nil} +value; note that you will no longer be able to quit Emacs using the +volume buttons in that case. + @node Android Fonts @section Font backends and selection under Android @cindex fonts, android diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index d96c93a83a1..7d13ff99abb 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -174,6 +174,10 @@ public final class EmacsNative main thread's looper to respond. */ public static native void endSynchronous (); + /* Return whether or not KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP and + KEYCODE_VOLUME_MUTE should be forwarded to Emacs. */ + public static native boolean shouldForwardMultimediaButtons (); + /* Input connection functions. These mostly correspond to their diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 6ace609f386..878ef2f3fbf 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -361,6 +361,12 @@ public final class EmacsView extends ViewGroup public boolean onKeyDown (int keyCode, KeyEvent event) { + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) + && !EmacsNative.shouldForwardMultimediaButtons ()) + return false; + window.onKeyDown (keyCode, event); return true; } @@ -369,6 +375,12 @@ public final class EmacsView extends ViewGroup public boolean onKeyMultiple (int keyCode, int repeatCount, KeyEvent event) { + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) + && !EmacsNative.shouldForwardMultimediaButtons ()) + return false; + window.onKeyDown (keyCode, event); return true; } @@ -377,6 +389,12 @@ public final class EmacsView extends ViewGroup public boolean onKeyUp (int keyCode, KeyEvent event) { + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) + && !EmacsNative.shouldForwardMultimediaButtons ()) + return false; + window.onKeyUp (keyCode, event); return true; } diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 6be609edcfe..d786c104153 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -1234,7 +1234,10 @@ public final class EmacsWindow extends EmacsHandleObject public void run () { - view.setPointerIcon (cursor.icon); + if (cursor != null) + view.setPointerIcon (cursor.icon); + else + view.setPointerIcon (null); } }); } diff --git a/src/android.c b/src/android.c index e39c34136de..79598ab86df 100644 --- a/src/android.c +++ b/src/android.c @@ -2825,6 +2825,15 @@ NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, return event_serial; } +JNIEXPORT jboolean JNICALL +NATIVE_NAME (shouldForwardMultimediaButtons) (JNIEnv *env, + jobject object) +{ + /* Yes, android_pass_multimedia_buttons_to_system is being + read from the UI thread. */ + return !android_pass_multimedia_buttons_to_system; +} + /* Forward declarations of deadlock prevention functions. */ static void android_begin_query (void); diff --git a/src/androidfns.c b/src/androidfns.c index 589ae4331cb..e1d423ab3eb 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -3075,6 +3075,17 @@ syms_of_androidfns (void) doc: /* SKIP: real doc in xfns.c. */); Vx_max_tooltip_size = Qnil; + DEFVAR_BOOL ("android-pass-multimedia-buttons-to-system", + android_pass_multimedia_buttons_to_system, + doc: /* Whether or not to pass volume control buttons to the system. +Generally, the `volume-up', `volume-down' and `volume-mute' keys are +processed by Emacs, but setting this to non-nil they are passed to the +operating system instead of being intercepted by Emacs. + +Note that if you set this, you will no longer be able to quit Emacs +using the volume down button. */); + android_pass_multimedia_buttons_to_system = false; + /* Functions defined. */ defsubr (&Sx_create_frame); defsubr (&Sxw_color_defined_p); diff --git a/src/keyboard.c b/src/keyboard.c index a38c7394543..0a74b435f0e 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -4972,6 +4972,7 @@ const char *const lispy_function_keys[] = [111] = "escape", [112] = "delete", [121] = "break", + [120] = "sysrq", [122] = "home", [123] = "end", [124] = "insert", @@ -4988,6 +4989,7 @@ const char *const lispy_function_keys[] = [141] = "f11", [142] = "f12", [160] = "kp-ret", + [164] = "volume-mute", [19] = "up", [20] = "down", [213] = "muhenkan", @@ -4996,6 +4998,8 @@ const char *const lispy_function_keys[] = [218] = "kana", [21] = "left", [22] = "right", + [24] = "volume-up", + [25] = "volume-down", [259] = "help", [268] = "kp-up-left", [269] = "kp-down-left", @@ -5010,6 +5014,7 @@ const char *const lispy_function_keys[] = [66] = "return", [67] = "backspace", [82] = "menu", + [84] = "find", [92] = "prior", [93] = "next", }; -- cgit v1.3 From 889b61b99918d1c6313d4f884de2e2cb3ab466c9 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 7 May 2023 11:09:56 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (requestCursorUpdates): * java/org/gnu/emacs/EmacsNative.java (requestCursorUpdates): * java/org/gnu/emacs/EmacsService.java (updateCursorAnchorInfo): New functions. * src/android.c (struct android_emacs_service) (android_init_emacs_service): Add new method. (android_update_cursor_anchor_info): New function. * src/androidfns.c (android_set_preeditarea): New function. * src/androidgui.h (enum android_ime_operation): New operation `REQUEST_CURSOR_UPDATES'. (struct android_ime_event): Document new meaning of `length'. * src/androidterm.c (android_request_cursor_updates): New function. (android_handle_ime_event): Handle new operations. (handle_one_android_event, android_draw_window_cursor): Update the preedit area if needed, like on X. (requestCursorUpdates): New function. * src/androidterm.h (struct android_output): New field `need_cursor_updates'. --- java/org/gnu/emacs/EmacsInputConnection.java | 11 ++++ java/org/gnu/emacs/EmacsNative.java | 1 + java/org/gnu/emacs/EmacsService.java | 32 ++++++++++ src/android.c | 34 +++++++++++ src/androidfns.c | 28 +++++++++ src/androidgui.h | 15 ++++- src/androidterm.c | 88 ++++++++++++++++++++++++++++ src/androidterm.h | 5 ++ 8 files changed, 213 insertions(+), 1 deletion(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index d13b48288ce..21bbaca5d07 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -324,6 +324,17 @@ public final class EmacsInputConnection extends BaseInputConnection return this.deleteSurroundingText (beforeLength, afterLength); } + @Override + public boolean + requestCursorUpdates (int cursorUpdateMode) + { + if (EmacsService.DEBUG_IC) + Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode); + + EmacsNative.requestCursorUpdates (windowHandle, cursorUpdateMode); + return true; + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 7d13ff99abb..e699dda9ad4 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -209,6 +209,7 @@ public final class EmacsNative ExtractedTextRequest req, int flags); public static native void requestSelectionUpdate (short window); + public static native void requestCursorUpdates (short window, int mode); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 33436892caa..30ef71540a9 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -25,10 +25,12 @@ import java.io.UnsupportedEncodingException; import java.util.List; +import android.graphics.Matrix; import android.graphics.Point; import android.view.InputDevice; import android.view.KeyEvent; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.ExtractedText; import android.app.Notification; @@ -635,6 +637,36 @@ public final class EmacsService extends Service window.view.imManager.restartInput (window.view); } + public void + updateCursorAnchorInfo (EmacsWindow window, float x, + float y, float yBaseline, + float yBottom) + { + CursorAnchorInfo info; + CursorAnchorInfo.Builder builder; + Matrix matrix; + int[] offsets; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return; + + offsets = new int[2]; + builder = new CursorAnchorInfo.Builder (); + matrix = new Matrix (window.view.getMatrix ()); + window.view.getLocationOnScreen (offsets); + matrix.postTranslate (offsets[0], offsets[1]); + builder.setMatrix (matrix); + builder.setInsertionMarkerLocation (x, y, yBaseline, yBottom, + 0); + info = builder.build (); + + if (DEBUG_IC) + Log.d (TAG, ("updateCursorAnchorInfo: " + x + " " + y + + " " + yBaseline + "-" + yBottom)); + + window.view.imManager.updateCursorAnchorInfo (window.view, info); + } + /* Open a content URI described by the bytes BYTES, a non-terminated string; make it writable if WRITABLE, and readable if READABLE. Truncate the file if TRUNCATE. diff --git a/src/android.c b/src/android.c index 129ad6b5767..8a41a7cdec5 100644 --- a/src/android.c +++ b/src/android.c @@ -113,6 +113,7 @@ struct android_emacs_service jmethodID query_battery; jmethodID display_toast; jmethodID update_extracted_text; + jmethodID update_cursor_anchor_info; }; struct android_emacs_pixmap @@ -2209,6 +2210,8 @@ android_init_emacs_service (void) FIND_METHOD (update_extracted_text, "updateExtractedText", "(Lorg/gnu/emacs/EmacsWindow;" "Landroid/view/inputmethod/ExtractedText;I)V"); + FIND_METHOD (update_cursor_anchor_info, "updateCursorAnchorInfo", + "(Lorg/gnu/emacs/EmacsWindow;FFFF)V"); #undef FIND_METHOD } @@ -6277,6 +6280,37 @@ android_update_extracted_text (android_window window, void *text, android_exception_check_1 (text); } +/* Report the position of the cursor to the input method connection on + WINDOW. + + X is the horizontal position of the end of the insertion marker. Y + is the top of the insertion marker. Y_BASELINE is the baseline of + the row containing the insertion marker, and Y_BOTTOM is the bottom + of the insertion marker. */ + +void +android_update_cursor_anchor_info (android_window window, float x, + float y, float y_baseline, + float y_bottom) +{ + jobject object; + jmethodID method; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + method = service_class.update_cursor_anchor_info; + + (*android_java_env)->CallNonvirtualVoidMethod (android_java_env, + emacs_service, + service_class.class, + method, + object, + (jfloat) x, + (jfloat) y, + (jfloat) y_baseline, + (jfloat) y_bottom); + android_exception_check (); +} + /* Window decoration management functions. */ diff --git a/src/androidfns.c b/src/androidfns.c index 3bd34edd5b9..60b0549e7d1 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -3000,6 +3000,34 @@ for more details about these values. */) make_fixnum (state.temperature)); } + + +/* Miscellaneous input method related stuff. */ + +/* Report X, Y, by the phys cursor width and height as the cursor + anchor rectangle for W's frame. */ + +void +android_set_preeditarea (struct window *w, int x, int y) +{ + struct frame *f; + + f = WINDOW_XFRAME (w); + + /* Convert the window coordinates to the frame's coordinate + space. */ + x = (WINDOW_TO_FRAME_PIXEL_X (w, x) + + WINDOW_LEFT_FRINGE_WIDTH (w) + + WINDOW_LEFT_MARGIN_WIDTH (w)); + y = WINDOW_TO_FRAME_PIXEL_Y (w, y); + + /* Note that calculating the baseline is too hard, so the bottom of + the cursor is used instead. */ + android_update_cursor_anchor_info (FRAME_ANDROID_WINDOW (f), x, + y, y + w->phys_cursor_height, + y + w->phys_cursor_height); +} + #endif diff --git a/src/androidgui.h b/src/androidgui.h index ddd8e9fcf72..6db25098398 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -432,6 +432,13 @@ enum android_ime_operation ANDROID_IME_START_BATCH_EDIT, ANDROID_IME_END_BATCH_EDIT, ANDROID_IME_REQUEST_SELECTION_UPDATE, + ANDROID_IME_REQUEST_CURSOR_UPDATES, + }; + +enum + { + ANDROID_CURSOR_UPDATE_IMMEDIATE = 1, + ANDROID_CURSOR_UPDATE_MONITOR = (1 << 1), }; struct android_ime_event @@ -452,7 +459,11 @@ struct android_ime_event indices, and may actually mean ``left'' and ``right''. */ ptrdiff_t start, end, position; - /* The number of characters in TEXT. */ + /* The number of characters in TEXT. + + If OPERATION is ANDROID_IME_REQUEST_CURSOR_UPDATES, then this is + actually the cursor update mode associated with that + operation. */ size_t length; /* TEXT is either NULL, or a pointer to LENGTH bytes of malloced @@ -620,6 +631,8 @@ extern void android_update_ic (android_window, ptrdiff_t, ptrdiff_t, extern void android_reset_ic (android_window, enum android_ic_mode); extern void android_update_extracted_text (android_window, void *, int); +extern void android_update_cursor_anchor_info (android_window, float, + float, float, float); extern int android_set_fullscreen (android_window, bool); enum android_cursor_shape diff --git a/src/androidterm.c b/src/androidterm.c index 8ba7fb6a798..6f7c06875ca 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -632,6 +632,40 @@ android_decode_utf16 (unsigned short *utf16, size_t n) return coding.dst_object; } +/* Handle a cursor update request for F from the input method. + MODE specifies whether or not an update should be sent immediately, + and whether or not they are needed in the future. + + If MODE & ANDROID_CURSOR_UPDATE_IMMEDIATE, report the position of + F's old selected window's phys cursor now. + + If MODE & ANDROID_CURSOR_UPDATE_MONITOR, set + `need_cursor_updates'. */ + +static void +android_request_cursor_updates (struct frame *f, int mode) +{ + struct window *w; + + if (mode & ANDROID_CURSOR_UPDATE_IMMEDIATE + && WINDOWP (WINDOW_LIVE_P (f->old_selected_window) + ? f->old_selected_window + : f->selected_window)) + { + /* Prefer the old selected window, as its selection is what was + reported to the IME previously. */ + + w = XWINDOW (WINDOW_LIVE_P (f->old_selected_window) + ? f->old_selected_window + : f->selected_window); + android_set_preeditarea (w, w->cursor.x, w->cursor.y); + } + + /* Now say whether or not updates are needed in the future. */ + FRAME_OUTPUT_DATA (f)->need_cursor_updates + = (mode & ANDROID_CURSOR_UPDATE_MONITOR); +} + /* Handle a single input method event EVENT, delivered to the frame F. @@ -705,6 +739,10 @@ android_handle_ime_event (union android_event *event, struct frame *f) case ANDROID_IME_REQUEST_SELECTION_UPDATE: request_point_update (f, event->ime.counter); break; + + case ANDROID_IME_REQUEST_CURSOR_UPDATES: + android_request_cursor_updates (f, event->ime.length); + break; } } @@ -724,6 +762,7 @@ handle_one_android_event (struct android_display_info *dpyinfo, double scroll_unit; int keysym; ptrdiff_t nchars, i; + struct window *w; /* It is okay for this to not resemble handle_one_xevent so much. Differences in event handling code are much less nasty than @@ -812,6 +851,12 @@ handle_one_android_event (struct android_display_info *dpyinfo, inev.ie.kind = MOVE_FRAME_EVENT; XSETFRAME (inev.ie.frame_or_window, f); } + + if (f && FRAME_OUTPUT_DATA (f)->need_cursor_updates) + { + w = XWINDOW (f->selected_window); + android_set_preeditarea (w, w->cursor.x, w->cursor.y); + } } goto OTHER; @@ -954,6 +999,16 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto done_keysym; done_keysym: + + /* Now proceed to tell the input method the current position of + the cursor, if required. */ + + if (f && FRAME_OUTPUT_DATA (f)->need_cursor_updates) + { + w = XWINDOW (f->selected_window); + android_set_preeditarea (w, w->cursor.x, w->cursor.y); + } + goto OTHER; case ANDROID_FOCUS_IN: @@ -4321,6 +4376,10 @@ android_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, int x, int y, enum text_cursor_kinds cursor_type, int cursor_width, bool on_p, bool active_p) { + struct frame *f; + + f = WINDOW_XFRAME (w); + if (on_p) { w->phys_cursor_type = cursor_type; @@ -4362,6 +4421,13 @@ android_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, emacs_abort (); } } + + /* Now proceed to tell the input method the current position of + the cursor, if required. */ + + if (FRAME_OUTPUT_DATA (f)->need_cursor_updates + && w == XWINDOW (f->selected_window)) + android_set_preeditarea (w, x, y); } } @@ -5404,6 +5470,28 @@ NATIVE_NAME (requestSelectionUpdate) (JNIEnv *env, jobject object, android_write_event (&event); } +JNIEXPORT void JNICALL +NATIVE_NAME (requestCursorUpdates) (JNIEnv *env, jobject object, + jshort window, jint mode) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_REQUEST_CURSOR_UPDATES; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = mode; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + #ifdef __clang__ #pragma clang diagnostic pop #else diff --git a/src/androidterm.h b/src/androidterm.h index 9396d5fe315..e3738fb2192 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -231,6 +231,10 @@ struct android_output because the frame contents have been dirtied. */ bool_bf need_buffer_flip : 1; + /* Whether or not the input method should be notified every time the + position of this frame's selected window changes. */ + bool_bf need_cursor_updates : 1; + /* Relief GCs, colors etc. */ struct relief { struct android_gc *gc; @@ -383,6 +387,7 @@ extern struct android_display_info *x_display_list; extern void android_free_gcs (struct frame *); extern void android_default_font_parameter (struct frame *, Lisp_Object); +extern void android_set_preeditarea (struct window *, int, int); /* Defined in androidterm.c. */ -- cgit v1.3 From 7fdde02f3216536aa8977fa4b396bec8fbaf994b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 29 May 2023 17:46:19 +0800 Subject: Work around more problems with Bitmaps * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function `blitRect'. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): Use it on Android 8.x. * src/android.c (blitRect): Implement new function. --- java/org/gnu/emacs/EmacsNative.java | 10 ++++ java/org/gnu/emacs/EmacsSurfaceView.java | 33 +++++++++-- src/android.c | 98 ++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 4 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index e699dda9ad4..56c03ee38dc 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -20,6 +20,9 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import android.content.res.AssetManager; + +import android.graphics.Bitmap; + import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -216,6 +219,13 @@ public final class EmacsNative failure. */ public static native int[] getSelection (short window); + + /* Graphics functions used as a replacement for potentially buggy + Android APIs. */ + + public static native void blitRect (Bitmap src, Bitmap dest, int x1, + int y1, int x2, int y2); + static { /* Older versions of Android cannot link correctly with shared diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index e0411f7f8b3..0deb930c2c2 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -57,11 +57,36 @@ public final class EmacsSurfaceView extends View private void copyToFrontBuffer (Bitmap bitmap, Rect damageRect) { - if (damageRect != null) - bitmapCanvas.drawBitmap (bitmap, damageRect, damageRect, - bitmapPaint); + EmacsService.checkEmacsThread (); + + if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O + && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1) + { + /* If `drawBitmap' can safely be used while a bitmap is locked + by another thread, continue here... */ + + if (damageRect != null) + bitmapCanvas.drawBitmap (bitmap, damageRect, damageRect, + bitmapPaint); + else + bitmapCanvas.drawBitmap (bitmap, 0f, 0f, bitmapPaint); + } else - bitmapCanvas.drawBitmap (bitmap, 0f, 0f, bitmapPaint); + { + /* But if it can not, as on Android 8.0 and 8.1, then use a + replacement function. */ + + if (damageRect != null) + EmacsNative.blitRect (bitmap, frontBuffer, + damageRect.left, + damageRect.top, + damageRect.right, + damageRect.bottom); + else + EmacsNative.blitRect (bitmap, frontBuffer, 0, 0, + bitmap.getWidth (), + bitmap.getHeight ()); + } } private void diff --git a/src/android.c b/src/android.c index 7b9c478f212..4184be3086b 100644 --- a/src/android.c +++ b/src/android.c @@ -4754,6 +4754,104 @@ android_copy_area (android_drawable src, android_drawable dest, +JNIEXPORT void JNICALL +NATIVE_NAME (blitRect) (JNIEnv *env, jobject object, + jobject src, jobject dest, + jint x1, jint y1, jint x2, jint y2) +{ + AndroidBitmapInfo src_info, dest_info; + unsigned char *src_data_1, *dest_data_1; + void *src_data, *dest_data; + + /* N.B. that X2 and Y2 represent the pixel past the edge of the + rectangle; thus, the width is x2 - x1 and the height is y2 - + y1. */ + + memset (&src_info, 0, sizeof src_info); + memset (&dest_info, 0, sizeof dest_info); + AndroidBitmap_getInfo (env, src, &src_info); + AndroidBitmap_getInfo (env, dest, &dest_info); + + /* If the stride is 0 after a call to `getInfo', assume it + failed. */ + + if (!src_info.stride || !dest_info.stride) + return; + + /* If formats differ, abort. */ + eassert (src_info.format == dest_info.format + && src_info.format == ANDROID_BITMAP_FORMAT_RGBA_8888); + + /* Lock the image data. */ + src_data = NULL; + AndroidBitmap_lockPixels (env, src, &src_data); + + if (!src_data) + return; + + dest_data = NULL; + AndroidBitmap_lockPixels (env, dest, &dest_data); + + if (!dest_data) + goto fail1; + + /* Now clip the rectangle to the bounds of the source and + destination bitmap. */ + + x1 = MAX (x1, 0); + y1 = MAX (y1, 0); + x2 = MAX (x2, 0); + y2 = MAX (y2, 0); + + + if (x1 >= src_info.width + || x1 >= dest_info.width) + x1 = MIN (dest_info.width - 1, src_info.width - 1); + + if (x2 > src_info.width + || x2 > dest_info.width) + x2 = MIN (src_info.width, dest_info.width); + + if (y1 >= src_info.height + || y1 >= dest_info.height) + y1 = MIN (dest_info.height - 1, src_info.height - 1); + + if (y2 > src_info.height + || y2 > dest_info.height) + y2 = MIN (src_info.height, dest_info.height); + + if (x1 >= x2 || y1 >= y2) + goto fail2; + + /* Determine the address of the first line to copy. */ + + src_data_1 = src_data; + dest_data_1 = dest_data; + src_data_1 += x1 * 4; + src_data_1 += y1 * src_info.stride; + dest_data_1 += x1 * 4; + dest_data_1 += y1 * dest_info.stride; + + /* Start copying each line. */ + + while (y1 != y2) + { + memcpy (dest_data_1, src_data_1, (x2 - x1) * 4); + src_data_1 += src_info.stride; + dest_data_1 += dest_info.stride; + y1++; + } + + /* Complete the copy and unlock the bitmap. */ + + fail2: + AndroidBitmap_unlockPixels (env, dest); + fail1: + AndroidBitmap_unlockPixels (env, src); +} + + + void android_free_pixmap (android_pixmap pixmap) { -- cgit v1.3 From aed0a11147e29fc73405f1815fef91ecf6cca7fb Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 1 Jun 2023 15:16:02 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection, performContextMenuAction): New function. * java/org/gnu/emacs/EmacsNative.java (EmacsNative) (performContextMenuAction): New function. * src/android.c (android_get_gc_values): Implement more efficiently. * src/androidterm.c (android_handle_ime_event): Pass through `update' argument to `finish_composing_text'. Fix thinko. * src/textconv.c (really_finish_composing_text) (really_set_composing_text, really_set_composing_region) (handle_pending_conversion_events_1, finish_composing_text): New argument `update'. Notify IME of conversion region changes if set. * src/textconv.h: Update structs and prototypes. --- java/org/gnu/emacs/EmacsInputConnection.java | 46 ++++++++++++++++++ java/org/gnu/emacs/EmacsNative.java | 2 + src/android.c | 15 ++---- src/androidterm.c | 70 ++++++++++++++++++++++++++-- src/textconv.c | 23 ++++++--- src/textconv.h | 3 +- 6 files changed, 136 insertions(+), 23 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 54c98d950aa..eb6fd5f2763 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -256,6 +256,52 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + performContextMenuAction (int contextMenuAction) + { + int action; + + if (EmacsService.DEBUG_IC) + Log.d (TAG, "performContextMenuAction: " + contextMenuAction); + + /* Translate the action in Java code. That way, a great deal of + JNI boilerplate can be avoided. */ + + switch (contextMenuAction) + { + case android.R.id.selectAll: + action = 0; + break; + + case android.R.id.startSelectingText: + action = 1; + break; + + case android.R.id.stopSelectingText: + action = 2; + break; + + case android.R.id.cut: + action = 3; + break; + + case android.R.id.copy: + action = 4; + break; + + case android.R.id.paste: + action = 5; + break; + + default: + return true; + } + + EmacsNative.performContextMenuAction (windowHandle, action); + return true; + } + @Override public ExtractedText getExtractedText (ExtractedTextRequest request, int flags) diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 56c03ee38dc..eb75201088b 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -208,6 +208,8 @@ public final class EmacsNative public static native void setSelection (short window, int start, int end); public static native void performEditorAction (short window, int editorAction); + public static native void performContextMenuAction (short window, + int contextMenuAction); public static native ExtractedText getExtractedText (short window, ExtractedTextRequest req, int flags); diff --git a/src/android.c b/src/android.c index 67590ae373d..94587344eb5 100644 --- a/src/android.c +++ b/src/android.c @@ -3863,22 +3863,13 @@ android_get_gc_values (struct android_gc *gc, values->clip_y_origin = gc->clip_y_origin; if (mask & ANDROID_GC_FILL_STYLE) - values->fill_style - = (*android_java_env)->GetIntField (android_java_env, - gcontext, - emacs_gc_fill_style); + values->fill_style = gc->fill_style; if (mask & ANDROID_GC_TILE_STIP_X_ORIGIN) - values->ts_x_origin - = (*android_java_env)->GetIntField (android_java_env, - gcontext, - emacs_gc_ts_origin_x); + values->ts_x_origin = gc->ts_x_origin; if (mask & ANDROID_GC_TILE_STIP_Y_ORIGIN) - values->ts_y_origin - = (*android_java_env)->GetIntField (android_java_env, - gcontext, - emacs_gc_ts_origin_y); + values->ts_y_origin = gc->ts_y_origin; /* Fields involving handles are not used by Emacs, and thus not implemented */ diff --git a/src/androidterm.c b/src/androidterm.c index a9b5834c08f..c302e3f2877 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -681,7 +681,6 @@ android_handle_ime_event (union android_event *event, struct frame *f) switch (event->ime.operation) { case ANDROID_IME_COMMIT_TEXT: - case ANDROID_IME_FINISH_COMPOSING_TEXT: case ANDROID_IME_SET_COMPOSING_TEXT: text = android_decode_utf16 (event->ime.text, event->ime.length); @@ -708,7 +707,8 @@ android_handle_ime_event (union android_event *event, struct frame *f) break; case ANDROID_IME_FINISH_COMPOSING_TEXT: - finish_composing_text (f, event->ime.counter); + finish_composing_text (f, event->ime.counter, + event->ime.length); break; case ANDROID_IME_SET_COMPOSING_TEXT: @@ -5161,7 +5161,71 @@ NATIVE_NAME (performEditorAction) (JNIEnv *env, jobject object, /* Undocumented behavior: performEditorAction is apparently expected to finish composing any text. */ - NATIVE_NAME (finishComposingText) (env, object, window); + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_FINISH_COMPOSING_TEXT; + event.ime.start = 0; + event.ime.end = 0; + + /* This value of `length' means that the input method should receive + an update containing the new conversion region. */ + + event.ime.length = 1; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); + + /* Finally, send the return key press. */ + + event.xkey.type = ANDROID_KEY_PRESS; + event.xkey.serial = ++event_serial; + event.xkey.window = window; + event.xkey.time = 0; + event.xkey.state = 0; + event.xkey.keycode = 66; + event.xkey.unicode_char = 0; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (performContextMenuAction) (JNIEnv *env, jobject object, + jshort window, int action) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + int key; + + /* Note that ACTION is determined in EmacsInputConnection, and as + such they are not actual resource IDs. */ + + switch (action) + { + case 0: /* android.R.id.selectAll */ + case 1: /* android.R.id.startSelectingText */ + case 2: /* android.R.id.stopSelectingText */ + /* These actions are not implemented. */ + return; + + case 3: /* android.R.id.cut */ + key = 277; + break; + + case 4: /* android.R.id.copy */ + key = 278; + break; + + case 5: /* android.R.id.paste */ + key = 279; + break; + + default: + emacs_abort (); + } event.xkey.type = ANDROID_KEY_PRESS; event.xkey.serial = ++event_serial; diff --git a/src/textconv.c b/src/textconv.c index dcf016104fe..d8166bcfd03 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -676,10 +676,11 @@ really_commit_text (struct frame *f, EMACS_INT position, } /* Remove the composition region on the frame F, while leaving its - contents intact. */ + contents intact. If UPDATE, also notify the input method of the + change. */ static void -really_finish_composing_text (struct frame *f) +really_finish_composing_text (struct frame *f, bool update) { if (!NILP (f->conversion.compose_region_start)) { @@ -687,6 +688,10 @@ really_finish_composing_text (struct frame *f) Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); f->conversion.compose_region_start = Qnil; f->conversion.compose_region_end = Qnil; + + if (update && text_interface + && text_interface->compose_region_changed) + (*text_interface->compose_region_changed) (f); } /* Delete the composition region overlay. */ @@ -796,7 +801,7 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, the documentation, but is ultimately what programs expect. */ if (!SCHARS (text)) - really_finish_composing_text (f); + really_finish_composing_text (f, false); /* If PT hasn't changed, the conversion region definitely has. Otherwise, redisplay will update the input method instead. */ @@ -838,7 +843,7 @@ really_set_composing_region (struct frame *f, ptrdiff_t start, if (max (0, start) == max (0, end)) { - really_finish_composing_text (f); + really_finish_composing_text (f, false); return; } @@ -1167,7 +1172,7 @@ handle_pending_conversion_events_1 (struct frame *f, break; case TEXTCONV_FINISH_COMPOSING_TEXT: - really_finish_composing_text (f); + really_finish_composing_text (f, !NILP (data)); break; case TEXTCONV_SET_COMPOSING_TEXT: @@ -1360,16 +1365,20 @@ commit_text (struct frame *f, Lisp_Object string, /* Remove the composition region and its overlay from F's current buffer. Leave the text being composed intact. + If UPDATE, call `compose_region_changed' after the region is + removed. + COUNTER means the same as in `start_batch_edit'. */ void -finish_composing_text (struct frame *f, unsigned long counter) +finish_composing_text (struct frame *f, unsigned long counter, + bool update) { struct text_conversion_action *action, **last; action = xmalloc (sizeof *action); action->operation = TEXTCONV_FINISH_COMPOSING_TEXT; - action->data = Qnil; + action->data = update ? Qt : Qnil; action->next = NULL; action->counter = counter; for (last = &f->conversion.actions; *last; last = &(*last)->next) diff --git a/src/textconv.h b/src/textconv.h index 055bf251651..e632a9dddcf 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -128,7 +128,8 @@ extern void start_batch_edit (struct frame *, unsigned long); extern void end_batch_edit (struct frame *, unsigned long); extern void commit_text (struct frame *, Lisp_Object, ptrdiff_t, unsigned long); -extern void finish_composing_text (struct frame *, unsigned long); +extern void finish_composing_text (struct frame *, unsigned long, + bool); extern void set_composing_text (struct frame *, Lisp_Object, ptrdiff_t, unsigned long); extern void set_composing_region (struct frame *, ptrdiff_t, ptrdiff_t, -- cgit v1.3 From 740af4668c8d9bc8e4ee1e60ebeb366690fee93e Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 4 Jun 2023 12:04:15 +0800 Subject: Fix input method synchronization problems * java/debug.sh (gdbserver_cmd, is_root): Prefer TCP again. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function `queryAndSpin'. * java/org/gnu/emacs/EmacsService.java (EmacsService) (icBeginSynchronous, icEndSynchronous, viewGetSelection): New synchronization functions. (resetIC, updateCursorAnchorInfo): Call those instead. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Call viewGetSelection. * src/android.c (JNICALL, android_answer_query_spin): New functions. --- java/debug.sh | 13 ++--- java/org/gnu/emacs/EmacsNative.java | 6 ++ java/org/gnu/emacs/EmacsService.java | 103 ++++++++++++++++++++++++++--------- java/org/gnu/emacs/EmacsView.java | 13 +++-- src/android.c | 26 +++++++++ 5 files changed, 123 insertions(+), 38 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/debug.sh b/java/debug.sh index 0458003fe72..d6e439bec90 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -19,7 +19,6 @@ ## along with GNU Emacs. If not, see . set -m -set -x oldpwd=`pwd` cd `dirname $0` @@ -273,7 +272,7 @@ fi gdbserver_cmd= is_root= if [ -z "$gdbserver" ]; then - gdbserver_bin=/system/bin/gdbserver + gdbserver_bin=/system/bin/gdbserver64 else gdbserver_bin=/data/local/tmp/gdbserver gdbserver_cat="cat $gdbserver_bin | run-as $package sh -c \ @@ -312,12 +311,12 @@ rm -f /tmp/file-descriptor-stamp if [ -z "$gdbserver" ]; then if [ "$is_root" = "yes" ]; then adb -s $device shell $gdbserver_bin --multi \ - "+/data/local/tmp/debug.$package.socket" --attach $pid >&5 & - gdb_socket="localfilesystem:/data/local/tmp/debug.$package.socket" + "0.0.0.0:7564" --attach $pid >&5 & + gdb_socket="tcp:7564" else - adb -s $device shell run-as $package $gdbserver_bin --multi \ - "+debug.$package.socket" --attach $pid >&5 & - gdb_socket="localfilesystem:$app_data_dir/debug.$package.socket" + adb -s $device shell $gdbserver_bin --multi \ + "0.0.0.0:7564" --attach $pid >&5 & + gdb_socket="tcp:7564" fi else # Normally the program cannot access $gdbserver_bin when it is diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index eb75201088b..f03736fe614 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -177,6 +177,12 @@ public final class EmacsNative main thread's looper to respond. */ public static native void endSynchronous (); + /* Prevent deadlocks while reliably allowing queries from the Emacs + thread to the main thread to complete by waiting for a query to + start from the main thread, then answer it; assume that a query + is certain to start shortly. */ + public static native void answerQuerySpin (); + /* Return whether or not KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP and KEYCODE_VOLUME_MUTE should be forwarded to Emacs. */ public static native boolean shouldForwardMultimediaButtons (); diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index dde60e1c5af..6d70536faf0 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -25,6 +25,8 @@ import java.io.UnsupportedEncodingException; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + import android.graphics.Matrix; import android.graphics.Point; @@ -106,8 +108,17 @@ public final class EmacsService extends Service performing drawing calls. */ private static final boolean DEBUG_THREADS = false; - /* Whether or not onCreateInputMethod is calling getSelection. */ - public static volatile boolean imSyncInProgress; + /* Atomic integer used for synchronization between + icBeginSynchronous/icEndSynchronous and viewGetSelection. + + Value is 0 if no query is in progress, 1 if viewGetSelection is + being called, and 2 if icBeginSynchronous was called. */ + public static final AtomicInteger servicingQuery; + + static + { + servicingQuery = new AtomicInteger (); + }; /* Return the directory leading to the directory in which native library files are stored on behalf of CONTEXT. */ @@ -658,46 +669,79 @@ public final class EmacsService extends Service EmacsNative.endSynchronous (); } + + + /* IMM functions such as `updateSelection' holds an internal lock + that is also taken before `onCreateInputConnection' (in + EmacsView.java) is called; when that then asks the UI thread for + the current selection, a dead lock results. To remedy this, + reply to any synchronous queries now -- and prohibit more queries + for the duration of `updateSelection' -- if EmacsView may have + been asking for the value of the region. */ + + public static void + icBeginSynchronous () + { + /* Set servicingQuery to 2, so viewGetSelection knows it shouldn't + proceed. */ + + if (servicingQuery.getAndSet (2) == 1) + /* But if viewGetSelection is already in progress, answer it + first. */ + EmacsNative.answerQuerySpin (); + } + + public static void + icEndSynchronous () + { + if (servicingQuery.getAndSet (0) != 2) + throw new RuntimeException ("incorrect value of `servicingQuery': " + + "likely 1"); + } + + public static int[] + viewGetSelection (short window) + { + int[] selection; + + /* See if a query is already in progress from the other + direction. */ + if (!servicingQuery.compareAndSet (0, 1)) + return null; + + /* Now call the regular getSelection. Note that this can't race + with answerQuerySpin, as `android_servicing_query' can never be + 2 when icBeginSynchronous is called, so a query will always be + started. */ + selection = EmacsNative.getSelection (window); + + /* Finally, clear servicingQuery if its value is still 1. If a + query has started from the other side, it ought to be 2. */ + + servicingQuery.compareAndSet (1, 0); + return selection; + } + + + public void updateIC (EmacsWindow window, int newSelectionStart, int newSelectionEnd, int composingRegionStart, int composingRegionEnd) { - boolean wasSynchronous; - if (DEBUG_IC) Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart + " " + newSelectionEnd + " " + composingRegionStart + " " + composingRegionEnd)); - /* `updateSelection' holds an internal lock that is also taken - before `onCreateInputConnection' (in EmacsView.java) is called; - when that then asks the UI thread for the current selection, a - dead lock results. To remedy this, reply to any synchronous - queries now -- and prohibit more queries for the duration of - `updateSelection' -- if EmacsView may have been asking for the - value of the region. */ - - wasSynchronous = false; - if (EmacsService.imSyncInProgress) - { - /* `beginSynchronous' will answer any outstanding queries and - signal that one is now in progress, thereby preventing - `getSelection' from blocking. */ - - EmacsNative.beginSynchronous (); - wasSynchronous = true; - } - + icBeginSynchronous (); window.view.imManager.updateSelection (window.view, newSelectionStart, newSelectionEnd, composingRegionStart, composingRegionEnd); - - if (wasSynchronous) - EmacsNative.endSynchronous (); + icEndSynchronous (); } public void @@ -707,7 +751,10 @@ public final class EmacsService extends Service Log.d (TAG, "resetIC: " + window); window.view.setICMode (icMode); + + icBeginSynchronous (); window.view.imManager.restartInput (window.view); + icEndSynchronous (); } public void @@ -733,11 +780,15 @@ public final class EmacsService extends Service 0); info = builder.build (); + + if (DEBUG_IC) Log.d (TAG, ("updateCursorAnchorInfo: " + x + " " + y + " " + yBaseline + "-" + yBottom)); + icBeginSynchronous (); window.view.imManager.updateCursorAnchorInfo (window.view, info); + icEndSynchronous (); } /* Open a content URI described by the bytes BYTES, a non-terminated diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index bb450bb8e6b..c223dfa7911 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -630,12 +630,11 @@ public final class EmacsView extends ViewGroup /* Obtain the current position of point and set it as the selection. Don't do this under one specific situation: if `android_update_ic' is being called in the main thread, trying - to synchronize with it can cause a dead lock in the IM - manager. */ + to synchronize with it can cause a dead lock in the IM manager. + See icBeginSynchronous in EmacsService.java for more + details. */ - EmacsService.imSyncInProgress = true; - selection = EmacsNative.getSelection (window.handle); - EmacsService.imSyncInProgress = false; + selection = EmacsService.viewGetSelection (window.handle); if (selection != null) Log.d (TAG, "onCreateInputConnection: current selection is: " @@ -664,6 +663,10 @@ public final class EmacsView extends ViewGroup if (inputConnection == null) inputConnection = new EmacsInputConnection (this); + else + /* Reset the composing region, in case there is still composing + text. */ + inputConnection.finishComposingText (); /* Return the input connection. */ return inputConnection; diff --git a/src/android.c b/src/android.c index e74d40a0cdb..f59c0d9e5d2 100644 --- a/src/android.c +++ b/src/android.c @@ -2997,6 +2997,7 @@ NATIVE_NAME (blitRect) (JNIEnv *env, jobject object, static void android_begin_query (void); static void android_end_query (void); +static void android_answer_query_spin (void); JNIEXPORT void JNICALL NATIVE_NAME (beginSynchronous) (JNIEnv *env, jobject object) @@ -3014,6 +3015,14 @@ NATIVE_NAME (endSynchronous) (JNIEnv *env, jobject object) android_end_query (); } +JNIEXPORT void JNICALL +NATIVE_NAME (answerQuerySpin) (JNIEnv *env, jobject object) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + android_answer_query_spin (); +} + #ifdef __clang__ #pragma clang diagnostic pop #else @@ -7041,6 +7050,23 @@ android_answer_query (void) sem_post (&android_query_sem); } +/* Like `android_answer_query'. However, the query may not have + begun; spin until it has. */ + +static void +android_answer_query_spin (void) +{ + int n; + + while (!(n = __atomic_load_n (&android_servicing_query, + __ATOMIC_SEQ_CST))) + eassert (!n); + + /* Note that this function is supposed to be called before + `android_begin_query' starts, so clear the service flag. */ + android_check_query (); +} + /* Notice that the Emacs thread will start blocking waiting for a response from the UI thread. Process any pending queries from the UI thread. -- cgit v1.3 From 66783af554176c68cb58726aeff4ae6a23224234 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 5 Jun 2023 10:38:25 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function clearInputFlags. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Stop reporting changes after a new input method connection is established. * src/androidterm.c (android_handle_ime_event): Implement that change. (JNICALL): New function. --- java/org/gnu/emacs/EmacsNative.java | 1 + java/org/gnu/emacs/EmacsView.java | 8 +++---- src/androidterm.c | 48 ++++++++++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 5 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index f03736fe614..cb1c6caa79a 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -221,6 +221,7 @@ public final class EmacsNative int flags); public static native void requestSelectionUpdate (short window); public static native void requestCursorUpdates (short window, int mode); + public static native void clearInputFlags (short window); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index c223dfa7911..a78dec08839 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -627,6 +627,10 @@ public final class EmacsView extends ViewGroup return null; } + /* Reset flags set by the previous input method. */ + + EmacsNative.clearInputFlags (window.handle); + /* Obtain the current position of point and set it as the selection. Don't do this under one specific situation: if `android_update_ic' is being called in the main thread, trying @@ -663,10 +667,6 @@ public final class EmacsView extends ViewGroup if (inputConnection == null) inputConnection = new EmacsInputConnection (this); - else - /* Reset the composing region, in case there is still composing - text. */ - inputConnection.finishComposingText (); /* Return the input connection. */ return inputConnection; diff --git a/src/androidterm.c b/src/androidterm.c index d8a8b4c2d71..afa10a1b94b 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -675,6 +675,7 @@ static void android_handle_ime_event (union android_event *event, struct frame *f) { Lisp_Object text UNINIT; + struct android_output *output; /* First, decode the text if necessary. */ @@ -707,8 +708,23 @@ android_handle_ime_event (union android_event *event, struct frame *f) break; case ANDROID_IME_FINISH_COMPOSING_TEXT: + + if (event->ime.length == 2) + { + output = FRAME_ANDROID_OUTPUT (f); + + /* A new input method has connected to Emacs. Stop + reporting changes that the previous input method has + asked to monitor. */ + + output->extracted_text_flags = 0; + output->extracted_text_token = 0; + output->extracted_text_hint = 0; + output->need_cursor_updates = false; + } + finish_composing_text (f, event->ime.counter, - event->ime.length); + event->ime.length == 1); break; case ANDROID_IME_SET_COMPOSING_TEXT: @@ -5580,6 +5596,36 @@ NATIVE_NAME (requestCursorUpdates) (JNIEnv *env, jobject object, android_write_event (&event); } +/* Notice that a new input method connection has been initialized and + clear cursor update requests, extracted text requests, and the + composing region. */ + +JNIEXPORT void JNICALL +NATIVE_NAME (clearInputFlags) (JNIEnv *env, jobject object, + jshort window) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_FINISH_COMPOSING_TEXT; + event.ime.start = 0; + event.ime.end = 0; + + /* This value of `length' means that updates to the cursor position + and extracted text should not be reported anymore. */ + + event.ime.length = 2; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + #ifdef __clang__ #pragma clang diagnostic pop #else -- cgit v1.3 From 63339a9577f085074d16fa8c56ddd56864c94dda Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 7 Jun 2023 11:03:56 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (beginBatchEdit) (endBatchEdit, commitCompletion, commitText, deleteSurroundingText) (finishComposingText, getSelectedText, getTextAfterCursor) (getTextBeforeCursor, setComposingText, setComposingRegion) (performEditorAction, performContextMenuAction, getExtractedText) (setSelection, sendKeyEvent, deleteSurroundingTextInCodePoints) (requestCursorUpdates): Ensure that the input connection is up to date. (getSurroundingText): New function. * java/org/gnu/emacs/EmacsNative.java (getSurroundingText): Export new C function. * java/org/gnu/emacs/EmacsService.java (resetIC): Invalidate previously created input connections. * java/org/gnu/emacs/EmacsView.java (EmacsView) (onCreateInputConnection): Signify that input connections are now up to date. * src/androidterm.c (struct android_get_surrounding_text_context): New structure. (android_get_surrounding_text, NATIVE_NAME): * src/textconv.c (get_surrounding_text): * src/textconv.h: New functions. --- java/org/gnu/emacs/EmacsInputConnection.java | 105 ++++++++++++++++++++ java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsService.java | 1 + java/org/gnu/emacs/EmacsView.java | 13 +++ src/androidterm.c | 143 +++++++++++++++++++++++++++ src/textconv.c | 100 +++++++++++++++++++ src/textconv.h | 4 + 7 files changed, 370 insertions(+) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 9ced7cb7aaf..73c93c67ac7 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -23,7 +23,9 @@ import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.SurroundingText; import android.view.inputmethod.TextSnapshot; + import android.view.KeyEvent; import android.os.Build; @@ -88,6 +90,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean beginBatchEdit () { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "beginBatchEdit"); @@ -99,6 +105,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean endBatchEdit () { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "endBatchEdit"); @@ -110,6 +120,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean commitCompletion (CompletionInfo info) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "commitCompletion: " + info); @@ -125,6 +139,10 @@ public final class EmacsInputConnection extends BaseInputConnection { int[] selection; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "commitText: " + text + " " + newCursorPosition); @@ -156,6 +174,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean deleteSurroundingText (int leftLength, int rightLength) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, ("deleteSurroundingText: " + leftLength + " " + rightLength)); @@ -169,6 +191,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean finishComposingText () { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "finishComposingText"); @@ -180,6 +206,10 @@ public final class EmacsInputConnection extends BaseInputConnection public String getSelectedText (int flags) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getSelectedText: " + flags); @@ -192,6 +222,10 @@ public final class EmacsInputConnection extends BaseInputConnection { String string; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); @@ -210,6 +244,10 @@ public final class EmacsInputConnection extends BaseInputConnection { String string; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); @@ -226,6 +264,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean setComposingText (CharSequence text, int newCursorPosition) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, ("setComposingText: " + text + " ## " + newCursorPosition)); @@ -239,6 +281,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean setComposingRegion (int start, int end) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "setComposingRegion: " + start + " " + end); @@ -250,6 +296,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean performEditorAction (int editorAction) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "performEditorAction: " + editorAction); @@ -263,6 +313,10 @@ public final class EmacsInputConnection extends BaseInputConnection { int action; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "performContextMenuAction: " + contextMenuAction); @@ -310,6 +364,10 @@ public final class EmacsInputConnection extends BaseInputConnection ExtractedText text; int[] selection; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + request.hintMaxChars + ", " + request.hintMaxLines + " " + flags); @@ -360,6 +418,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean setSelection (int start, int end) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "setSelection: " + start + " " + end); @@ -371,6 +433,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean sendKeyEvent (KeyEvent key) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "sendKeyEvent: " + key); @@ -381,6 +447,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean deleteSurroundingTextInCodePoints (int beforeLength, int afterLength) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + /* This can be implemented the same way as deleteSurroundingText. */ return this.deleteSurroundingText (beforeLength, afterLength); @@ -390,6 +460,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean requestCursorUpdates (int cursorUpdateMode) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode); @@ -397,6 +471,37 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public SurroundingText + getSurroundingText (int beforeLength, int afterLength, + int flags) + { + SurroundingText text; + + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + + if (EmacsService.DEBUG_IC) + Log.d (TAG, ("getSurroundingText: " + beforeLength + ", " + + afterLength)); + + text = EmacsNative.getSurroundingText (windowHandle, beforeLength, + afterLength, flags); + + if (text != null) + Log.d (TAG, ("getSurroundingText: " + + text.getSelectionStart () + + "," + + text.getSelectionEnd () + + "+" + + text.getOffset () + + ": " + + text.getText ())); + + return text; + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index cb1c6caa79a..cb89cf6808a 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,6 +25,7 @@ import android.graphics.Bitmap; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.SurroundingText; public final class EmacsNative { @@ -222,6 +223,9 @@ public final class EmacsNative public static native void requestSelectionUpdate (short window); public static native void requestCursorUpdates (short window, int mode); public static native void clearInputFlags (short window); + public static native SurroundingText getSurroundingText (short window, + int left, int right, + int flags); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 2074a5b7c2b..48e39f8b355 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -748,6 +748,7 @@ public final class EmacsService extends Service window.view.setICMode (icMode); icBeginSynchronous (); + window.view.icGeneration++; window.view.imManager.restartInput (window.view); icEndSynchronous (); } diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index a78dec08839..d432162132d 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -111,6 +111,13 @@ public final class EmacsView extends ViewGroup details. */ private int icMode; + /* The number of calls to `resetIC' to have taken place the last + time an InputConnection was created. */ + public long icSerial; + + /* The number of calls to `recetIC' that have taken place. */ + public volatile long icGeneration; + public EmacsView (EmacsWindow window) { @@ -627,6 +634,12 @@ public final class EmacsView extends ViewGroup return null; } + /* Set icSerial. If icSerial < icGeneration, the input connection + has been reset, and future input should be ignored until a new + connection is created. */ + + icSerial = icGeneration; + /* Reset flags set by the previous input method. */ EmacsNative.clearInputFlags (window.handle); diff --git a/src/androidterm.c b/src/androidterm.c index 2a054715d6a..77f2bd1c7a0 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -5636,6 +5636,149 @@ NATIVE_NAME (clearInputFlags) (JNIEnv *env, jobject object, android_write_event (&event); } + + +/* Context for a call to `getSurroundingText'. */ + +struct android_get_surrounding_text_context +{ + /* Number of characters before the region to return. */ + int before_length; + + /* Number of characters after the region to return. */ + int after_length; + + /* The returned text, or NULL. */ + char *text; + + /* The size of that text in characters and bytes. */ + ptrdiff_t length, bytes; + + /* Offsets into that text. */ + ptrdiff_t offset, start, end; + + /* The window. */ + android_window window; +}; + +/* Return the surrounding text in the surrounding text context + specified by DATA. */ + +static void +android_get_surrounding_text (void *data) +{ + struct android_get_surrounding_text_context *request; + struct frame *f; + ptrdiff_t temp; + + request = data; + + /* Find the frame associated with the window. */ + f = android_window_to_frame (NULL, request->window); + + if (!f) + return; + + /* Now get the surrounding text. */ + request->text + = get_surrounding_text (f, request->before_length, + request->after_length, &request->length, + &request->bytes, &request->offset, + &request->start, &request->end); + + /* Sort request->start and request->end for compatibility with some + bad input methods. */ + + if (request->end < request->start) + { + temp = request->start; + request->start = request->end; + request->end = temp; + } +} + +JNIEXPORT jobject JNICALL +NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, + jshort window, jint before_length, + jint after_length, jint flags) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + static jclass class; + static jmethodID constructor; + + struct android_get_surrounding_text_context context; + jstring string; + jobject object; + + /* Initialize CLASS if it has not yet been initialized. */ + + if (!class) + { + class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/SurroundingText")); + +#if __ANDROID_API__ < 31 + /* If CLASS cannot be found, the version of Android currently + running is too old. */ + + if (!class) + { + (*env)->ExceptionClear (env); + return NULL; + } +#else /* __ANDROID_API__ >= 31 */ + assert (class); +#endif /* __ANDROID_API__ < 31 */ + + class = (*env)->NewGlobalRef (env, class); + if (!class) + return NULL; + + /* Now look for its constructor. */ + constructor = (*env)->GetMethodID (env, class, "", + "(Ljava/lang/CharSequence;III)V"); + assert (constructor); + } + + context.before_length = before_length; + context.after_length = after_length; + context.window = window; + context.text = NULL; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_surrounding_text, + &context)) + return NULL; + + if (!context.text) + return NULL; + + /* Encode the returned text. */ + string = android_text_to_string (env, context.text, context.length, + context.bytes); + free (context.text); + + if (!string) + return NULL; + + /* Create an SurroundingText object containing this information. */ + object = (*env)->NewObject (env, class, constructor, string, + (jint) min (context.start, + TYPE_MAXIMUM (jint)), + (jint) min (context.end, + TYPE_MAXIMUM (jint)), + /* Adjust point offsets to fit into + Android's 0-based indexing. */ + (jint) min (context.offset - 1, + TYPE_MAXIMUM (jint))); + if (!object) + return NULL; + + return object; +} + #ifdef __clang__ #pragma clang diagnostic pop #else diff --git a/src/textconv.c b/src/textconv.c index 0dcf5bdcea8..1161b781b6a 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -1732,6 +1732,106 @@ get_extracted_text (struct frame *f, ptrdiff_t n, return buffer; } +/* Return the text between the positions PT - LEFT and PT + RIGHT. If + the mark is active, return the range of text relative to the bounds + of the region instead. + + Set *LENGTH to the number of characters returned, *BYTES to the + number of bytes returned, *OFFSET to the character position of the + returned text, and *START_RETURN and *END_RETURN to the mark and + point relative to that position. */ + +char * +get_surrounding_text (struct frame *f, ptrdiff_t left, + ptrdiff_t right, ptrdiff_t *length, + ptrdiff_t *bytes, ptrdiff_t *offset, + ptrdiff_t *start_return, + ptrdiff_t *end_return) +{ + specpdl_ref count; + ptrdiff_t start, end, start_byte, end_byte, mark, temp; + char *buffer; + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return NULL; + + /* Save the excursion, as there will be extensive changes to the + selected window. */ + count = SPECPDL_INDEX (); + record_unwind_protect_excursion (); + + /* Inhibit quitting. */ + specbind (Qinhibit_quit, Qt); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + select_window (f->old_selected_window, Qt); + buffer = NULL; + + /* Figure out the bounds of the text to return. */ + + /* First, obtain start and end. */ + end = get_mark (); + start = PT; + + /* If the mark is not active, make it start and end. */ + + if (end == -1) + end = start; + + /* Now sort start and end. */ + + if (end < start) + { + temp = start; + start = end; + end = temp; + } + + /* And subtract left and right. */ + + if (INT_SUBTRACT_WRAPV (start, left, &start) + || INT_ADD_WRAPV (end, right, &end)) + goto finish; + + start = max (start, BEGV); + end = min (end, ZV); + + /* Detect overflow. */ + + if (!(start <= PT && PT <= end)) + goto finish; + + /* Convert the character positions to byte positions. */ + start_byte = CHAR_TO_BYTE (start); + end_byte = CHAR_TO_BYTE (end); + + /* Extract the text from the buffer. */ + buffer = xmalloc (end_byte - start_byte); + copy_buffer (start, start_byte, end, end_byte, + buffer); + + /* Get the mark. If it's not active, use PT. */ + + mark = get_mark (); + + if (mark == -1) + mark = PT; + + /* Return the offsets. Unlike `get_extracted_text', this need not + sort mark and point. */ + + *offset = start; + *start_return = mark - start; + *end_return = PT - start; + *length = end - start; + *bytes = end_byte - start_byte; + + finish: + unbind_to (count, Qnil); + return buffer; +} + /* Return whether or not text conversion is temporarily disabled. `reset' should always call this to determine whether or not to disable the input method. */ diff --git a/src/textconv.h b/src/textconv.h index 339cefdba92..7550388a723 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -143,6 +143,10 @@ extern void textconv_barrier (struct frame *, unsigned long); extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, bool *); +extern char *get_surrounding_text (struct frame *, ptrdiff_t, + ptrdiff_t, ptrdiff_t *, + ptrdiff_t *, ptrdiff_t *, + ptrdiff_t *, ptrdiff_t *); extern bool conversion_disabled_p (void); extern void register_textconv_interface (struct textconv_interface *); -- cgit v1.3 From c321eea5af535d102c5e1d2d7e95029ce6fee427 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 9 Jun 2023 14:03:50 +0800 Subject: Block profiling signals in the Android UI thread * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function `setupSystemThread'. * java/org/gnu/emacs/EmacsService.java (onCreate): Block all signals except for SIGBUS and SIGSEGV in the UI thread. * src/android.c (setupSystemThread): New function. --- java/org/gnu/emacs/EmacsNative.java | 4 ++++ java/org/gnu/emacs/EmacsService.java | 25 +++++++++++++++++++++++++ src/android.c | 24 ++++++++++++++++++++++++ 3 files changed, 53 insertions(+) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index cb89cf6808a..2fcbf8b94ef 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -188,6 +188,10 @@ public final class EmacsNative KEYCODE_VOLUME_MUTE should be forwarded to Emacs. */ public static native boolean shouldForwardMultimediaButtons (); + /* Initialize the current thread, by blocking signals that do not + interest it. */ + public static native void setupSystemThread (); + /* Input connection functions. These mostly correspond to their diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 48e39f8b355..96216e51cf4 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -25,6 +25,7 @@ import java.io.UnsupportedEncodingException; import java.util.List; +import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import android.graphics.Matrix; @@ -211,6 +212,7 @@ public final class EmacsService extends Service final String filesDir, libDir, cacheDir, classPath; final double pixelDensityX; final double pixelDensityY; + final Semaphore signalSemaphore; SERVICE = this; handler = new Handler (Looper.getMainLooper ()); @@ -220,6 +222,7 @@ public final class EmacsService extends Service pixelDensityX = metrics.xdpi; pixelDensityY = metrics.ydpi; resolver = getContentResolver (); + signalSemaphore = new Semaphore (0); try { @@ -248,11 +251,33 @@ public final class EmacsService extends Service cacheDir, (float) pixelDensityX, (float) pixelDensityY, classPath, EmacsService.this); + + /* Wait for the signal mask to be set up in the UI + thread. */ + + while (true) + { + try + { + signalSemaphore.acquire (); + break; + } + catch (InterruptedException e) + { + ;; + } + } } }, extraStartupArgument, /* If any file needs to be opened, open it now. */ EmacsOpenActivity.fileToOpen); thread.start (); + + /* Now that the thread has been started, block signals which + don't interest the current thread. */ + + EmacsNative.setupSystemThread (); + signalSemaphore.release (); } catch (IOException exception) { diff --git a/src/android.c b/src/android.c index 92aab548180..681723124ee 100644 --- a/src/android.c +++ b/src/android.c @@ -3077,6 +3077,30 @@ NATIVE_NAME (answerQuerySpin) (JNIEnv *env, jobject object) android_answer_query_spin (); } + + +/* System thread setup. Android doesn't always block signals Emacs is + interested in from being received by the UI or render threads, + which can lead to problems when those signals then interrupt one of + those threads. */ + +JNIEXPORT void JNICALL +NATIVE_NAME (setupSystemThread) (void) +{ + sigset_t sigset; + + /* Block everything except for SIGSEGV and SIGBUS; those two are + used by the runtime. */ + + sigfillset (&sigset); + sigaddset (&sigset, SIGSEGV); + sigaddset (&sigset, SIGBUS); + + if (pthread_sigmask (SIG_BLOCK, &sigset, NULL)) + __android_log_print (ANDROID_LOG_WARN, __func__, + "pthread_sigmask: %s", strerror (errno)); +} + #ifdef __clang__ #pragma clang diagnostic pop #else -- cgit v1.3 From 363e293cc919ab02c40bd9a8fa4875c2e5644b2d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 15 Jun 2023 12:36:50 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection, beginBatchEdit, reset, endBatchEdit): Keep track of the number of batch edits and return an appropriate value. (takeSnapshot): Implement function. * java/org/gnu/emacs/EmacsNative.java (takeSnapshot): New function. * java/org/gnu/emacs/EmacsService.java (resetIC): Improve debugging output. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Call `reset' to clear the UI side batch edit count. * src/androidterm.c (struct android_get_surrounding_text_context): New fields `conversion_start' and `conversion_end'. (android_get_surrounding_text): Return the conversion region. (android_get_surrounding_text_internal, NATIVE_NAME): Factor out `getSurroundingText'. (takeSnapshot): New function. --- java/org/gnu/emacs/EmacsInputConnection.java | 66 +++++++++++--- java/org/gnu/emacs/EmacsNative.java | 2 + java/org/gnu/emacs/EmacsService.java | 25 +++++- java/org/gnu/emacs/EmacsView.java | 3 + src/androidterm.c | 129 +++++++++++++++++++++++++-- 5 files changed, 201 insertions(+), 24 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 1bcc9a62a81..f8dce5dfa79 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -50,6 +50,11 @@ public final class EmacsInputConnection implements InputConnection /* The handle ID associated with that view's window. */ private short windowHandle; + /* Number of batch edits currently underway. Used to avoid + synchronizing with the Emacs thread after each + `endBatchEdit'. */ + private int batchEditCount; + /* Whether or not to synchronize and call `updateIC' with the selection position after committing text. @@ -110,6 +115,10 @@ public final class EmacsInputConnection implements InputConnection Log.d (TAG, "beginBatchEdit"); EmacsNative.beginBatchEdit (windowHandle); + + /* Keep a record of the number of outstanding batch edits here as + well. */ + batchEditCount++; return true; } @@ -125,7 +134,14 @@ public final class EmacsInputConnection implements InputConnection Log.d (TAG, "endBatchEdit"); EmacsNative.endBatchEdit (windowHandle); - return true; + + /* Subtract one from the UI thread record of the number of batch + edits currently under way. */ + + if (batchEditCount > 0) + batchEditCount -= 1; + + return batchEditCount > 0; } public boolean @@ -584,21 +600,50 @@ public final class EmacsInputConnection implements InputConnection return text; } - - /* Override functions which are not implemented. */ - @Override - public Handler - getHandler () + public TextSnapshot + takeSnapshot () { - return null; + TextSnapshot snapshot; + + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + + snapshot = EmacsNative.takeSnapshot (windowHandle); + + if (EmacsService.DEBUG_IC) + Log.d (TAG, ("takeSnapshot: " + + snapshot.getSurroundingText ().getText () + + " @ " + snapshot.getCompositionEnd () + + ", " + snapshot.getCompositionStart ())); + + return snapshot; } @Override public void closeConnection () { + batchEditCount = 0; + } + + + public void + reset () + { + batchEditCount = 0; + } + + + /* Override functions which are not implemented. */ + + @Override + public Handler + getHandler () + { + return null; } @Override @@ -616,13 +661,6 @@ public final class EmacsInputConnection implements InputConnection return false; } - @Override - public TextSnapshot - takeSnapshot () - { - return null; - } - @Override public boolean clearMetaKeyStates (int states) diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 2fcbf8b94ef..9e87c419f95 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -26,6 +26,7 @@ import android.graphics.Bitmap; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextSnapshot; public final class EmacsNative { @@ -230,6 +231,7 @@ public final class EmacsNative public static native SurroundingText getSurroundingText (short window, int left, int right, int flags); + public static native TextSnapshot takeSnapshot (short window); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 96216e51cf4..2fe4e8c4146 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -767,8 +767,31 @@ public final class EmacsService extends Service public void resetIC (EmacsWindow window, int icMode) { + int oldMode; + if (DEBUG_IC) - Log.d (TAG, "resetIC: " + window); + Log.d (TAG, "resetIC: " + window + ", " + icMode); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && (oldMode = window.view.getICMode ()) == icMode + /* Don't do this if there is currently no input + connection. */ + && oldMode != IC_MODE_NULL) + { + if (DEBUG_IC) + Log.d (TAG, "resetIC: calling invalidateInput"); + + /* Android 33 and later allow the IM reset to be optimized out + and replaced by a call to `invalidateInput', which is much + faster, as it does not involve resetting the input + connection. */ + + icBeginSynchronous (); + window.view.imManager.invalidateInput (window.view); + icEndSynchronous (); + + return; + } window.view.setICMode (icMode); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 278c6025902..aba1184b0c2 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -681,6 +681,9 @@ public final class EmacsView extends ViewGroup if (inputConnection == null) inputConnection = new EmacsInputConnection (this); + else + /* Clear several pieces of state in the input connection. */ + inputConnection.reset (); /* Return the input connection. */ return inputConnection; diff --git a/src/androidterm.c b/src/androidterm.c index 191ff65199b..29076981a47 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -5668,6 +5668,10 @@ struct android_get_surrounding_text_context /* Offsets into that text. */ ptrdiff_t offset, start, end; + /* The start and end indices of the conversion region. + -1 if it does not exist. */ + ptrdiff_t conversion_start, conversion_end; + /* The window. */ android_window window; }; @@ -5706,22 +5710,47 @@ android_get_surrounding_text (void *data) request->start = request->end; request->end = temp; } + + /* Retrieve the conversion region. */ + + request->conversion_start = -1; + request->conversion_end = -1; + + if (MARKERP (f->conversion.compose_region_start)) + { + request->conversion_start + = marker_position (f->conversion.compose_region_start) - 1; + request->conversion_end + = marker_position (f->conversion.compose_region_end) - 1; + } } -JNIEXPORT jobject JNICALL -NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, - jshort window, jint before_length, - jint after_length, jint flags) -{ - JNI_STACK_ALIGNMENT_PROLOGUE; +/* Return a local reference to a `SurroundingText' object describing + WINDOW's surrounding text. ENV should be a valid JNI environment + for the current thread. - static jclass class; - static jmethodID constructor; + BEFORE_LENGTH and AFTER_LENGTH specify the number of characters + around point and mark to return. + Return the conversion region (or -1) in *CONVERSION_START and + *CONVERSION_END if non-NULL. + + Value is the object upon success, else NULL. */ + +static jobject +android_get_surrounding_text_internal (JNIEnv *env, jshort window, + jint before_length, + jint after_length, + ptrdiff_t *conversion_start, + ptrdiff_t *conversion_end) +{ struct android_get_surrounding_text_context context; jstring string; jobject object; + static jclass class; + static jmethodID constructor; + /* Initialize CLASS if it has not yet been initialized. */ if (!class) @@ -5745,7 +5774,9 @@ NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, class = (*env)->NewGlobalRef (env, class); if (!class) - return NULL; + /* Clear class to prevent a local reference from remaining in + `class'. */ + return (class = NULL); /* Now look for its constructor. */ constructor = (*env)->GetMethodID (env, class, "", @@ -5787,6 +5818,86 @@ NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, if (!object) return NULL; + /* Now return the conversion region if that was requested. */ + + if (conversion_start) + { + *conversion_start = context.conversion_start; + *conversion_end = context.conversion_start; + } + + return object; +} + +JNIEXPORT jobject JNICALL +NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject object, + jshort window, jint before_length, + jint after_length, jint flags) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + return android_get_surrounding_text_internal (env, window, before_length, + after_length, NULL, NULL); +} + +JNIEXPORT jobject JNICALL +NATIVE_NAME (takeSnapshot) (JNIEnv *env, jobject object, jshort window) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + jobject text; + ptrdiff_t start, end; + + static jclass class; + static jmethodID constructor; + + /* First, obtain the surrounding text and conversion region. */ + text = android_get_surrounding_text_internal (env, window, 600, 600, + &start, &end); + + /* If that fails, return NULL. */ + + if (!text) + return NULL; + + /* Next, initialize the TextSnapshot class. */ + + if (!class) + { + class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/TextSnapshot")); +#if __ANDROID_API__ < 33 + /* If CLASS cannot be found, the version of Android currently + running is too old. */ + + if (!class) + { + (*env)->ExceptionClear (env); + return NULL; + } +#else /* __ANDROID_API__ >= 33 */ + assert (class); +#endif /* __ANDROID_API__ < 33 */ + + class = (*env)->NewGlobalRef (env, class); + if (!class) + /* Clear class to prevent a local reference from remaining in + `class'. */ + return (class = NULL); + + constructor = (*env)->GetMethodID (env, class, "", + "(Landroid/view/inputmethod" + "/SurroundingText;III)V"); + assert (constructor); + } + + /* Try to create a TextSnapshot object. */ + eassert (start <= end); + object = (*env)->NewObject (env, class, constructor, text, + (jint) min (start, TYPE_MAXIMUM (jint)), + (jint) min (end, TYPE_MAXIMUM (jint)), + (jint) 0); return object; } -- cgit v1.3 From 75db4511704284a739c93895d7a31478b1a71181 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 6 Jul 2023 09:35:27 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsNative.java (scaledDensity): Announce new argument `scaledDensity'. * java/org/gnu/emacs/EmacsNoninteractive.java (main): Specify new argument. * java/org/gnu/emacs/EmacsService.java (onCreate): Compute an adjusted DPI for the font size based on the ratio between density and scaledDensity. (run): Specify that adjusted density. * src/android.c (setEmacsParams): Set `android_scaled_pixel_density'. * src/android.h: (android_scaled_pixel_density: New variable. * src/androidterm.c (android_term_init): Set `font_resolution'. * src/androidterm.h (struct android_display_info): New field. * src/font.c (font_pixel_size, font_find_for_lface) (font_open_for_lface, Ffont_face_attributes, Fopen_font): Use FRAME_RES instead of FRAME_RES_Y. * src/frame.h (FRAME_RES): New macro. Use this to convert between font point and pixel sizes as opposed to FRAME_RES_Y. * src/w32font.c (fill_in_logfont): * src/xfaces.c (Fx_family_fonts, set_lface_from_font): Use FRAME_RES instead of FRAME_RES_Y. (bug#64444) --- java/org/gnu/emacs/EmacsNative.java | 4 ++++ java/org/gnu/emacs/EmacsNoninteractive.java | 2 +- java/org/gnu/emacs/EmacsService.java | 5 +++++ src/android.c | 6 ++++++ src/android.h | 1 + src/androidterm.c | 5 ++--- src/androidterm.h | 5 +++++ src/font.c | 12 ++++++------ src/frame.h | 18 ++++++++++++++++-- src/w32font.c | 2 +- src/xfaces.c | 4 ++-- 11 files changed, 49 insertions(+), 15 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 9e87c419f95..5b8b0f1eae5 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -60,6 +60,9 @@ public final class EmacsNative pixelDensityX and pixelDensityY are the DPI values that will be used by Emacs. + scaledDensity is the DPI value used to translate point sizes to + pixel sizes when loading fonts. + classPath must be the classpath of this app_process process, or NULL. @@ -70,6 +73,7 @@ public final class EmacsNative String cacheDir, float pixelDensityX, float pixelDensityY, + float scaledDensity, String classPath, EmacsService emacsService); diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index aaba74d877c..aa6fa41ba97 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -190,7 +190,7 @@ public final class EmacsNoninteractive EmacsNative.setEmacsParams (assets, filesDir, libDir, cacheDir, 0.0f, - 0.0f, null, null); + 0.0f, 0.0f, null, null); /* Now find the dump file that Emacs should use, if it has already been dumped. */ diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index e1fd2dbffda..37048960f25 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -211,6 +211,7 @@ public final class EmacsService extends Service final String filesDir, libDir, cacheDir, classPath; final double pixelDensityX; final double pixelDensityY; + final double scaledDensity; SERVICE = this; handler = new Handler (Looper.getMainLooper ()); @@ -219,6 +220,9 @@ public final class EmacsService extends Service metrics = getResources ().getDisplayMetrics (); pixelDensityX = metrics.xdpi; pixelDensityY = metrics.ydpi; + scaledDensity = ((metrics.scaledDensity + / metrics.density) + * pixelDensityX); resolver = getContentResolver (); try @@ -247,6 +251,7 @@ public final class EmacsService extends Service EmacsNative.setEmacsParams (manager, filesDir, libDir, cacheDir, (float) pixelDensityX, (float) pixelDensityY, + (float) scaledDensity, classPath, EmacsService.this); } }, extraStartupArgument, diff --git a/src/android.c b/src/android.c index 2a2d134c3c8..a6bc8217820 100644 --- a/src/android.c +++ b/src/android.c @@ -198,6 +198,10 @@ char *android_class_path; /* The display's pixel densities. */ double android_pixel_density_x, android_pixel_density_y; +/* The display pixel density used to convert between point and pixel + font sizes. */ +double android_scaled_pixel_density; + /* The Android application data directory. */ static char *android_files_dir; @@ -2000,6 +2004,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject cache_dir, jfloat pixel_density_x, jfloat pixel_density_y, + jfloat scaled_density, jobject class_path, jobject emacs_service_object) { @@ -2021,6 +2026,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, android_pixel_density_x = pixel_density_x; android_pixel_density_y = pixel_density_y; + android_scaled_pixel_density = scaled_density; __android_log_print (ANDROID_LOG_INFO, __func__, "Initializing "PACKAGE_STRING"...\nPlease report bugs to " diff --git a/src/android.h b/src/android.h index 8634ba01a3d..33ca379e6ba 100644 --- a/src/android.h +++ b/src/android.h @@ -58,6 +58,7 @@ extern int android_fclose (FILE *); extern const char *android_get_home_directory (void); extern double android_pixel_density_x, android_pixel_density_y; +extern double android_scaled_pixel_density; enum android_handle_type { diff --git a/src/androidterm.c b/src/androidterm.c index aed8e24b9fa..20a8860a913 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -6295,11 +6295,10 @@ android_term_init (void) dpyinfo->color_map = color_map; #ifndef ANDROID_STUBIFY - dpyinfo->resx = android_pixel_density_x; dpyinfo->resy = android_pixel_density_y; - -#endif + dpyinfo->font_resolution = android_scaled_pixel_density; +#endif /* ANDROID_STUBIFY */ /* https://lists.gnu.org/r/emacs-devel/2015-11/msg00194.html */ dpyinfo->smallest_font_height = 1; diff --git a/src/androidterm.h b/src/androidterm.h index e3738fb2192..e75d46b1dfb 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -65,6 +65,11 @@ struct android_display_info /* DPI of the display. */ double resx, resy; + /* DPI used to convert font point sizes into pixel dimensions. + This is resy adjusted by a fixed scaling factor specified by + the user. */ + double font_resolution; + /* Scratch GC for drawing a cursor in a non-default face. */ struct android_gc *scratch_cursor_gc; diff --git a/src/font.c b/src/font.c index 9dcafb3bb33..7f8ddc4dc34 100644 --- a/src/font.c +++ b/src/font.c @@ -348,7 +348,7 @@ font_pixel_size (struct frame *f, Lisp_Object spec) if (FIXNUMP (val)) dpi = XFIXNUM (val); else - dpi = FRAME_RES_Y (f); + dpi = FRAME_RES (f); pixel_size = POINT_TO_PIXEL (point_size, dpi); return pixel_size; } @@ -3023,7 +3023,7 @@ font_find_for_lface (struct frame *f, Lisp_Object *attrs, Lisp_Object spec, int { double pt = XFIXNUM (attrs[LFACE_HEIGHT_INDEX]); - pixel_size = POINT_TO_PIXEL (pt / 10, FRAME_RES_Y (f)); + pixel_size = POINT_TO_PIXEL (pt / 10, FRAME_RES (f)); if (pixel_size < 1) pixel_size = 1; } @@ -3175,13 +3175,13 @@ font_open_for_lface (struct frame *f, Lisp_Object entity, Lisp_Object *attrs, Li } pt /= 10; - size = POINT_TO_PIXEL (pt, FRAME_RES_Y (f)); + size = POINT_TO_PIXEL (pt, FRAME_RES (f)); #ifdef HAVE_NS if (size == 0) { Lisp_Object ffsize = get_frame_param (f, Qfontsize); size = (NUMBERP (ffsize) - ? POINT_TO_PIXEL (XFLOATINT (ffsize), FRAME_RES_Y (f)) + ? POINT_TO_PIXEL (XFLOATINT (ffsize), FRAME_RES (f)) : 0); } #endif @@ -4082,7 +4082,7 @@ are to be displayed on. If omitted, the selected frame is used. */) if (FIXNUMP (val)) { Lisp_Object font_dpi = AREF (font, FONT_DPI_INDEX); - int dpi = FIXNUMP (font_dpi) ? XFIXNUM (font_dpi) : FRAME_RES_Y (f); + int dpi = FIXNUMP (font_dpi) ? XFIXNUM (font_dpi) : FRAME_RES (f); plist[n++] = QCheight; plist[n++] = make_fixnum (PIXEL_TO_POINT (XFIXNUM (val) * 10, dpi)); } @@ -4986,7 +4986,7 @@ DEFUN ("open-font", Fopen_font, Sopen_font, 1, 3, 0, { CHECK_NUMBER (size); if (FLOATP (size)) - isize = POINT_TO_PIXEL (XFLOAT_DATA (size), FRAME_RES_Y (f)); + isize = POINT_TO_PIXEL (XFLOAT_DATA (size), FRAME_RES (f)); else if (! integer_to_intmax (size, &isize)) args_out_of_range (font_entity, size); if (! (INT_MIN <= isize && isize <= INT_MAX)) diff --git a/src/frame.h b/src/frame.h index 8ed9c0f37d8..62fc63c7c1f 100644 --- a/src/frame.h +++ b/src/frame.h @@ -981,12 +981,26 @@ default_pixels_per_inch_y (void) #define FRAME_RES_Y(f) \ (eassert (FRAME_WINDOW_P (f)), FRAME_DISPLAY_INFO (f)->resy) +#ifdef HAVE_ANDROID + +/* Android systems use a font scaling factor independent from the + display DPI. */ + +#define FRAME_RES(f) \ + (eassert (FRAME_WINDOW_P (f)), \ + FRAME_DISPLAY_INFO (f)->font_resolution) + +#else /* !HAVE_ANDROID */ +#define FRAME_RES(f) (FRAME_RES_Y (f)) +#endif /* HAVE_ANDROID */ + #else /* !HAVE_WINDOW_SYSTEM */ /* Defaults when no window system available. */ -#define FRAME_RES_X(f) default_pixels_per_inch_x () -#define FRAME_RES_Y(f) default_pixels_per_inch_y () +#define FRAME_RES_X(f) default_pixels_per_inch_x () +#define FRAME_RES_Y(f) default_pixels_per_inch_y () +#define FRAME_RES(f) default_pixels_per_inch_y () #endif /* HAVE_WINDOW_SYSTEM */ diff --git a/src/w32font.c b/src/w32font.c index 2917fa55f9f..0371b24e1d1 100644 --- a/src/w32font.c +++ b/src/w32font.c @@ -2031,7 +2031,7 @@ static void fill_in_logfont (struct frame *f, LOGFONT *logfont, Lisp_Object font_spec) { Lisp_Object tmp, extra; - int dpi = FRAME_RES_Y (f); + int dpi = FRAME_RES (f); tmp = AREF (font_spec, FONT_DPI_INDEX); if (FIXNUMP (tmp)) diff --git a/src/xfaces.c b/src/xfaces.c index af3428ad995..30487c0e8fb 100644 --- a/src/xfaces.c +++ b/src/xfaces.c @@ -1612,7 +1612,7 @@ the face font sort order, see `face-font-selection-order'. */) { Lisp_Object font = AREF (vec, i); int point = PIXEL_TO_POINT (XFIXNUM (AREF (font, FONT_SIZE_INDEX)) * 10, - FRAME_RES_Y (f)); + FRAME_RES (f)); Lisp_Object spacing = Ffont_get (font, QCspacing); Lisp_Object v = CALLN (Fvector, AREF (font, FONT_FAMILY_INDEX), @@ -2173,7 +2173,7 @@ set_lface_from_font (struct frame *f, Lisp_Object lface, if (force_p || UNSPECIFIEDP (LFACE_HEIGHT (lface))) { - int pt = PIXEL_TO_POINT (font->pixel_size * 10, FRAME_RES_Y (f)); + int pt = PIXEL_TO_POINT (font->pixel_size * 10, FRAME_RES (f)); eassert (pt > 0); ASET (lface, LFACE_HEIGHT_INDEX, make_fixnum (pt)); -- cgit v1.3 From ae9f1a075c3a5f5bd0425828b6144f97265d8794 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 13 Jul 2023 18:17:59 +0800 Subject: Improve workaround for partial texture updates on Android * java/AndroidManifest.xml.in: * java/org/gnu/emacs/EmacsDialog.java (toAlertDialog): Don't change hardware acceleration state. * java/org/gnu/emacs/EmacsNative.java (notifyPixelsChanged): New function. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): New field `bitmapChanged'. (copyToFrontBuffer): Signal that the bitmap has changed. (onDraw): If the bitmap has changed, increment the generation ID. * src/android.c (JNICALL): Implement new function. --- java/AndroidManifest.xml.in | 5 --- java/org/gnu/emacs/EmacsDialog.java | 11 ------ java/org/gnu/emacs/EmacsNative.java | 5 +++ java/org/gnu/emacs/EmacsSurfaceView.java | 61 +++++++++++++++++++++----------- src/android.c | 17 +++++++++ 5 files changed, 63 insertions(+), 36 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index f2aede7369c..e79fb4e46e7 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -77,14 +77,10 @@ along with GNU Emacs. If not, see . --> @ANDROID_SHARED_USER_ID@ android:extractNativeLibs="true"> - - @@ -177,7 +173,6 @@ along with GNU Emacs. If not, see . --> = Build.VERSION_CODES.HONEYCOMB) - { - flag = WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; - window = dialog.getWindow (); - window.addFlags (flag); - } - return dialog; } diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 5b8b0f1eae5..1331539879a 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -249,6 +249,11 @@ public final class EmacsNative public static native void blitRect (Bitmap src, Bitmap dest, int x1, int y1, int x2, int y2); + /* Increment the generation ID of the specified BITMAP, forcing its + texture to be re-uploaded to the GPU. */ + + public static native void notifyPixelsChanged (Bitmap bitmap); + static { /* Older versions of Android cannot link correctly with shared diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 54fe70e1634..c47696b35c0 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -42,6 +42,10 @@ public final class EmacsSurfaceView extends View /* The complete buffer contents at the time of the last draw. */ private Bitmap frontBuffer; + /* Whether frontBuffer has been updated since the last call to + `onDraw'. */ + private boolean bitmapChanged; + /* Canvas representing the front buffer. */ private Canvas bitmapCanvas; @@ -105,6 +109,9 @@ public final class EmacsSurfaceView extends View bitmap.getWidth (), bitmap.getHeight ()); } + + /* See the large comment inside `onDraw'. */ + bitmapChanged = true; } private void @@ -176,27 +183,41 @@ public final class EmacsSurfaceView extends View onDraw (Canvas canvas) { /* Paint the view's bitmap; the bitmap might be recycled right - now. - - Hardware acceleration is disabled in AndroidManifest.xml to - prevent Android from uploading the front buffer to the GPU from - a separate thread. This is important for two reasons: first, - the GPU command queue uses a massive amount of memory (dozens - of MiB) to upload bitmaps to the GPU, regardless of how much of - the bitmap has actually changed. - - Secondly, asynchronous texturization leads to race conditions - when a buffer swap occurs before the front buffer is fully - uploaded to the GPU. Normally, only slight and tolerable - tearing should result from this behavior, but Android does not - properly interlock the ``generation ID'' used to avoid - texturizing unchanged bitmaps with the bitmap contents, - consequentially leading to textures in an incomplete state - remaining in use to the GPU if a buffer swap happens between - the image data being uploaded and the ``generation ID'' being - read. */ + now. */ if (frontBuffer != null) - canvas.drawBitmap (frontBuffer, 0f, 0f, uiThreadPaint); + { + /* The first time the bitmap is drawn after a buffer swap, + mark its contents as having changed. This increments the + ``generation ID'' used by Android to avoid uploading buffer + textures for unchanged bitmaps. + + When a buffer swap takes place, the bitmap is initially + updated from the Emacs thread, resulting in the generation + ID being increased. If the render thread is texturizing + the bitmap while the swap takes place, it might record the + generation ID after the update for a texture containing the + contents of the bitmap prior to the swap, leaving the + texture tied to the bitmap partially updated. + + Android never calls `onDraw' if the render thread is still + processing the bitmap. Update the generation ID here to + ensure that a new texture will be uploaded if the bitmap + has changed. + + Uploading the bitmap contents to the GPU uses an excessive + amount of memory, as the entire bitmap is placed into the + graphics command queue, but this memory is actually shared + among all other applications and reclaimed by the system + when necessary. */ + + if (bitmapChanged) + { + EmacsNative.notifyPixelsChanged (frontBuffer); + bitmapChanged = false; + } + + canvas.drawBitmap (frontBuffer, 0f, 0f, uiThreadPaint); + } } }; diff --git a/src/android.c b/src/android.c index 5eb6f65419c..f8ad78a2b39 100644 --- a/src/android.c +++ b/src/android.c @@ -3133,6 +3133,23 @@ NATIVE_NAME (blitRect) (JNIEnv *env, jobject object, AndroidBitmap_unlockPixels (env, src); } +JNIEXPORT void JNICALL +NATIVE_NAME (notifyPixelsChanged) (JNIEnv *env, jobject object, + jobject bitmap) +{ + void *data; + + /* Lock and unlock the bitmap. This calls + SkBitmap->notifyPixelsChanged. */ + + if (AndroidBitmap_lockPixels (env, bitmap, &data) < 0) + /* The return value is less than 0 if an error occurs. + Good luck finding this in the documentation. */ + return; + + AndroidBitmap_unlockPixels (env, bitmap); +} + /* Forward declarations of deadlock prevention functions. */ static void android_begin_query (void); -- cgit v1.3 From 5ff31bf36cf00f9d5a378ce139fd5c2ae8d3f25e Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 18 Jul 2023 10:12:40 +0800 Subject: Update Android port * doc/lispref/commands.texi (Touchscreen Events): Describe treatment of canceled touch sequences during touch event translation. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Update JNI prototypes. * java/org/gnu/emacs/EmacsWindow.java (motionEvent): Set cancelation flag in events sent where appropriate. * lisp/touch-screen.el (touch-screen-handle-point-update): Improve treatment of horizontal scrolling near window edges. (touch-screen-handle-touch): Don't handle point up if the touch sequence has been canceled. * src/android.c (sendTouchDown, sendTouchUp, sendTouchMove): New argument `flags'. * src/androidgui.h (enum android_touch_event_flags): New enum. (struct android_touch_event): New field `flags'. * src/androidterm.c (handle_one_android_event): Report cancelation in TOUCHSCREEN_END_EVENTs. * src/keyboard.c (make_lispy_event): Fix botched merge. --- doc/lispref/commands.texi | 23 ++++---- java/org/gnu/emacs/EmacsNative.java | 9 ++-- java/org/gnu/emacs/EmacsWindow.java | 19 ++++--- lisp/touch-screen.el | 27 ++++++++-- src/android.c | 12 +++-- src/androidgui.h | 10 ++++ src/androidterm.c | 5 ++ src/keyboard.c | 103 +++++++++++++++++++++++++----------- 8 files changed, 152 insertions(+), 56 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index ffb01254fc9..55fecdce2d7 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2061,9 +2061,11 @@ keymaps that are active at the location of the of a single @code{down-mouse-1} event, with subsequent @code{touchscreen-update} events translated to mouse motion events (@pxref{Motion Events}), and a final @code{touchscreen-end} event -translated to a @code{mouse-1} or @code{drag-mouse-1} event. This is -referred to ``simple translation'', and produces a simple -correspondence between touchpoint motion and mouse motion. +translated to a @code{mouse-1} or @code{drag-mouse-1} event (unless +the @code{touchscreen-end} event indicates that the touch sequence has +been intercepted by another program.) This is referred to ``simple +translation'', and produces a simple correspondence between touchpoint +motion and mouse motion. @cindex @code{ignored-mouse-command}, a symbol property However, some commands bound to @@ -2078,13 +2080,14 @@ takes place: here, Emacs processes touch screen gestures (@pxref{Touchscreens,,, emacs, The GNU Emacs Manual}) first, and finally attempts to translate touch screen events into mouse events if no gesture was detected prior to a closing @code{touchscreen-end} -event and a command is bound to @code{mouse-1} at the location of that -event. Before generating the @code{mouse-1} event, point is also set -to the location of the @code{touchscreen-end} event, and the window -containing the position of that event is selected, as a compromise for -packages which assume @code{mouse-drag-region} has already set point -to the location of any mouse click and selected the window where it -took place. +event (with its @var{canceled} parameter @code{nil}, as with simple +translation) and a command is bound to @code{mouse-1} at the location +of that event. Before generating the @code{mouse-1} event, point is +also set to the location of the @code{touchscreen-end} event, and the +window containing the position of that event is selected, as a +compromise for packages which assume @code{mouse-drag-region} has +already set point to the location of any mouse click and selected the +window where it took place. To prevent unwanted @code{mouse-1} events arriving after a mouse menu is dismissed (@pxref{Mouse Menus}), Emacs also avoids simple diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 1331539879a..d4d502ede5a 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -142,15 +142,18 @@ public final class EmacsNative /* Send an ANDROID_TOUCH_DOWN event. */ public static native long sendTouchDown (short window, int x, int y, - long time, int pointerID); + long time, int pointerID, + int flags); /* Send an ANDROID_TOUCH_UP event. */ public static native long sendTouchUp (short window, int x, int y, - long time, int pointerID); + long time, int pointerID, + int flags); /* Send an ANDROID_TOUCH_MOVE event. */ public static native long sendTouchMove (short window, int x, int y, - long time, int pointerID); + long time, int pointerID, + int flags); /* Send an ANDROID_WHEEL event. */ public static native long sendWheel (short window, int x, int y, diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 0e96a8382d0..8118479319e 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -1025,23 +1025,30 @@ public final class EmacsWindow extends EmacsHandleObject /* Touch down event. */ EmacsNative.sendTouchDown (this.handle, coordinate.x, coordinate.y, time, - coordinate.id); + coordinate.id, 0); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: + /* Touch up event. */ + EmacsNative.sendTouchUp (this.handle, coordinate.x, + coordinate.y, time, + coordinate.id, 0); + break; + case MotionEvent.ACTION_CANCEL: - /* Touch up event. Android documentation says ACTION_CANCEL - should be treated as more or less equivalent to ACTION_UP, - so that is what is done here. */ + /* Touch sequence cancellation event. */ EmacsNative.sendTouchUp (this.handle, coordinate.x, - coordinate.y, time, coordinate.id); + coordinate.y, time, + coordinate.id, + 1 /* ANDROID_TOUCH_SEQUENCE_CANCELED */); break; case MotionEvent.ACTION_MOVE: /* Pointer motion event. */ EmacsNative.sendTouchMove (this.handle, coordinate.x, - coordinate.y, time, coordinate.id); + coordinate.y, time, + coordinate.id, 0); break; } } diff --git a/lisp/touch-screen.el b/lisp/touch-screen.el index 1ef66d0043f..8f10bc3e794 100644 --- a/lisp/touch-screen.el +++ b/lisp/touch-screen.el @@ -548,11 +548,26 @@ then move point to the position of POINT." ;; WINDOW. (relative-xy (touch-screen-relative-xy posn window)) + (col (and (eq (posn-area posn) 'text-area) + (car (posn-col-row posn + (posn-window posn))))) + ;; Don't start horizontal scrolling if the touch + ;; point originated within two columns of the window + ;; edges, as systems like Android use those two + ;; columns to implement gesture navigation. + (diff-x-eligible + (and col (> (car col) 2) + (< (car col) (- (window-width window) 2)))) (diff-x (- (car last-posn) (car relative-xy))) (diff-y (- (cdr last-posn) (cdr relative-xy)))) - ;; Decide whether or not to start scrolling. - (when (or (> diff-y 10) (> diff-x 10) - (< diff-y -10) (< diff-x -10)) + ;; Decide whether or not to start scrolling. Make the + ;; hscrolling threshold slightly larger than the vertical + ;; scrolling threshold, to compensate better for + ;; Android-style gesture navigation. + (when (or (> diff-y 10) (and diff-x-eligible + (> diff-x 20)) + (< diff-y -10) (and diff-x-eligible + (< diff-x -20))) (setcar (nthcdr 3 touch-screen-current-tool) 'scroll) (setcar (nthcdr 2 touch-screen-current-tool) @@ -852,7 +867,11 @@ the place of EVENT within the key sequence being translated, or (cancel-timer touch-screen-current-timer) (setq touch-screen-current-timer nil)) (unwind-protect - (touch-screen-handle-point-up (cadr event) prefix) + ;; Don't perform any actions associated with releasing the + ;; tool if the touch sequence was intercepted by another + ;; program. + (unless (caddr event) + (touch-screen-handle-point-up (cadr event) prefix)) ;; Make sure the tool list is cleared even if ;; `touch-screen-handle-point-up' throws. (setq touch-screen-current-tool nil))) diff --git a/src/android.c b/src/android.c index 90288737c77..f2e5e75d35e 100644 --- a/src/android.c +++ b/src/android.c @@ -2870,7 +2870,8 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, jshort window, jint x, jint y, - jlong time, jint pointer_id) + jlong time, jint pointer_id, + jint flags) { JNI_STACK_ALIGNMENT_PROLOGUE; @@ -2883,6 +2884,7 @@ NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, event.touch.y = y; event.touch.time = time; event.touch.pointer_id = pointer_id; + event.touch.flags = flags; android_write_event (&event); return event_serial; @@ -2891,7 +2893,8 @@ NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, jshort window, jint x, jint y, - jlong time, jint pointer_id) + jlong time, jint pointer_id, + jint flags) { JNI_STACK_ALIGNMENT_PROLOGUE; @@ -2904,6 +2907,7 @@ NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, event.touch.y = y; event.touch.time = time; event.touch.pointer_id = pointer_id; + event.touch.flags = flags; android_write_event (&event); return event_serial; @@ -2912,7 +2916,8 @@ NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, jshort window, jint x, jint y, - jlong time, jint pointer_id) + jlong time, jint pointer_id, + jint flags) { JNI_STACK_ALIGNMENT_PROLOGUE; @@ -2925,6 +2930,7 @@ NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, event.touch.y = y; event.touch.time = time; event.touch.pointer_id = pointer_id; + event.touch.flags = flags; android_write_event (&event); return event_serial; diff --git a/src/androidgui.h b/src/androidgui.h index 9e604cdcb8c..265ec29b678 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -365,6 +365,13 @@ struct android_expose_event int width, height; }; +enum android_touch_event_flags + { + /* This touch sequence has been intercepted by the WM (probably + for back gesture navigation or some such.) */ + ANDROID_TOUCH_SEQUENCE_CANCELED = 1, + }; + struct android_touch_event { /* Type of the event. */ @@ -384,6 +391,9 @@ struct android_touch_event /* Index of the pointer being tracked. */ unsigned int pointer_id; + + /* Flags associated with this event. */ + int flags; }; struct android_wheel_event diff --git a/src/androidterm.c b/src/androidterm.c index 27800a61864..bcb6cd6db45 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -1511,6 +1511,11 @@ handle_one_android_event (struct android_display_info *dpyinfo, inev.ie.kind = TOUCHSCREEN_END_EVENT; inev.ie.timestamp = event->touch.time; + /* Report whether the sequence has been canceled. */ + + if (event->touch.flags & ANDROID_TOUCH_SEQUENCE_CANCELED) + inev.ie.modifiers = 1; + XSETFRAME (inev.ie.frame_or_window, any); XSETINT (inev.ie.x, event->touch.x); XSETINT (inev.ie.y, event->touch.y); diff --git a/src/keyboard.c b/src/keyboard.c index bd7433e584a..73c4e3f2593 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -6560,11 +6560,10 @@ make_lispy_event (struct input_event *event) { Lisp_Object x, y, id, position; struct frame *f; +#ifdef HAVE_WINDOW_SYSTEM int tab_bar_item; bool close; -#if defined HAVE_WINDOW_SYSTEM && !defined HAVE_EXT_MENU_BAR - int column, row, dummy; -#endif /* defined HAVE_WINDOW_SYSTEM && !defined HAVE_EXT_MENU_BAR */ +#endif /* HAVE_WINDOW_SYSTEM */ f = XFRAME (event->frame_or_window); id = event->arg; @@ -6572,16 +6571,82 @@ make_lispy_event (struct input_event *event) y = event->y; #if defined HAVE_WINDOW_SYSTEM && !defined HAVE_EXT_MENU_BAR - if (event->kind == TOUCHSCREEN_BEGIN_EVENT - && coords_in_menu_bar_window (f, XFIXNUM (x), XFIXNUM (y))) + if (coords_in_menu_bar_window (f, XFIXNUM (x), XFIXNUM (y))) { /* If the tap began in the menu bar window, then save the id. */ menu_bar_touch_id = id; return Qnil; } - else if (event->kind == TOUCHSCREEN_END_EVENT - && EQ (menu_bar_touch_id, id)) +#endif /* defined HAVE_WINDOW_SYSTEM && !defined HAVE_EXT_MENU_BAR */ + + position = make_lispy_position (f, x, y, event->timestamp); + +#ifdef HAVE_WINDOW_SYSTEM + + /* Now check if POSITION lies on the tab bar. If so, look up + the corresponding tab bar item's propertized string as the + OBJECT. */ + + if (coords_in_tab_bar_window (f, XFIXNUM (event->x), + XFIXNUM (event->y)) + /* `get_tab_bar_item_kbd' returns 0 if the item was + previously highlighted, 1 otherwise, and -1 if there is + no tab bar item. */ + && get_tab_bar_item_kbd (f, XFIXNUM (event->x), + XFIXNUM (event->y), &tab_bar_item, + &close) >= 0) + { + /* First, obtain the propertized string. */ + x = Fcopy_sequence (AREF (f->tab_bar_items, + (tab_bar_item + + TAB_BAR_ITEM_CAPTION))); + + /* Next, add the key binding. */ + AUTO_LIST2 (y, Qmenu_item, list3 (AREF (f->tab_bar_items, + (tab_bar_item + + TAB_BAR_ITEM_KEY)), + AREF (f->tab_bar_items, + (tab_bar_item + + TAB_BAR_ITEM_BINDING)), + close ? Qt : Qnil)); + + /* And add the new properties to the propertized string. */ + Fadd_text_properties (make_fixnum (0), + make_fixnum (SCHARS (x)), + y, x); + + /* Set the position to 0. */ + x = Fcons (x, make_fixnum (0)); + + /* Finally, add the OBJECT. */ + position = nconc2 (position, Fcons (x, Qnil)); + } + +#endif /* HAVE_WINDOW_SYSTEM */ + + return list2 (Qtouchscreen_begin, + Fcons (id, position)); + } + + case TOUCHSCREEN_END_EVENT: + { + Lisp_Object x, y, id, position; + struct frame *f = XFRAME (event->frame_or_window); +#if defined HAVE_WINDOW_SYSTEM && !defined HAVE_EXT_MENU_BAR + int column, row, dummy; +#endif /* defined HAVE_WINDOW_SYSTEM && !defined HAVE_EXT_MENU_BAR */ +#ifdef HAVE_WINDOW_SYSTEM + int tab_bar_item; + bool close; +#endif /* HAVE_WINDOW_SYSTEM */ + + id = event->arg; + x = event->x; + y = event->y; + +#if defined HAVE_WINDOW_SYSTEM && !defined HAVE_EXT_MENU_BAR + if (EQ (menu_bar_touch_id, id)) { /* This touch should activate the menu bar. Generate the menu bar event. */ @@ -6631,8 +6696,6 @@ make_lispy_event (struct input_event *event) } #endif /* defined HAVE_WINDOW_SYSTEM && !defined HAVE_EXT_MENU_BAR */ - position = make_lispy_position (f, x, y, event->timestamp); - #ifdef HAVE_WINDOW_SYSTEM /* Now check if POSITION lies on the tab bar. If so, look up @@ -6676,29 +6739,9 @@ make_lispy_event (struct input_event *event) #endif /* HAVE_WINDOW_SYSTEM */ - return list2 (((event->kind - == TOUCHSCREEN_BEGIN_EVENT) - ? Qtouchscreen_begin - : Qtouchscreen_end), - Fcons (id, position)); - } - - case TOUCHSCREEN_END_EVENT: - { - Lisp_Object x, y, id, position; - struct frame *f = XFRAME (event->frame_or_window); - - id = event->arg; - x = event->x; - y = event->y; - position = make_lispy_position (f, x, y, event->timestamp); - return list3 (((event->kind - == TOUCHSCREEN_BEGIN_EVENT) - ? Qtouchscreen_begin - : Qtouchscreen_end), - Fcons (id, position), + return list3 (Qtouchscreen_end, Fcons (id, position), event->modifiers ? Qt : Qnil); } -- cgit v1.3 From 0709e03f88cdef8f785338cab9315b527db0854e Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 28 Jul 2023 15:19:37 +0800 Subject: Allow quitting from Android content provider operations * doc/emacs/android.texi (Android Document Providers): Say that quitting is now possible. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New functions `safSyncAndReadInput', `safync' and `safPostRequest'. * java/org/gnu/emacs/EmacsSafThread.java: New file. Move cancel-able SAF operations here. * java/org/gnu/emacs/EmacsService.java (EmacsService): Allow quitting from most SAF operations. * src/androidvfs.c (android_saf_exception_check): Return EINTR if OperationCanceledException is received. (android_saf_stat, android_saf_access) (android_document_id_from_name, android_saf_tree_opendir_1) (android_saf_file_open): Don't allow reentrant calls from async input handlers. (NATIVE_NAME): Implement new synchronization primitives for JNI. (android_vfs_init): Initialize new class. * src/dired.c (open_directory): Handle EINTR from opendir. * src/sysdep.c: Describe which operations may return EINTR on Android. --- doc/emacs/android.texi | 8 +- java/org/gnu/emacs/EmacsNative.java | 17 + java/org/gnu/emacs/EmacsSafThread.java | 922 +++++++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsService.java | 520 ++----------------- src/androidvfs.c | 132 ++++- src/dired.c | 9 + src/sysdep.c | 2 +- 7 files changed, 1127 insertions(+), 483 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsSafThread.java (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index b86c71cea49..0330e9b5890 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -305,14 +305,10 @@ file-system. In addition, although Emacs can normally write and create files inside these directories, it cannot create symlinks or hard links. -@c TODO: fix this! Since document providers are allowed to perform expensive network operations to obtain file contents, a file access operation within one -of these directories will possibly take a significant amount of time. -Emacs presently does not support quitting out of such file system -operations, and the timeouts applied are fully subject to the -discretion of the system and the document provider that is responding -to these operations. +of these directories has the potential to take a significant amount of +time. @node Android Environment @section Running Emacs under Android diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index d4d502ede5a..ea200037218 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -257,6 +257,23 @@ public final class EmacsNative public static native void notifyPixelsChanged (Bitmap bitmap); + + /* Functions used to synchronize document provider access with the + main thread. */ + + /* Wait for a call to `safPostRequest' while also reading async + input. + + If asynchronous input arrives and sets Vquit_flag, return 1. */ + public static native int safSyncAndReadInput (); + + /* Wait for a call to `safPostRequest'. */ + public static native void safSync (); + + /* Post the semaphore used to await the completion of SAF + operations. */ + public static native void safPostRequest (); + static { /* Older versions of Android cannot link correctly with shared diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java new file mode 100644 index 00000000000..fd06603fab3 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -0,0 +1,922 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + + Copyright (C) 2023 Free Software Foundation, Inc. + + This file is part of GNU Emacs. + + GNU Emacs is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or (at + your option) any later version. + + GNU Emacs is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; + +import android.os.Build; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.ParcelFileDescriptor; + +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; + + + +/* Emacs runs long-running SAF operations on a second thread running + its own handler. These operations include opening files and + maintaining the path to document ID cache. + +#if 0 + Because Emacs paths are based on file display names, while Android + document identifiers have no discernible hierarchy of their own, + each file name lookup must carry out a repeated search for + directory documents with the names of all of the file name's + constituent components, where each iteration searches within the + directory document identified by the previous iteration. + + A time limited cache tying components to document IDs is maintained + in order to speed up consecutive searches for file names sharing + the same components. Since listening for changes to each document + in the cache is prohibitively expensive, Emacs instead elects to + periodically remove entries that are older than a predetermined + amount of a time. + + The cache is structured much like the directory trees whose + information it records, with each entry in the cache containing a + list of entries for their children. File name lookup consults the + cache and populates it with missing information simultaneously. + + This is not yet implemented. +#endif + + Long-running operations are also run on this thread for another + reason: Android uses special cancellation objects to terminate + ongoing IPC operations. However, the functions that perform these + operations block instead of providing mechanisms for the caller to + wait for their completion while also reading async input, as a + consequence of which the calling thread is unable to signal the + cancellation objects that it provides. Performing the blocking + operations in this auxiliary thread enables the main thread to wait + for completion itself, signaling the cancellation objects when it + deems necessary. */ + + + +public final class EmacsSafThread extends HandlerThread +{ + /* The content resolver used by this thread. */ + private final ContentResolver resolver; + + /* Handler for this thread's main loop. */ + private Handler handler; + + /* File access mode constants. See `man 7 inode'. */ + public static final int S_IRUSR = 0000400; + public static final int S_IWUSR = 0000200; + public static final int S_IFCHR = 0020000; + public static final int S_IFDIR = 0040000; + public static final int S_IFREG = 0100000; + + public + EmacsSafThread (ContentResolver resolver) + { + super ("Document provider access thread"); + this.resolver = resolver; + } + + + + @Override + public void + start () + { + super.start (); + + /* Set up the handler after the thread starts. */ + handler = new Handler (getLooper ()); + } + + + + /* ``Prototypes'' for nested functions that are run within the SAF + thread and accepts a cancellation signal. They differ in their + return types. */ + + private abstract class SafIntFunction + { + /* The ``throws Throwable'' here is a Java idiosyncracy that tells + the compiler to allow arbitrary error objects to be signaled + from within this function. + + Later, runIntFunction will try to re-throw any error object + generated by this function in the Emacs thread, using a trick + to avoid the compiler requirement to expressly declare that an + error (and which types of errors) will be signaled. */ + + public abstract int runInt (CancellationSignal signal) + throws Throwable; + }; + + private abstract class SafObjectFunction + { + /* The ``throws Throwable'' here is a Java idiosyncracy that tells + the compiler to allow arbitrary error objects to be signaled + from within this function. + + Later, runObjectFunction will try to re-throw any error object + generated by this function in the Emacs thread, using a trick + to avoid the compiler requirement to expressly declare that an + error (and which types of errors) will be signaled. */ + + public abstract Object runObject (CancellationSignal signal) + throws Throwable; + }; + + + + /* Functions that run cancel-able queries. These functions are + internally run within the SAF thread. */ + + /* Throw the specified EXCEPTION. The type template T is erased by + the compiler before the object is compiled, so the compiled code + simply throws EXCEPTION without the cast being verified. + + T should be RuntimeException to obtain the desired effect of + throwing an exception without a compiler check. */ + + @SuppressWarnings("unchecked") + private static void + throwException (Throwable exception) + throws T + { + throw (T) exception; + } + + /* Run the given function (or rather, its `runInt' field) within the + SAF thread, waiting for it to complete. + + If async input arrives in the meantime and sets Vquit_flag, + signal the cancellation signal supplied to that function. + + Rethrow any exception thrown from that function, and return its + value otherwise. */ + + private int + runIntFunction (final SafIntFunction function) + { + final EmacsHolder result; + final CancellationSignal signal; + Throwable throwable; + + result = new EmacsHolder (); + signal = new CancellationSignal (); + + handler.post (new Runnable () { + @Override + public void + run () + { + try + { + result.thing + = Integer.valueOf (function.runInt (signal)); + } + catch (Throwable throwable) + { + result.thing = throwable; + } + + EmacsNative.safPostRequest (); + } + }); + + if (EmacsNative.safSyncAndReadInput () != 0) + { + signal.cancel (); + + /* Now wait for the function to finish. Either the signal has + arrived after the query took place, in which case it will + finish normally, or an OperationCanceledException will be + thrown. */ + + EmacsNative.safSync (); + } + + if (result.thing instanceof Throwable) + { + throwable = (Throwable) result.thing; + EmacsSafThread.throwException (throwable); + } + + return (Integer) result.thing; + } + + /* Run the given function (or rather, its `runObject' field) within + the SAF thread, waiting for it to complete. + + If async input arrives in the meantime and sets Vquit_flag, + signal the cancellation signal supplied to that function. + + Rethrow any exception thrown from that function, and return its + value otherwise. */ + + private Object + runObjectFunction (final SafObjectFunction function) + { + final EmacsHolder result; + final CancellationSignal signal; + Throwable throwable; + + result = new EmacsHolder (); + signal = new CancellationSignal (); + + handler.post (new Runnable () { + @Override + public void + run () + { + try + { + result.thing = function.runObject (signal); + } + catch (Throwable throwable) + { + result.thing = throwable; + } + + EmacsNative.safPostRequest (); + } + }); + + if (EmacsNative.safSyncAndReadInput () != 0) + { + signal.cancel (); + + /* Now wait for the function to finish. Either the signal has + arrived after the query took place, in which case it will + finish normally, or an OperationCanceledException will be + thrown. */ + + EmacsNative.safSync (); + } + + if (result.thing instanceof Throwable) + { + throwable = (Throwable) result.thing; + EmacsSafThread.throwException (throwable); + } + + return result.thing; + } + + /* The crux of `documentIdFromName1', run within the SAF thread. + SIGNAL should be a cancellation signal run upon quitting. */ + + private int + documentIdFromName1 (String tree_uri, String name, + String[] id_return, CancellationSignal signal) + { + Uri uri, treeUri; + String id, type; + String[] components, projection; + Cursor cursor; + int column; + + projection = new String[] { + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_MIME_TYPE, + }; + + /* Parse the URI identifying the tree first. */ + uri = Uri.parse (tree_uri); + + /* Now, split NAME into its individual components. */ + components = name.split ("/"); + + /* Set id and type to the value at the root of the tree. */ + type = id = null; + cursor = null; + + /* For each component... */ + + try + { + for (String component : components) + { + /* Java split doesn't behave very much like strtok when it + comes to trailing and leading delimiters... */ + if (component.isEmpty ()) + continue; + + /* Create the tree URI for URI from ID if it exists, or + the root otherwise. */ + + if (id == null) + id = DocumentsContract.getTreeDocumentId (uri); + + treeUri + = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id); + + /* Look for a file in this directory by the name of + component. */ + + cursor = resolver.query (treeUri, projection, + (Document.COLUMN_DISPLAY_NAME + + " = ?s"), + new String[] { component, }, + null, signal); + + if (cursor == null) + return -1; + + while (true) + { + /* Even though the query selects for a specific + display name, some content providers nevertheless + return every file within the directory. */ + + if (!cursor.moveToNext ()) + { + /* If the last component considered is a + directory... */ + if ((type == null + || type.equals (Document.MIME_TYPE_DIR)) + /* ... and type and id currently represent the + penultimate component. */ + && component == components[components.length - 1]) + { + /* The cursor is empty. In this case, return + -2 and the current document ID (belonging + to the previous component) in + ID_RETURN. */ + + id_return[0] = id; + + /* But return -1 on the off chance that id is + null. */ + + if (id == null) + return -1; + + return -2; + } + + /* The last component found is not a directory, so + return -1. */ + return -1; + } + + /* So move CURSOR to a row with the right display + name. */ + + column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); + + if (column < 0) + continue; + + name = cursor.getString (column); + + /* Break out of the loop only once a matching + component is found. */ + + if (name.equals (component)) + break; + } + + /* Look for a column by the name of + COLUMN_DOCUMENT_ID. */ + + column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); + + if (column < 0) + return -1; + + /* Now replace ID with the document ID. */ + + id = cursor.getString (column); + + /* If this is the last component, be sure to initialize + the document type. */ + + if (component == components[components.length - 1]) + { + column + = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + + if (column < 0) + return -1; + + type = cursor.getString (column); + + /* Type may be NULL depending on how the Cursor + returned is implemented. */ + + if (type == null) + return -1; + } + + /* Now close the cursor. */ + cursor.close (); + cursor = null; + + /* ID may have become NULL if the data is in an invalid + format. */ + if (id == null) + return -1; + } + } + finally + { + /* If an error is thrown within the block above, let + android_saf_exception_check handle it, but make sure the + cursor is closed. */ + + if (cursor != null) + cursor.close (); + } + + /* Here, id is either NULL (meaning the same as TREE_URI), and + type is either NULL (in which case id should also be NULL) or + the MIME type of the file. */ + + /* First return the ID. */ + + if (id == null) + id_return[0] = DocumentsContract.getTreeDocumentId (uri); + else + id_return[0] = id; + + /* Next, return whether or not this is a directory. */ + if (type == null || type.equals (Document.MIME_TYPE_DIR)) + return 1; + + return 0; + } + + /* Find the document ID of the file within TREE_URI designated by + NAME. + + NAME is a ``file name'' comprised of the display names of + individual files. Each constituent component prior to the last + must name a directory file within TREE_URI. + + Upon success, return 0 or 1 (contingent upon whether or not the + last component within NAME is a directory) and place the document + ID of the named file in ID_RETURN[0]. + + If the designated file can't be located, but each component of + NAME up to the last component can and is a directory, return -2 + and the ID of the last component located in ID_RETURN[0]. + + If the designated file can't be located, return -1, or signal one + of OperationCanceledException, SecurityException, + FileNotFoundException, or UnsupportedOperationException. */ + + public int + documentIdFromName (final String tree_uri, final String name, + final String[] id_return) + { + return runIntFunction (new SafIntFunction () { + @Override + public int + runInt (CancellationSignal signal) + { + return documentIdFromName1 (tree_uri, name, id_return, + signal); + } + }); + } + + /* The bulk of `statDocument'. SIGNAL should be a cancelation + signal. */ + + private long[] + statDocument1 (String uri, String documentId, + CancellationSignal signal) + { + Uri uriObject; + String[] projection; + long[] stat; + int index; + long tem; + String tem1; + Cursor cursor; + + uriObject = Uri.parse (uri); + + if (documentId == null) + documentId = DocumentsContract.getTreeDocumentId (uriObject); + + /* Create a document URI representing DOCUMENTID within URI's + authority. */ + + uriObject + = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); + + /* Now stat this document. */ + + projection = new String[] { + Document.COLUMN_FLAGS, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_SIZE, + }; + + cursor = resolver.query (uriObject, projection, null, + null, null, signal); + + if (cursor == null) + return null; + + if (!cursor.moveToFirst ()) + { + cursor.close (); + return null; + } + + /* Create the array of file status. */ + stat = new long[3]; + + try + { + index = cursor.getColumnIndex (Document.COLUMN_FLAGS); + if (index < 0) + return null; + + tem = cursor.getInt (index); + + stat[0] |= S_IRUSR; + if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0) + stat[0] |= S_IWUSR; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0) + stat[0] |= S_IFCHR; + + index = cursor.getColumnIndex (Document.COLUMN_SIZE); + if (index < 0) + return null; + + if (cursor.isNull (index)) + stat[1] = -1; /* The size is unknown. */ + else + stat[1] = cursor.getLong (index); + + index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + if (index < 0) + return null; + + tem1 = cursor.getString (index); + + /* Check if this is a directory file. */ + if (tem1.equals (Document.MIME_TYPE_DIR) + /* Files shouldn't be specials and directories at the same + time, but Android doesn't forbid document providers + from returning this information. */ + && (stat[0] & S_IFCHR) == 0) + /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, + just assume they're writable. */ + stat[0] |= S_IFDIR | S_IWUSR; + + /* If this file is neither a character special nor a + directory, indicate that it's a regular file. */ + + if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) + stat[0] |= S_IFREG; + + index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); + + if (index >= 0 && !cursor.isNull (index)) + { + /* Content providers are allowed to not provide mtime. */ + tem = cursor.getLong (index); + stat[2] = tem; + } + } + finally + { + cursor.close (); + } + + return stat; + } + + /* Return file status for the document designated by the given + DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document + ID in URI itself. + + Value is null upon failure, or an array of longs [MODE, SIZE, + MTIM] upon success, where MODE contains the file type and access + modes of the file as in `struct stat', SIZE is the size of the + file in BYTES or -1 if not known, and MTIM is the time of the + last modification to this file in milliseconds since 00:00, + January 1st, 1970. + + OperationCanceledException and other typical exceptions may be + signaled upon receiving async input or other errors. */ + + public long[] + statDocument (final String uri, final String documentId) + { + return (long[]) runObjectFunction (new SafObjectFunction () { + @Override + public Object + runObject (CancellationSignal signal) + { + return statDocument1 (uri, documentId, signal); + } + }); + } + + /* The bulk of `accessDocument'. SIGNAL should be a cancellation + signal. */ + + private int + accessDocument1 (String uri, String documentId, boolean writable, + CancellationSignal signal) + { + Uri uriObject; + String[] projection; + int tem, index; + String tem1; + Cursor cursor; + + uriObject = Uri.parse (uri); + + if (documentId == null) + documentId = DocumentsContract.getTreeDocumentId (uriObject); + + /* Create a document URI representing DOCUMENTID within URI's + authority. */ + + uriObject + = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); + + /* Now stat this document. */ + + projection = new String[] { + Document.COLUMN_FLAGS, + Document.COLUMN_MIME_TYPE, + }; + + cursor = resolver.query (uriObject, projection, null, + null, null, signal); + + if (cursor == null) + return -1; + + try + { + if (!cursor.moveToFirst ()) + return -1; + + if (!writable) + return 0; + + index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + if (index < 0) + return -3; + + /* Get the type of this file to check if it's a directory. */ + tem1 = cursor.getString (index); + + /* Check if this is a directory file. */ + if (tem1.equals (Document.MIME_TYPE_DIR)) + { + /* If so, don't check for FLAG_SUPPORTS_WRITE. + Check for FLAG_DIR_SUPPORTS_CREATE instead. */ + + if (!writable) + return 0; + + index = cursor.getColumnIndex (Document.COLUMN_FLAGS); + if (index < 0) + return -3; + + tem = cursor.getInt (index); + if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) + return -3; + + return 0; + } + + index = cursor.getColumnIndex (Document.COLUMN_FLAGS); + if (index < 0) + return -3; + + tem = cursor.getInt (index); + if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) + return -3; + } + finally + { + /* Close the cursor if an exception occurs. */ + cursor.close (); + } + + return 0; + } + + /* Find out whether Emacs has access to the document designated by + the specified DOCUMENTID within the tree URI. If DOCUMENTID is + NULL, use the document ID in URI itself. + + If WRITABLE, also check that the file is writable, which is true + if it is either a directory or its flags contains + FLAG_SUPPORTS_WRITE. + + Value is 0 if the file is accessible, and one of the following if + not: + + -1, if the file does not exist. + -2, if WRITABLE and the file is not writable. + -3, upon any other error. + + In addition, arbitrary runtime exceptions (such as + SecurityException or UnsupportedOperationException) may be + thrown. */ + + public int + accessDocument (final String uri, final String documentId, + final boolean writable) + { + return runIntFunction (new SafIntFunction () { + @Override + public int + runInt (CancellationSignal signal) + { + return accessDocument1 (uri, documentId, writable, + signal); + } + }); + } + + /* The crux of openDocumentDirectory. SIGNAL must be a cancellation + signal. */ + + private Cursor + openDocumentDirectory1 (String uri, String documentId, + CancellationSignal signal) + { + Uri uriObject; + Cursor cursor; + String projection[]; + + uriObject = Uri.parse (uri); + + /* If documentId is not set, use the document ID of the tree URI + itself. */ + + if (documentId == null) + documentId = DocumentsContract.getTreeDocumentId (uriObject); + + /* Build a URI representing each directory entry within + DOCUMENTID. */ + + uriObject + = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject, + documentId); + + projection = new String [] { + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, + }; + + cursor = resolver.query (uriObject, projection, null, null, + null, signal); + /* Return the cursor. */ + return cursor; + } + + /* Open a cursor representing each entry within the directory + designated by the specified DOCUMENTID within the tree URI. + + If DOCUMENTID is NULL, use the document ID within URI itself. + Value is NULL upon failure. + + In addition, arbitrary runtime exceptions (such as + SecurityException or UnsupportedOperationException) may be + thrown. */ + + public Cursor + openDocumentDirectory (final String uri, final String documentId) + { + return (Cursor) runObjectFunction (new SafObjectFunction () { + @Override + public Object + runObject (CancellationSignal signal) + { + return openDocumentDirectory1 (uri, documentId, signal); + } + }); + } + + /* The crux of `openDocument'. SIGNAL must be a cancellation + signal. */ + + public ParcelFileDescriptor + openDocument1 (String uri, String documentId, boolean write, + boolean truncate, CancellationSignal signal) + throws Throwable + { + Uri treeUri, documentUri; + String mode; + ParcelFileDescriptor fileDescriptor; + + treeUri = Uri.parse (uri); + + /* documentId must be set for this request, since it doesn't make + sense to ``open'' the root of the directory tree. */ + + documentUri + = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); + + if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) + { + /* Select the mode used to open the file. `rw' means open + a stat-able file, while `rwt' means that and to + truncate the file as well. */ + + if (truncate) + mode = "rwt"; + else + mode = "rw"; + + fileDescriptor + = resolver.openFileDescriptor (documentUri, mode, + signal); + } + else + { + /* Select the mode used to open the file. `openFile' + below means always open a stat-able file. */ + + if (truncate) + /* Invalid mode! */ + return null; + else + mode = "r"; + + fileDescriptor = resolver.openFile (documentUri, mode, + signal); + } + + return fileDescriptor; + } + + /* Open a file descriptor for a file document designated by + DOCUMENTID within the document tree identified by URI. If + TRUNCATE and the document already exists, truncate its contents + before returning. + + On Android 9.0 and earlier, always open the document in + ``read-write'' mode; this instructs the document provider to + return a seekable file that is stored on disk and returns correct + file status. + + Under newer versions of Android, open the document in a + non-writable mode if WRITE is false. This is possible because + these versions allow Emacs to explicitly request a seekable + on-disk file. + + Value is NULL upon failure or a parcel file descriptor upon + success. Call `ParcelFileDescriptor.close' on this file + descriptor instead of using the `close' system call. + + FileNotFoundException and/or SecurityException and/or + UnsupportedOperationException and/or OperationCanceledException + may be thrown upon failure. */ + + public ParcelFileDescriptor + openDocument (final String uri, final String documentId, + final boolean write, final boolean truncate) + { + Object tem; + + tem = runObjectFunction (new SafObjectFunction () { + @Override + public Object + runObject (CancellationSignal signal) + throws Throwable + { + return openDocument1 (uri, documentId, write, truncate, + signal); + } + }); + + return (ParcelFileDescriptor) tem; + } +}; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index aa672994f12..e410754071b 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -109,13 +109,6 @@ public final class EmacsService extends Service public static final int IC_MODE_ACTION = 1; public static final int IC_MODE_TEXT = 2; - /* File access mode constants. See `man 7 inode'. */ - public static final int S_IRUSR = 0000400; - public static final int S_IWUSR = 0000200; - public static final int S_IFCHR = 0020000; - public static final int S_IFDIR = 0040000; - public static final int S_IFREG = 0100000; - /* Display metrics used by font backends. */ public DisplayMetrics metrics; @@ -134,6 +127,10 @@ public final class EmacsService extends Service being called, and 2 if icBeginSynchronous was called. */ public static final AtomicInteger servicingQuery; + /* Thread used to query document providers, or null if it hasn't + been created yet. */ + private EmacsSafThread storageThread; + static { servicingQuery = new AtomicInteger (); @@ -1160,10 +1157,7 @@ public final class EmacsService extends Service /* Document tree management functions. These functions shouldn't be - called before Android 5.0. - - TODO: a timeout, let alone quitting, has yet to be implemented - for any of these functions. */ + called before Android 5.0. */ /* Return an array of each document authority providing at least one tree URI that Emacs holds the rights to persistently access. */ @@ -1319,223 +1313,26 @@ public final class EmacsService extends Service If the designated file can't be located, but each component of NAME up to the last component can and is a directory, return -2 - and the ID of the last component located in ID_RETURN[0]; + and the ID of the last component located in ID_RETURN[0]. - If the designated file can't be located, return -1. */ + If the designated file can't be located, return -1, or signal one + of OperationCanceledException, SecurityException, + FileNotFoundException, or UnsupportedOperationException. */ private int documentIdFromName (String tree_uri, String name, String[] id_return) { - Uri uri, treeUri; - String id, type; - String[] components, projection; - Cursor cursor; - int column; - - projection = new String[] { - Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_DOCUMENT_ID, - Document.COLUMN_MIME_TYPE, - }; - - /* Parse the URI identifying the tree first. */ - uri = Uri.parse (tree_uri); - - /* Now, split NAME into its individual components. */ - components = name.split ("/"); + /* Start the thread used to run SAF requests if it isn't already + running. */ - /* Set id and type to the value at the root of the tree. */ - type = id = null; - - /* For each component... */ - - for (String component : components) + if (storageThread == null) { - /* Java split doesn't behave very much like strtok when it - comes to trailing and leading delimiters... */ - if (component.isEmpty ()) - continue; - - /* Create the tree URI for URI from ID if it exists, or the - root otherwise. */ - - if (id == null) - id = DocumentsContract.getTreeDocumentId (uri); - - treeUri - = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id); - - /* Look for a file in this directory by the name of - component. */ - - try - { - cursor = resolver.query (treeUri, projection, - (Document.COLUMN_DISPLAY_NAME - + " = ?s"), - new String[] { component, }, null); - } - catch (SecurityException exception) - { - /* A SecurityException can be thrown if Emacs doesn't have - access to treeUri. */ - return -1; - } - catch (Exception exception) - { - exception.printStackTrace (); - - /* Why is this? */ - return -1; - } - - if (cursor == null) - return -1; - - while (true) - { - /* Even though the query selects for a specific display - name, some content providers nevertheless return every - file within the directory. */ - - if (!cursor.moveToNext ()) - { - cursor.close (); - - /* If the last component considered is a - directory... */ - if ((type == null - || type.equals (Document.MIME_TYPE_DIR)) - /* ... and type and id currently represent the - penultimate component. */ - && component == components[components.length - 1]) - { - /* The cursor is empty. In this case, return -2 - and the current document ID (belonging to the - previous component) in ID_RETURN. */ - - id_return[0] = id; - - /* But return -1 on the off chance that id is - null. */ - - if (id == null) - return -1; - - return -2; - } - - /* The last component found is not a directory, so - return -1. */ - return -1; - } - - /* So move CURSOR to a row with the right display - name. */ - - column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); - - if (column < 0) - continue; - - try - { - name = cursor.getString (column); - } - catch (Exception exception) - { - cursor.close (); - return -1; - } - - /* Break out of the loop only once a matching component is - found. */ - - if (name.equals (component)) - break; - } - - /* Look for a column by the name of COLUMN_DOCUMENT_ID. */ - - column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); - - if (column < 0) - { - cursor.close (); - return -1; - } - - /* Now replace ID with the document ID. */ - - try - { - id = cursor.getString (column); - } - catch (Exception exception) - { - cursor.close (); - return -1; - } - - /* If this is the last component, be sure to initialize the - document type. */ - - if (component == components[components.length - 1]) - { - column - = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); - - if (column < 0) - { - cursor.close (); - return -1; - } - - try - { - type = cursor.getString (column); - } - catch (Exception exception) - { - cursor.close (); - return -1; - } - - /* Type may be NULL depending on how the Cursor returned - is implemented. */ - - if (type == null) - { - cursor.close (); - return -1; - } - } - - /* Now close the cursor. */ - cursor.close (); - - /* ID may have become NULL if the data is in an invalid - format. */ - if (id == null) - return -1; + storageThread = new EmacsSafThread (resolver); + storageThread.start (); } - /* Here, id is either NULL (meaning the same as TREE_URI), and - type is either NULL (in which case id should also be NULL) or - the MIME type of the file. */ - - /* First return the ID. */ - - if (id == null) - id_return[0] = DocumentsContract.getTreeDocumentId (uri); - else - id_return[0] = id; - - /* Next, return whether or not this is a directory. */ - if (type == null || type.equals (Document.MIME_TYPE_DIR)) - return 1; - - return 0; + return storageThread.documentIdFromName (tree_uri, name, + id_return); } /* Return an encoded document URI representing a tree with the @@ -1585,130 +1382,24 @@ public final class EmacsService extends Service modes of the file as in `struct stat', SIZE is the size of the file in BYTES or -1 if not known, and MTIM is the time of the last modification to this file in milliseconds since 00:00, - January 1st, 1970. */ + January 1st, 1970. + + OperationCanceledException and other typical exceptions may be + signaled upon receiving async input or other errors. */ public long[] statDocument (String uri, String documentId) { - Uri uriObject; - String[] projection; - long[] stat; - int index; - long tem; - String tem1; - Cursor cursor; - - uriObject = Uri.parse (uri); - - if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (uriObject); - - /* Create a document URI representing DOCUMENTID within URI's - authority. */ - - uriObject - = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); - - /* Now stat this document. */ - - projection = new String[] { - Document.COLUMN_FLAGS, - Document.COLUMN_LAST_MODIFIED, - Document.COLUMN_MIME_TYPE, - Document.COLUMN_SIZE, - }; - - try - { - cursor = resolver.query (uriObject, projection, null, - null, null); - } - catch (SecurityException exception) - { - /* A SecurityException can be thrown if Emacs doesn't have - access to uriObject. */ - return null; - } - catch (UnsupportedOperationException exception) - { - exception.printStackTrace (); - - /* Why is this? */ - return null; - } - - if (cursor == null || !cursor.moveToFirst ()) - return null; - - /* Create the array of file status. */ - stat = new long[3]; + /* Start the thread used to run SAF requests if it isn't already + running. */ - try + if (storageThread == null) { - index = cursor.getColumnIndex (Document.COLUMN_FLAGS); - if (index < 0) - return null; - - tem = cursor.getInt (index); - - stat[0] |= S_IRUSR; - if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0) - stat[0] |= S_IWUSR; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0) - stat[0] |= S_IFCHR; - - index = cursor.getColumnIndex (Document.COLUMN_SIZE); - if (index < 0) - return null; - - if (cursor.isNull (index)) - stat[1] = -1; /* The size is unknown. */ - else - stat[1] = cursor.getLong (index); - - index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); - if (index < 0) - return null; - - tem1 = cursor.getString (index); - - /* Check if this is a directory file. */ - if (tem1.equals (Document.MIME_TYPE_DIR) - /* Files shouldn't be specials and directories at the same - time, but Android doesn't forbid document providers - from returning this information. */ - && (stat[0] & S_IFCHR) == 0) - /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, - just assume they're writable. */ - stat[0] |= S_IFDIR | S_IWUSR; - - /* If this file is neither a character special nor a - directory, indicate that it's a regular file. */ - - if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) - stat[0] |= S_IFREG; - - index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); - - if (index >= 0 && !cursor.isNull (index)) - { - /* Content providers are allowed to not provide mtime. */ - tem = cursor.getLong (index); - stat[2] = tem; - } - } - catch (Exception exception) - { - /* Whether or not type errors cause exceptions to be signaled - is defined ``by the implementation of Cursor'', whatever - that means. */ - exception.printStackTrace (); - return null; + storageThread = new EmacsSafThread (resolver); + storageThread.start (); } - return stat; + return storageThread.statDocument (uri, documentId); } /* Find out whether Emacs has access to the document designated by @@ -1733,83 +1424,16 @@ public final class EmacsService extends Service public int accessDocument (String uri, String documentId, boolean writable) { - Uri uriObject; - String[] projection; - int tem, index; - String tem1; - Cursor cursor; - - uriObject = Uri.parse (uri); - - if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (uriObject); - - /* Create a document URI representing DOCUMENTID within URI's - authority. */ - - uriObject - = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); - - /* Now stat this document. */ + /* Start the thread used to run SAF requests if it isn't already + running. */ - projection = new String[] { - Document.COLUMN_FLAGS, - Document.COLUMN_MIME_TYPE, - }; - - cursor = resolver.query (uriObject, projection, null, - null, null); - - if (cursor == null || !cursor.moveToFirst ()) - return -1; - - if (!writable) - return 0; - - try + if (storageThread == null) { - index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); - if (index < 0) - return -3; - - /* Get the type of this file to check if it's a directory. */ - tem1 = cursor.getString (index); - - /* Check if this is a directory file. */ - if (tem1.equals (Document.MIME_TYPE_DIR)) - { - /* If so, don't check for FLAG_SUPPORTS_WRITE. - Check for FLAG_DIR_SUPPORTS_CREATE instead. */ - - if (!writable) - return 0; - - index = cursor.getColumnIndex (Document.COLUMN_FLAGS); - if (index < 0) - return -3; - - tem = cursor.getInt (index); - if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) - return -3; - - return 0; - } - - index = cursor.getColumnIndex (Document.COLUMN_FLAGS); - if (index < 0) - return -3; - - tem = cursor.getInt (index); - if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) - return -3; - } - finally - { - /* Close the cursor if an exception occurs. */ - cursor.close (); + storageThread = new EmacsSafThread (resolver); + storageThread.start (); } - return 0; + return storageThread.accessDocument (uri, documentId, writable); } /* Open a cursor representing each entry within the directory @@ -1825,34 +1449,16 @@ public final class EmacsService extends Service public Cursor openDocumentDirectory (String uri, String documentId) { - Uri uriObject; - Cursor cursor; - String projection[]; - - uriObject = Uri.parse (uri); - - /* If documentId is not set, use the document ID of the tree URI - itself. */ - - if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (uriObject); - - /* Build a URI representing each directory entry within - DOCUMENTID. */ - - uriObject - = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject, - documentId); + /* Start the thread used to run SAF requests if it isn't already + running. */ - projection = new String [] { - Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_MIME_TYPE, - }; + if (storageThread == null) + { + storageThread = new EmacsSafThread (resolver); + storageThread.start (); + } - cursor = resolver.query (uriObject, projection, null, null, - null); - /* Return the cursor. */ - return cursor; + return storageThread.openDocumentDirectory (uri, documentId); } /* Read a single directory entry from the specified CURSOR. Return @@ -1945,50 +1551,18 @@ public final class EmacsService extends Service public ParcelFileDescriptor openDocument (String uri, String documentId, boolean write, boolean truncate) - throws FileNotFoundException { - Uri treeUri, documentUri; - String mode; - ParcelFileDescriptor fileDescriptor; - - treeUri = Uri.parse (uri); - - /* documentId must be set for this request, since it doesn't make - sense to ``open'' the root of the directory tree. */ + /* Start the thread used to run SAF requests if it isn't already + running. */ - documentUri - = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); - - if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) + if (storageThread == null) { - /* Select the mode used to open the file. `rw' means open - a stat-able file, while `rwt' means that and to - truncate the file as well. */ - - if (truncate) - mode = "rwt"; - else - mode = "rw"; - - fileDescriptor - = resolver.openFileDescriptor (documentUri, mode, - null); - } - else - { - /* Select the mode used to open the file. `openFile' - below means always open a stat-able file. */ - - if (truncate) - /* Invalid mode! */ - return null; - else - mode = "r"; - - fileDescriptor = resolver.openFile (documentUri, mode, null); + storageThread = new EmacsSafThread (resolver); + storageThread.start (); } - return fileDescriptor; + return storageThread.openDocument (uri, documentId, write, + truncate); } /* Create a new document with the given display NAME within the diff --git a/src/androidvfs.c b/src/androidvfs.c index 2cd50963e97..4f485622ff4 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -26,6 +26,7 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include #include #include @@ -34,6 +35,7 @@ along with GNU Emacs. If not, see . */ #include "android.h" #include "systime.h" +#include "blockinput.h" #if __ANDROID_API__ >= 9 #include @@ -278,6 +280,7 @@ static struct android_parcel_file_descriptor_class fd_class; /* Global references to several exception classes. */ static jclass file_not_found_exception, security_exception; +static jclass operation_canceled_exception; static jclass unsupported_operation_exception, out_of_memory_error; /* Initialize `cursor_class' using the given JNI environment ENV. @@ -3692,6 +3695,10 @@ android_saf_root_get_directory (int dirfd) /* Functions common to both SAF directory and file nodes. */ +/* Whether or not Emacs is within an operation running from the SAF + thread. */ +static bool inside_saf_critical_section; + /* Check for JNI exceptions, clear them, and set errno accordingly. Also, free each of the N local references given as arguments if an exception takes place. @@ -3704,6 +3711,9 @@ android_saf_root_get_directory (int dirfd) If the exception thrown derives from SecurityException, set errno to EACCES. + If the exception thrown derives from OperationCanceledException, + set errno to EINTR. + If the exception thrown derives from UnsupportedOperationException, set errno to ENOSYS. @@ -3754,6 +3764,9 @@ android_saf_exception_check (int n, ...) else if ((*env)->IsInstanceOf (env, (jobject) exception, security_exception)) errno = EACCES; + else if ((*env)->IsInstanceOf (env, (jobject) exception, + operation_canceled_exception)) + errno = EINTR; else if ((*env)->IsInstanceOf (env, (jobject) exception, unsupported_operation_exception)) errno = ENOSYS; @@ -3786,6 +3799,15 @@ android_saf_stat (const char *uri_name, const char *id_name, jobject status; jlong mode, size, mtim, *longs; + /* Now guarantee that it is safe to call functions which + synchronize with the SAF thread. */ + + if (inside_saf_critical_section) + { + errno = EIO; + return -1; + } + /* Build strings for both URI and ID. */ uri = (*android_java_env)->NewStringUTF (android_java_env, uri_name); android_exception_check (); @@ -3801,10 +3823,12 @@ android_saf_stat (const char *uri_name, const char *id_name, /* Try to retrieve the file status. */ method = service_class.stat_document; + inside_saf_critical_section = true; status = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, emacs_service, service_class.class, method, uri, id); + inside_saf_critical_section = false; /* Check for exceptions and release unneeded local references. */ @@ -3870,6 +3894,15 @@ android_saf_access (const char *uri_name, const char *id_name, jstring uri, id; jint rc; + /* Now guarantee that it is safe to call functions which + synchronize with the SAF thread. */ + + if (inside_saf_critical_section) + { + errno = EIO; + return -1; + } + /* Build strings for both URI and ID. */ uri = (*android_java_env)->NewStringUTF (android_java_env, uri_name); android_exception_check (); @@ -3885,11 +3918,13 @@ android_saf_access (const char *uri_name, const char *id_name, /* Try to retrieve the file status. */ method = service_class.access_document; + inside_saf_critical_section = true; rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env, emacs_service, service_class.class, method, uri, id, (jboolean) writable); + inside_saf_critical_section = false; /* Check for exceptions and release unneeded local references. */ @@ -4161,7 +4196,19 @@ android_document_id_from_name (const char *tree_uri, char *name, contain characters that can't be encoded in Java. */ if (android_verify_jni_string (name)) - return -1; + { + errno = ENOENT; + return -1; + } + + /* Now guarantee that it is safe to call + `document_id_from_name'. */ + + if (inside_saf_critical_section) + { + errno = EIO; + return -1; + } /* First, create the array that will hold the result. */ result = (*android_java_env)->NewObjectArray (android_java_env, 1, @@ -4176,14 +4223,17 @@ android_document_id_from_name (const char *tree_uri, char *name, uri = (*android_java_env)->NewStringUTF (android_java_env, tree_uri); android_exception_check_2 (result, java_name); - /* Now, call documentIdFromName. */ + /* Now, call documentIdFromName. This will synchronize with the SAF + thread, so make sure reentrant calls don't happen. */ method = service_class.document_id_from_name; + inside_saf_critical_section = true; rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env, emacs_service, service_class.class, method, uri, java_name, result); + inside_saf_critical_section = false; if (android_saf_exception_check (3, result, uri, java_name)) goto finish; @@ -4562,6 +4612,12 @@ android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp) jobject uri, id, cursor; jmethodID method; + if (inside_saf_critical_section) + { + errno = EIO; + return NULL; + } + /* Build strings for both URI and ID. */ uri = (*android_java_env)->NewStringUTF (android_java_env, vp->tree_uri); @@ -4578,11 +4634,13 @@ android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp) /* Try to open the cursor. */ method = service_class.open_document_directory; + inside_saf_critical_section = true; cursor = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, emacs_service, service_class.class, method, uri, id); + inside_saf_critical_section = false; if (id) { @@ -5001,6 +5059,12 @@ android_saf_file_open (struct android_vnode *vnode, int flags, struct android_parcel_fd *info; struct stat statb; + if (inside_saf_critical_section) + { + errno = EIO; + return -1; + } + /* Build strings for both the URI and ID. */ vp = (struct android_saf_file_vnode *) vnode; @@ -5016,12 +5080,14 @@ android_saf_file_open (struct android_vnode *vnode, int flags, method = service_class.open_document; trunc = flags & O_TRUNC; write = ((flags & O_RDWR) == O_RDWR || (flags & O_WRONLY)); + inside_saf_critical_section = true; descriptor = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, emacs_service, service_class.class, method, uri, id, write, trunc); + inside_saf_critical_section = false; if (android_saf_exception_check (2, uri, id)) return -1; @@ -5468,6 +5534,48 @@ android_saf_new_opendir (struct android_vnode *vnode) +/* Synchronization between SAF and Emacs. Consult EmacsSafThread.java + for more details. */ + +/* Semaphore posted upon the completion of an SAF operation. */ +static sem_t saf_completion_sem; + +JNIEXPORT jint JNICALL +NATIVE_NAME (safSyncAndReadInput) (JNIEnv *env, jobject object) +{ + while (sem_wait (&saf_completion_sem) < 0) + { + if (input_blocked_p ()) + continue; + + process_pending_signals (); + + if (!NILP (Vquit_flag)) + { + __android_log_print (ANDROID_LOG_VERBOSE, __func__, + "quitting from IO operation"); + return 1; + } + } + + return 0; +} + +JNIEXPORT void JNICALL +NATIVE_NAME (safSync) (JNIEnv *env, jobject object) +{ + while (sem_wait (&saf_completion_sem) < 0) + process_pending_signals (); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (safPostRequest) (JNIEnv *env, jobject object) +{ + sem_post (&saf_completion_sem); +} + + + /* Root vnode. This vnode represents the root inode, and is a regular Unix vnode with modifications to `name' that make it return asset vnodes. */ @@ -5692,6 +5800,11 @@ android_vfs_init (JNIEnv *env, jobject manager) (*env)->DeleteLocalRef (env, old); eassert (security_exception); + old = (*env)->FindClass (env, "android/os/OperationCanceledException"); + operation_canceled_exception = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (operation_canceled_exception); + old = (*env)->FindClass (env, "java/lang/UnsupportedOperationException"); unsupported_operation_exception = (*env)->NewGlobalRef (env, old); (*env)->DeleteLocalRef (env, old); @@ -5701,6 +5814,12 @@ android_vfs_init (JNIEnv *env, jobject manager) out_of_memory_error = (*env)->NewGlobalRef (env, old); (*env)->DeleteLocalRef (env, old); eassert (out_of_memory_error); + + /* Initialize the semaphore used to wait for SAF operations to + complete. */ + + if (sem_init (&saf_completion_sem, 0, 0) < 0) + emacs_abort (); } /* The replacement functions that follow have several major @@ -5754,6 +5873,12 @@ android_vfs_init (JNIEnv *env, jobject manager) The sixth is that flags and other argument checking is nowhere near exhaustive on vnode types other than Unix vnodes. + The seventh is that certain vnode types may read async input and + return EINTR not upon the arrival of a signal itself, but instead + if subsequently read input causes Vquit_flag to be set. These + vnodes may not be reentrant, but operating on them from within an + async input handler will at worst cause an error to be returned. + And the final drawback is that directories cannot be directly opened. Instead, `dirfd' must be called on a directory stream used by `openat'. @@ -6409,7 +6534,8 @@ android_asset_fstat (struct android_fd_or_asset asset, /* Directory listing emulation. */ /* Open a directory stream from the VFS node designated by NAME. - Value is NULL upon failure with errno set accordingly. + Value is NULL upon failure with errno set accordingly. `errno' may + be set to EINTR. The directory stream returned holds local references to JNI objects and shouldn't be used after the current local reference frame is diff --git a/src/dired.c b/src/dired.c index f2a123dc168..c10531cdb16 100644 --- a/src/dired.c +++ b/src/dired.c @@ -115,10 +115,19 @@ open_directory (Lisp_Object dirname, Lisp_Object encoded_dirname, int *fdp) #ifndef HAVE_ANDROID d = opendir (name); #else + /* `android_opendir' can return EINTR if DIRNAME designates a file + within a slow-to-respond document provider. */ + + again: d = android_opendir (name); if (d) fd = android_dirfd (d); + else if (errno == EINTR) + { + maybe_quit (); + goto again; + } #endif opendir_errno = errno; #else diff --git a/src/sysdep.c b/src/sysdep.c index 88938d15b91..0a1905c9196 100644 --- a/src/sysdep.c +++ b/src/sysdep.c @@ -2656,7 +2656,7 @@ emacs_fclose (FILE *stream) /* Wrappers around unlink, symlink, rename, renameat_noreplace, and rmdir. These operations handle asset and content directories on - Android. */ + Android, and may return EINTR. */ int emacs_unlink (const char *name) -- cgit v1.3 From 9cf166db63b7383a94dc47a6a5251c0dbe1dae9b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 31 Jul 2023 14:18:12 +0800 Subject: Initialize Android API level earlier * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsNoninteractive.java (main): * java/org/gnu/emacs/EmacsService.java (run): * java/org/gnu/emacs/EmacsThread.java (run): * src/android.c (initEmacs, setEmacsParams): Set `android_api_level' within setEmacsParams, not in initEmacs. * src/androidvfs.c: Pacify compiler warnings. --- java/org/gnu/emacs/EmacsNative.java | 14 +++++++------- java/org/gnu/emacs/EmacsNoninteractive.java | 6 +++--- java/org/gnu/emacs/EmacsService.java | 3 ++- java/org/gnu/emacs/EmacsThread.java | 4 +--- src/android.c | 12 +++++++----- src/androidvfs.c | 14 ++++++++++++++ 6 files changed, 34 insertions(+), 19 deletions(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index ea200037218..7d72a9f192e 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -66,7 +66,9 @@ public final class EmacsNative classPath must be the classpath of this app_process process, or NULL. - emacsService must be the EmacsService singleton, or NULL. */ + emacsService must be the EmacsService singleton, or NULL. + + apiLevel is the version of Android being run. */ public static native void setEmacsParams (AssetManager assetManager, String filesDir, String libDir, @@ -75,18 +77,16 @@ public final class EmacsNative float pixelDensityY, float scaledDensity, String classPath, - EmacsService emacsService); + EmacsService emacsService, + int apiLevel); /* Initialize Emacs with the argument array ARGV. Each argument must contain a NULL terminated string, or else the behavior is undefined. DUMPFILE is the dump file to use, or NULL if Emacs is to load - loadup.el itself. - - APILEVEL is the version of Android being used. */ - public static native void initEmacs (String argv[], String dumpFile, - int apiLevel); + loadup.el itself. */ + public static native void initEmacs (String argv[], String dumpFile); /* Abort and generate a native core dump. */ public static native void emacsAbort (); diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index aa6fa41ba97..1c7513e1cc9 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -190,14 +190,14 @@ public final class EmacsNoninteractive EmacsNative.setEmacsParams (assets, filesDir, libDir, cacheDir, 0.0f, - 0.0f, 0.0f, null, null); + 0.0f, 0.0f, null, null, + Build.VERSION.SDK_INT); /* Now find the dump file that Emacs should use, if it has already been dumped. */ EmacsApplication.findDumpFile (context); /* Start Emacs. */ - EmacsNative.initEmacs (args, EmacsApplication.dumpFileName, - Build.VERSION.SDK_INT); + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); } }; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index e714f75fdf2..3c1bb0855f4 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -291,7 +291,8 @@ public final class EmacsService extends Service cacheDir, (float) pixelDensityX, (float) pixelDensityY, (float) scaledDensity, - classPath, EmacsService.this); + classPath, EmacsService.this, + Build.VERSION.SDK_INT); } }, extraStartupArgument, /* If any file needs to be opened, open it now. */ diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index c003ea95c50..5307015b46f 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -22,7 +22,6 @@ package org.gnu.emacs; import java.lang.Thread; import java.util.Arrays; -import android.os.Build; import android.util.Log; public final class EmacsThread extends Thread @@ -78,7 +77,6 @@ public final class EmacsThread extends Thread /* Run the native code now. */ Log.d (TAG, "run: " + Arrays.toString (args)); - EmacsNative.initEmacs (args, EmacsApplication.dumpFileName, - Build.VERSION.SDK_INT); + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); } }; diff --git a/src/android.c b/src/android.c index 8c0232a51f8..f60ff5acb54 100644 --- a/src/android.c +++ b/src/android.c @@ -1281,7 +1281,8 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jfloat pixel_density_y, jfloat scaled_density, jobject class_path, - jobject emacs_service_object) + jobject emacs_service_object, + jint api_level) { JNI_STACK_ALIGNMENT_PROLOGUE; @@ -1289,6 +1290,10 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, pthread_t thread; const char *java_string; + /* Set the Android API level early, as it is used by + `android_vfs_init'. */ + android_api_level = api_level; + /* This function should only be called from the main thread. */ android_pixel_density_x = pixel_density_x; @@ -1771,7 +1776,7 @@ android_init_emacs_cursor (void) JNIEXPORT void JNICALL NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, - jobject dump_file_object, jint api_level) + jobject dump_file_object) { /* android_emacs_init is not main, so GCC is not nice enough to add the stack alignment prologue. @@ -1788,9 +1793,6 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, const char *c_argument; char *dump_file; - /* Set the Android API level. */ - android_api_level = api_level; - android_java_env = env; nelements = (*env)->GetArrayLength (env, argv); diff --git a/src/androidvfs.c b/src/androidvfs.c index 9acc8f2b139..eeef5ea5db0 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -5976,6 +5976,14 @@ android_saf_new_opendir (struct android_vnode *vnode) /* Semaphore posted upon the completion of an SAF operation. */ static sem_t saf_completion_sem; +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#else /* GNUC */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-prototypes" +#endif /* __clang__ */ + JNIEXPORT jint JNICALL NATIVE_NAME (safSyncAndReadInput) (JNIEnv *env, jobject object) { @@ -6010,6 +6018,12 @@ NATIVE_NAME (safPostRequest) (JNIEnv *env, jobject object) sem_post (&saf_completion_sem); } +#ifdef __clang__ +#pragma clang diagnostic pop +#else /* GNUC */ +#pragma GCC diagnostic pop +#endif /* __clang__ */ + /* Root vnode. This vnode represents the root inode, and is a regular -- cgit v1.3 From 7873369338ee0159ca285153fd4592cbcff65d7a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 6 Aug 2023 21:45:29 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsNative.java: Declare ftruncate. * java/org/gnu/emacs/EmacsSafThread.java (openDocument1): If initially opening with rwt, verify the file descriptor is really writable; if not, resort to rw and truncating the file descriptor by hand instead. * src/androidvfs.c (NATIVE_NAME (ftruncate)): New function. Truncate file descriptor and return whether that was successful. --- ChangeLog.android | 9 +++++++++ java/org/gnu/emacs/EmacsNative.java | 4 ++++ java/org/gnu/emacs/EmacsSafThread.java | 37 ++++++++++++++++++++++++++++++++++ src/androidvfs.c | 8 +++++++- 4 files changed, 57 insertions(+), 1 deletion(-) (limited to 'java/org/gnu/emacs/EmacsNative.java') diff --git a/ChangeLog.android b/ChangeLog.android index 689482d2f1a..82ab75b40c1 100644 --- a/ChangeLog.android +++ b/ChangeLog.android @@ -1,11 +1,20 @@ 2023-08-06 Po Lu + * java/org/gnu/emacs/EmacsSafThread.java (openDocument1): If + initially opening with rwt, verify the file descriptor is really + writable; if not, resort to rw and truncating the file descriptor + by hand instead. + + * src/androidvfs.c (NATIVE_NAME (ftruncate)): New function. + Truncate file descriptor and return whether that was successful. + * src/androidvfs.c (android_saf_tree_chmod): Repair file access permissions allowed within FLAGS. 2023-08-05 Po Lu * doc/lispref/commands.texi (Touchscreen Events): Fix typo. + * lisp/subr.el (y-or-n-p): Don't call set-text-conversion-style when not present. diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 7d72a9f192e..fae0ba98f86 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -274,6 +274,10 @@ public final class EmacsNative operations. */ public static native void safPostRequest (); + /* Detect and return FD is writable. FD may be truncated to 0 bytes + in the process. */ + public static native boolean ftruncate (int fd); + static { /* Older versions of Android cannot link correctly with shared diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index 29cd3fa6bc7..3ae3c0839ce 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.Iterator; import java.io.FileNotFoundException; +import java.io.IOException; import android.content.ContentResolver; import android.database.Cursor; @@ -1597,6 +1598,42 @@ public final class EmacsSafThread extends HandlerThread = resolver.openFileDescriptor (documentUri, mode, signal); + /* If a writable file descriptor is requested and TRUNCATE is set, + then probe the file descriptor to detect if it is actually + readable. If not, close this file descriptor and reopen it + with MODE set to rw; some document providers granting access to + Samba shares don't implement rwt, but these document providers + invariably truncate the file opened even when the mode is + merely rw. + + This may be ascribed to a mix-up in Android's documentation + regardin DocumentsProvider: the `openDocument' function is only + documented to accept r or rw, whereas the default + implementation of the `openFile' function (which documents rwt) + delegates to `openDocument'. */ + + if (write && truncate && fileDescriptor != null + && !EmacsNative.ftruncate (fileDescriptor.getFd ())) + { + try + { + fileDescriptor.closeWithError ("File descriptor requested" + + " is not writable"); + } + catch (IOException e) + { + Log.w (TAG, "Leaking unclosed file descriptor " + e); + } + + fileDescriptor + = resolver.openFileDescriptor (documentUri, "rw", signal); + + /* Try to truncate fileDescriptor just to stay on the safe + side. */ + if (fileDescriptor != null) + EmacsNative.ftruncate (fileDescriptor.getFd ()); + } + /* Every time a document is opened, remove it from the file status cache. */ toplevel = getCache (treeUri); diff --git a/src/androidvfs.c b/src/androidvfs.c index dc5097f463e..d6daff481b0 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -5605,7 +5605,7 @@ android_saf_file_open (struct android_vnode *vnode, int flags, /* Open a parcel file descriptor according to flags. */ method = service_class.open_document; - trunc = flags & O_TRUNC; + trunc = (flags & O_TRUNC); write = ((flags & O_RDWR) == O_RDWR || (flags & O_WRONLY)); inside_saf_critical_section = true; descriptor @@ -6121,6 +6121,12 @@ NATIVE_NAME (safPostRequest) (JNIEnv *env, jobject object) sem_post (&saf_completion_sem); } +JNIEXPORT jboolean JNICALL +NATIVE_NAME (ftruncate) (JNIEnv *env, jobject object, jint fd) +{ + return ftruncate (fd, 0) != -1; +} + #ifdef __clang__ #pragma clang diagnostic pop #else /* GNUC */ -- cgit v1.3