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/EmacsActivity.java | 146 +++++++++ java/org/gnu/emacs/EmacsApplication.java | 27 ++ java/org/gnu/emacs/EmacsCopyArea.java | 131 ++++++++ java/org/gnu/emacs/EmacsDrawLine.java | 137 +++++++++ java/org/gnu/emacs/EmacsDrawPoint.java | 30 ++ java/org/gnu/emacs/EmacsDrawRectangle.java | 127 ++++++++ java/org/gnu/emacs/EmacsDrawable.java | 33 ++ java/org/gnu/emacs/EmacsFillPolygon.java | 150 +++++++++ java/org/gnu/emacs/EmacsFillRectangle.java | 129 ++++++++ java/org/gnu/emacs/EmacsFontDriver.java | 150 +++++++++ java/org/gnu/emacs/EmacsGC.java | 116 +++++++ java/org/gnu/emacs/EmacsHandleObject.java | 62 ++++ java/org/gnu/emacs/EmacsNative.java | 72 +++++ java/org/gnu/emacs/EmacsPaintQueue.java | 138 +++++++++ java/org/gnu/emacs/EmacsPaintReq.java | 33 ++ java/org/gnu/emacs/EmacsPixmap.java | 102 ++++++ java/org/gnu/emacs/EmacsSdk7FontDriver.java | 460 ++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsService.java | 384 +++++++++++++++++++++++ java/org/gnu/emacs/EmacsSurfaceView.java | 83 +++++ java/org/gnu/emacs/EmacsThread.java | 44 +++ java/org/gnu/emacs/EmacsView.java | 211 +++++++++++++ java/org/gnu/emacs/EmacsWindow.java | 336 ++++++++++++++++++++ 22 files changed, 3101 insertions(+) create mode 100644 java/org/gnu/emacs/EmacsActivity.java create mode 100644 java/org/gnu/emacs/EmacsApplication.java create mode 100644 java/org/gnu/emacs/EmacsCopyArea.java create mode 100644 java/org/gnu/emacs/EmacsDrawLine.java create mode 100644 java/org/gnu/emacs/EmacsDrawPoint.java create mode 100644 java/org/gnu/emacs/EmacsDrawRectangle.java create mode 100644 java/org/gnu/emacs/EmacsDrawable.java create mode 100644 java/org/gnu/emacs/EmacsFillPolygon.java create mode 100644 java/org/gnu/emacs/EmacsFillRectangle.java create mode 100644 java/org/gnu/emacs/EmacsFontDriver.java create mode 100644 java/org/gnu/emacs/EmacsGC.java create mode 100644 java/org/gnu/emacs/EmacsHandleObject.java create mode 100644 java/org/gnu/emacs/EmacsNative.java create mode 100644 java/org/gnu/emacs/EmacsPaintQueue.java create mode 100644 java/org/gnu/emacs/EmacsPaintReq.java create mode 100644 java/org/gnu/emacs/EmacsPixmap.java create mode 100644 java/org/gnu/emacs/EmacsSdk7FontDriver.java create mode 100644 java/org/gnu/emacs/EmacsService.java create mode 100644 java/org/gnu/emacs/EmacsSurfaceView.java create mode 100644 java/org/gnu/emacs/EmacsThread.java create mode 100644 java/org/gnu/emacs/EmacsView.java create mode 100644 java/org/gnu/emacs/EmacsWindow.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java new file mode 100644 index 00000000000..cacd5f13e60 --- /dev/null +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -0,0 +1,146 @@ +/* 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.IllegalStateException; +import java.util.List; +import java.util.ArrayList; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; + +public class EmacsActivity extends Activity +{ + public static final String TAG = "EmacsActivity"; + + /* List of all activities that do not have an associated + EmacsWindow. */ + public static List availableActivities; + + /* The currently attached EmacsWindow, or null if none. */ + private EmacsWindow window; + + /* The frame layout associated with the activity. */ + private FrameLayout layout; + + static + { + /* Set up the list of available activities. */ + availableActivities = new ArrayList (); + }; + + public void + attachChild (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); + + /* Remove the objects from the lists of what is available. */ + EmacsService.availableChildren.remove (child); + availableActivities.remove (this); + + /* Now set child->activity. */ + child.setActivity (this); + } + + /* Make this activity available for future windows to attach + again. */ + + public void + makeAvailable () + { + window = null; + + for (EmacsWindow iterWindow + : EmacsService.availableChildren) + { + synchronized (iterWindow) + { + if (!iterWindow.isDestroyed ()) + attachChild (iterWindow); + + return; + } + } + + availableActivities.add (this); + } + + @Override + public void + onCreate (Bundle savedInstanceState) + { + FrameLayout.LayoutParams params; + + 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); + + /* Make the activity available before starting the + service. */ + makeAvailable (); + + if (EmacsService.SERVICE == null) + /* Start the Emacs service now. */ + startService (new Intent (this, EmacsService.class)); + + super.onCreate (savedInstanceState); + } + + @Override + public void + onStop () + { + /* The activity is no longer visible. If there is a window + attached, detach it. */ + + if (window != null) + { + layout.removeView (window.view); + + /* 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; + } + + /* Finally, remove this activity from the list of available + activities. */ + availableActivities.remove (this); + super.onStop (); + } +}; diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java new file mode 100644 index 00000000000..125da05cfd4 --- /dev/null +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -0,0 +1,27 @@ +/* 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.app.Application; + +public class EmacsApplication extends Application +{ + /* This class currently does nothing. */ +}; diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java new file mode 100644 index 00000000000..f34d1ecde01 --- /dev/null +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -0,0 +1,131 @@ +/* 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.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 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; + + static + { + xorAlu = new PorterDuffXfermode (Mode.XOR); + srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); + }; + + public + EmacsCopyArea (EmacsDrawable destination, EmacsDrawable source, + int src_x, int src_y, int width, int height, + int dest_x, int dest_y, EmacsGC immutableGC) + { + 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; + } + + @Override + public EmacsGC + getGC () + { + return immutableGC; + } + + @Override + public void + paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) + { + int alu; + Bitmap bitmap; + Paint maskPaint; + Canvas maskCanvas; + Bitmap maskBitmap; + Rect rect, srcRect; + + /* TODO implement stippling. */ + if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + return; + + alu = immutableGC.function; + rect = getRect (); + bitmap = source.getBitmap (); + + if (alu == EmacsGC.GC_COPY) + paint.setXfermode (null); + else + 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); + 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) + return; + + maskPaint.setXfermode (srcInAlu); + 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); + } + } +} diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java new file mode 100644 index 00000000000..6389031bbfa --- /dev/null +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -0,0 +1,137 @@ +/* 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.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 class EmacsDrawLine implements EmacsPaintReq +{ + private int x, y, x2, y2; + private EmacsDrawable drawable; + private EmacsGC immutableGC; + private static Xfermode xorAlu, srcInAlu; + + static + { + xorAlu = new PorterDuffXfermode (Mode.XOR); + srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); + }; + + 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; + } + + @Override + public Rect + getRect () + { + return 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; + + /* TODO implement stippling. */ + if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + 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); + + if (immutableGC.clip_mask == null) + { + paint.setColor (immutableGC.foreground | 0xff000000); + canvas.drawLine ((float) x, (float) y, + (float) x2, (float) y2, + paint); + } + else + { + 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); + } + + paint.setXfermode (null); + } +} diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java new file mode 100644 index 00000000000..772757ff424 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDrawPoint.java @@ -0,0 +1,30 @@ +/* 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 EmacsDrawPoint extends EmacsDrawRectangle +{ + public + EmacsDrawPoint (EmacsDrawable drawable, int x, int y, + EmacsGC immutableGC) + { + super (drawable, x, y, 1, 1, immutableGC); + } +} diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java new file mode 100644 index 00000000000..462bf7c85b5 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -0,0 +1,127 @@ +/* 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.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; + } + + @Override + public Rect + getRect () + { + return new Rect (x, y, x + width, y + height); + } + + @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; + + /* TODO implement stippling. */ + if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + return; + + alu = immutableGC.function; + rect = getRect (); + + paint.setStyle (Paint.Style.STROKE); + + if (alu == EmacsGC.GC_COPY) + paint.setXfermode (null); + else + paint.setXfermode (xorAlu); + + if (immutableGC.clip_mask == null) + { + paint.setColor (immutableGC.foreground | 0xff000000); + canvas.drawRect (rect, paint); + } + else + { + 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.drawRect (srcRect, maskPaint); + canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + } + + paint.setXfermode (null); + } +} diff --git a/java/org/gnu/emacs/EmacsDrawable.java b/java/org/gnu/emacs/EmacsDrawable.java new file mode 100644 index 00000000000..19062137213 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDrawable.java @@ -0,0 +1,33 @@ +/* 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.graphics.Rect; +import android.graphics.Bitmap; +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 new file mode 100644 index 00000000000..3198c7f07c4 --- /dev/null +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -0,0 +1,150 @@ +/* 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.Math; + +import android.graphics.Bitmap; +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 +{ + private EmacsDrawable drawable; + private EmacsGC immutableGC; + private Path path; + + private static Xfermode xorAlu, srcInAlu; + + static + { + xorAlu = new PorterDuffXfermode (Mode.XOR); + srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); + }; + + public + EmacsFillPolygon (EmacsDrawable drawable, Point points[], + EmacsGC immutableGC) + { + int i; + + this.drawable = drawable; + this.immutableGC = immutableGC; + + /* Build the path from the given array of points. */ + path = new Path (); + + if (points.length >= 1) + { + path.moveTo (points[0].x, points[0].y); + + for (i = 1; i < points.length; ++i) + path.lineTo (points[i].x, points[i].y); + + path.close (); + } + } + + @Override + public Rect + getRect () + { + RectF rect; + + rect = new RectF (0, 0, 0, 0); + path.computeBounds (rect, true); + + return new Rect ((int) Math.floor (rect.left), + (int) Math.floor (rect.top), + (int) Math.ceil (rect.right), + (int) Math.ceil (rect.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 (); + + if (alu == EmacsGC.GC_COPY) + paint.setXfermode (null); + else + paint.setXfermode (xorAlu); + + paint.setStyle (Paint.Style.FILL); + + 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); + } + } +} diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java new file mode 100644 index 00000000000..7246c13de7f --- /dev/null +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -0,0 +1,129 @@ +/* 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.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 +{ + 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 () + { + 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; + + /* 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 (immutableGC.clip_mask == null) + { + paint.setColor (immutableGC.foreground | 0xff000000); + canvas.drawRect (rect, paint); + } + else + { + 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.drawRect (srcRect, maskPaint); + canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + } + + paint.setXfermode (null); + } +} diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java new file mode 100644 index 00000000000..f419e71059d --- /dev/null +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -0,0 +1,150 @@ +/* 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 java.util.List; + +public abstract class EmacsFontDriver +{ + /* Font weights. */ + public static final int THIN = 0; + public static final int ULTRA_LIGHT = 40; + public static final int LIGHT = 50; + public static final int SEMI_LIGHT = 55; + public static final int REGULAR = 80; + public static final int MEDIUM = 100; + public static final int SEMI_BOLD = 180; + public static final int BOLD = 200; + public static final int EXTRA_BOLD = 205; + public static final int BLACK = 210; + public static final int ULTRA_HEAVY = 250; + + /* Font slants. */ + public static final int REVERSE_OBLIQUE = 0; + public static final int REVERSE_ITALIC = 10; + public static final int NORMAL = 100; + public static final int ITALIC = 200; + public static final int OBLIQUE = 210; + + /* Font widths. */ + public static final int ULTRA_CONDENSED = 50; + public static final int EXTRA_CONDENSED = 63; + public static final int CONDENSED = 75; + public static final int SEMI_CONDENSED = 87; + public static final int UNSPECIFIED = 100; + public static final int SEMI_EXPANDED = 113; + public static final int EXPANDED = 125; + public static final int EXTRA_EXPANDED = 150; + public static final int ULTRA_EXPANDED = 200; + + /* Font spacings. */ + public static final int PROPORTIONAL = 0; + public static final int DUAL = 90; + public static final int MONO = 100; + public static final int CHARCELL = 110; + + public class FontSpec + { + /* The fields below mean the same as they do in enum + font_property_index in font.h. */ + + public String foundry; + public String family; + public String adstyle; + public String registry; + public Integer width; + public Integer weight; + public Integer slant; + public Integer size; + public Integer spacing; + public Integer avgwidth; + + @Override + public String + toString () + { + return ("foundry: " + foundry + + " family: " + family + + " adstyle: " + adstyle + + " registry: " + registry + + " width: " + width + + " weight: " + weight + + " slant: " + slant + + " spacing: " + spacing + + " avgwidth: " + avgwidth); + } + }; + + public class FontMetrics + { + public short lbearing; + public short rbearing; + public short width; + public short ascent; + public short descent; + } + + public class FontEntity extends FontSpec + { + /* No extra fields here. */ + }; + + public abstract class FontObject extends FontSpec + { + public int minWidth; + public int maxWidth; + public int pixelSize; + public int height; + public int spaceWidth; + public int averageWidth; + public int ascent; + public int descent; + public int underlineThickness; + public int underlinePosition; + public int baselineOffset; + public int relativeCompose; + public int defaultAscent; + public int encodingCharset; + public int repertoryCharset; + + public + FontObject () + { + encodingCharset = -1; + repertoryCharset = -1; + } + }; + + /* These mean the same as they do in struct font_driver. */ + public abstract FontEntity[] list (FontSpec fontSpec); + public abstract FontEntity match (FontSpec fontSpec); + public abstract String[] listFamilies (); + 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[]); + public abstract int encodeChar (FontObject fontObject, char charCode); + + public static EmacsFontDriver + createFontDriver () + { + return new EmacsSdk7FontDriver (); + } +}; diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java new file mode 100644 index 00000000000..0becb04519d --- /dev/null +++ b/java/org/gnu/emacs/EmacsGC.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; + +import android.graphics.Rect; + +/* X like graphics context structures. Keep the enums in synch with + androidgui.h! */ + +public class EmacsGC extends EmacsHandleObject +{ + public static final int GC_COPY = 0; + public static final int GC_XOR = 1; + + public static final int GC_FILL_SOLID = 0; + public static final int GC_FILL_OPAQUE_STIPPLED = 1; + + 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 EmacsPixmap clip_mask, stipple; + private boolean dirty; + private EmacsGC immutableGC; + + /* The following fields are only set on immutable GCs. */ + + public + EmacsGC (short handle) + { + /* For historical reasons the C code has an extra layer of + indirection above this GC handle. struct android_gc is the GC + used by Emacs code, while android_gcontext is the type of the + handle. */ + super (handle); + + fill_style = GC_FILL_SOLID; + function = GC_COPY; + foreground = 0; + background = 0xffffffff; + } + + public + EmacsGC (EmacsGC source) + { + super ((short) 0); + + 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]; + + 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); + } + } + } + + /* Mark this GC as dirty. This means immutableGC will return a new + copy of this GC the next time it is called. */ + + public void + markDirty () + { + dirty = true; + } + + public EmacsGC + immutableGC () + { + if (immutableGC == null || dirty) + { + immutableGC = new EmacsGC (this); + dirty = false; + } + + return immutableGC; + }; +}; diff --git a/java/org/gnu/emacs/EmacsHandleObject.java b/java/org/gnu/emacs/EmacsHandleObject.java new file mode 100644 index 00000000000..a57a3bbdfa9 --- /dev/null +++ b/java/org/gnu/emacs/EmacsHandleObject.java @@ -0,0 +1,62 @@ +/* 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.List; +import java.util.ArrayList; +import java.lang.Object; +import java.lang.IllegalStateException; + +/* This defines something that is a so-called ``handle''. Handles + must be created by C code, and will remain existing until + destroyHandle is called. C code then refers to the handle by a + number which maps into the Java object representing the handle. + + All handle operations must be done from the Emacs thread. */ + +public abstract class EmacsHandleObject +{ + /* Whether or not this handle has been destroyed. */ + volatile boolean destroyed; + + /* The handle associated with this object. */ + public short handle; + + public + EmacsHandleObject (short handle) + { + this.handle = handle; + } + + public void + destroyHandle () throws IllegalStateException + { + synchronized (this) + { + destroyed = true; + } + } + + public boolean + isDestroyed () + { + return destroyed; + } +}; 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"); + }; +}; diff --git a/java/org/gnu/emacs/EmacsPaintQueue.java b/java/org/gnu/emacs/EmacsPaintQueue.java new file mode 100644 index 00000000000..5af5868d3b9 --- /dev/null +++ b/java/org/gnu/emacs/EmacsPaintQueue.java @@ -0,0 +1,138 @@ +/* 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.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; + +public class EmacsPaintQueue +{ + /* Queue of paint operations. This is modified from the Emacs + thread, and entire paint queues are periodically flushed to the + application thread where it is executed. */ + private List paintOperations; + + /* Number of operations in this queue. */ + public int numRequests; + + public + EmacsPaintQueue () + { + paintOperations = new LinkedList (); + } + + public void + run () + { + EmacsDrawable drawable, last; + Canvas canvas; + EmacsGC gc, lastGC; + int i; + Paint paint; + Rect rect, offsetRect, copyRect; + + canvas = null; + last = null; + gc = null; + paint = new Paint (); + + 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 (); + + 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.clipRect (gc.clip_rects[0]); + req.paintTo (canvas, paint, gc); + } + else + { + /* There are multiple clip rects. Android doesn't let + programs use RegionOp.UNION on the clip rectangle, + so Emacs must iterate over each intersection and + paint it manually. This seems inefficient but + thankfully Emacs never seems to use more than one + clip rect. */ + + for (i = 0; i < gc.clip_rects.length; ++i) + { + copyRect = new Rect (gc.clip_rects[i]); + + if (copyRect.intersect (rect)) + { + canvas.save (); + canvas.clipRect (copyRect); + req.paintTo (canvas, paint, gc); + canvas.restore (); + } + } + } + + drawable.damageRect (rect); + canvas.restore (); + } + } + } + + public void + appendPaintOperation (EmacsPaintReq req) + { + paintOperations.add (req); + numRequests++; + } +}; diff --git a/java/org/gnu/emacs/EmacsPaintReq.java b/java/org/gnu/emacs/EmacsPaintReq.java new file mode 100644 index 00000000000..5b14b005093 --- /dev/null +++ b/java/org/gnu/emacs/EmacsPaintReq.java @@ -0,0 +1,33 @@ +/* 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.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; + +public interface EmacsPaintReq +{ + public EmacsDrawable getDrawable (); + public EmacsGC getGC (); + public void paintTo (Canvas canvas, Paint paint, + EmacsGC immutableGC); + public Rect getRect (); +}; diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java new file mode 100644 index 00000000000..ccd5f1e0043 --- /dev/null +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -0,0 +1,102 @@ +/* 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.IllegalArgumentException; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; + +/* Drawable backed by bitmap. */ + +public class EmacsPixmap extends EmacsHandleObject + implements EmacsDrawable +{ + /* The depth of the bitmap. This is not actually used, just defined + in order to be consistent with X. */ + public int depth, width, height; + + /* The bitmap itself. */ + public Bitmap bitmap; + + /* The canvas used to draw to BITMAP. */ + public Canvas canvas; + + public + EmacsPixmap (short handle, int colors[], 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 (colors, width, height, + Bitmap.Config.ALPHA_8); + break; + + case 24: + bitmap = Bitmap.createBitmap (colors, width, height, + Bitmap.Config.ARGB_8888); + bitmap.setHasAlpha (false); + break; + } + + this.width = width; + this.height = height; + this.depth = depth; + } + + @Override + public Canvas + lockCanvas () + { + if (canvas == null) + canvas = new Canvas (bitmap); + + return canvas; + } + + @Override + public void + unlockCanvas () + { + + } + + @Override + public void + damageRect (Rect damageRect) + { + + } + + @Override + public Bitmap + getBitmap () + { + return bitmap; + } +}; diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java new file mode 100644 index 00000000000..5a8cdbfc75b --- /dev/null +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -0,0 +1,460 @@ +/* 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 java.io.File; +import java.io.IOException; + +import java.util.LinkedList; +import java.util.List; + +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; + +import android.util.Log; + +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 + { + /* The typeface and paint. */ + public Typeface typeface; + public Paint typefacePaint; + public String familyName; + public int slant, width, weight, spacing; + + public + Sdk7Typeface (String fileName, Typeface typeface) + { + String style, testString; + int index, measured, i; + float[] widths; + + slant = NORMAL; + weight = REGULAR; + width = UNSPECIFIED; + spacing = PROPORTIONAL; + + typefacePaint = new Paint (); + typefacePaint.setTypeface (typeface); + + /* For the calls to measureText below. */ + typefacePaint.setTextSize (10.0f); + + /* Parse the file name into some useful data. First, strip off + the extension. */ + fileName = fileName.split ("\\.", 2)[0]; + + /* Next, split the file name by dashes. Everything before the + last dash is part of the family name. */ + index = fileName.lastIndexOf ("-"); + + if (index > 0) + { + style = fileName.substring (index + 1, fileName.length ()); + familyName = fileName.substring (0, index); + + /* Look for something describing the weight. */ + if (style.contains ("Thin")) + weight = THIN; + else if (style.contains ("UltraLight")) + weight = ULTRA_LIGHT; + else if (style.contains ("SemiLight")) + weight = SEMI_LIGHT; + else if (style.contains ("Light")) + weight = LIGHT; + else if (style.contains ("Medium")) + weight = MEDIUM; + else if (style.contains ("SemiBold")) + weight = SEMI_BOLD; + else if (style.contains ("ExtraBold")) + weight = EXTRA_BOLD; + else if (style.contains ("Bold")) + weight = BOLD; + else if (style.contains ("Black")) + weight = BLACK; + else if (style.contains ("UltraHeavy")) + weight = ULTRA_HEAVY; + + /* And the slant. */ + if (style.contains ("ReverseOblique")) + slant = OBLIQUE; + else if (style.contains ("ReverseItalic")) + slant = REVERSE_ITALIC; + else if (style.contains ("Italic")) + slant = ITALIC; + else if (style.contains ("Oblique")) + slant = OBLIQUE; + + /* Finally, the width. */ + if (style.contains ("UltraCondensed")) + width = ULTRA_CONDENSED; + else if (style.contains ("ExtraCondensed")) + width = EXTRA_CONDENSED; + else if (style.contains ("SemiCondensed")) + width = SEMI_CONDENSED; + else if (style.contains ("Condensed")) + width = CONDENSED; + else if (style.contains ("SemiExpanded")) + width = SEMI_EXPANDED; + else if (style.contains ("ExtraExpanded")) + width = EXTRA_EXPANDED; + else if (style.contains ("UltraExpanded")) + width = ULTRA_EXPANDED; + else if (style.contains ("Expanded")) + width = EXPANDED; + + /* Guess the spacing information. */ + testString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + widths = new float[testString.length ()]; + + measured = typefacePaint.getTextWidths (testString, + 0, testString.length (), + widths); + spacing = MONO; + for (i = 0; i < measured; ++i) + { + if (i != 0 && widths[i - 1] != widths[i]) + /* This isn't a monospace font. */ + spacing = PROPORTIONAL; + } + } + else + familyName = fileName; + + Log.d (TAG, "Initialized new typeface " + familyName); + } + + @Override + public String + toString () + { + return ("Sdk7Typeface (" + + String.valueOf (familyName) + ", " + + String.valueOf (slant) + ", " + + String.valueOf (width) + ", " + + String.valueOf (weight) + ", " + + String.valueOf (spacing) + ")"); + } + }; + + private class Sdk7FontEntity extends FontEntity + { + /* The typeface. */ + public Sdk7Typeface typeface; + + public + Sdk7FontEntity (Sdk7Typeface typeface) + { + float width; + + foundry = "Google"; + family = typeface.familyName; + adstyle = null; + weight = typeface.weight; + slant = typeface.slant; + spacing = typeface.spacing; + width = typeface.width; + + this.typeface = typeface; + } + }; + + private class Sdk7FontObject extends FontObject + { + /* The typeface. */ + public Sdk7Typeface typeface; + + /* The text size. */ + public int pixelSize; + + public + Sdk7FontObject (Sdk7Typeface typeface, int pixelSize) + { + float totalWidth; + String testWidth, testString; + + this.typeface = typeface; + this.pixelSize = pixelSize; + + family = typeface.familyName; + adstyle = null; + weight = typeface.weight; + slant = typeface.slant; + spacing = typeface.spacing; + width = typeface.width; + + /* Compute the ascent and descent. */ + typeface.typefacePaint.setTextSize (pixelSize); + ascent + = Math.round (-typeface.typefacePaint.ascent ()); + descent + = Math.round (typeface.typefacePaint.descent ()); + + /* Compute the average width. */ + testString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + totalWidth = typeface.typefacePaint.measureText (testString); + + if (totalWidth > 0) + avgwidth = Math.round (totalWidth + / testString.length ()); + + /* Android doesn't expose the font average width and height + information, so this will have to do. */ + minWidth = maxWidth = avgwidth; + + /* This is different from avgwidth in the font spec! */ + averageWidth = avgwidth; + + /* Set the space width. */ + totalWidth = typeface.typefacePaint.measureText (" "); + spaceWidth = Math.round (totalWidth); + + /* Set the height and default ascent. */ + height = ascent + descent; + defaultAscent = ascent; + } + }; + + private String[] fontFamilyList; + private Sdk7Typeface[] typefaceList; + private Sdk7Typeface fallbackTypeface; + + public + EmacsSdk7FontDriver () + { + int i; + File systemFontsDirectory, fontFile; + Typeface typeface; + + systemFontsDirectory = new File ("/system/fonts"); + + fontFamilyList = systemFontsDirectory.list (); + typefaceList = new Sdk7Typeface[fontFamilyList.length]; + + /* It would be nice to avoid opening each and every font upon + startup. But that doesn't seem to be possible on + Android. */ + + for (i = 0; i < fontFamilyList.length; ++i) + { + fontFile = new File (systemFontsDirectory, + fontFamilyList[i]); + typeface = Typeface.createFromFile (fontFile); + typefaceList[i] = new Sdk7Typeface (fontFile.getName (), + typeface); + } + + fallbackTypeface = new Sdk7Typeface ("monospace", + Typeface.MONOSPACE); + } + + private boolean + checkMatch (Sdk7Typeface typeface, FontSpec fontSpec) + { + if (fontSpec.family != null + && !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; + + if (fontSpec.spacing != null + && !fontSpec.spacing.equals (typeface.spacing)) + return false; + + if (fontSpec.weight != null + && !fontSpec.weight.equals (typeface.weight)) + return false; + + if (fontSpec.width != null + && !fontSpec.width.equals (typeface.width)) + return false; + + return true; + } + + @Override + public FontEntity[] + list (FontSpec fontSpec) + { + LinkedList list; + int i; + + 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]); + } + + @Override + public FontEntity + match (FontSpec fontSpec) + { + FontEntity[] entities; + int i; + + entities = this.list (fontSpec); + + if (entities.length == 0) + return new Sdk7FontEntity (fallbackTypeface); + + return entities[0]; + } + + @Override + public String[] + listFamilies () + { + return fontFamilyList; + } + + @Override + public FontObject + openFont (FontEntity fontEntity, int pixelSize) + { + return new Sdk7FontObject (((Sdk7FontEntity) fontEntity).typeface, + pixelSize); + } + + @Override + public int + hasChar (FontSpec font, char charCode) + { + float missingGlyphWidth, emGlyphWidth, width; + Rect rect1, rect2; + Paint paint; + Sdk7FontObject fontObject; + + if (font instanceof Sdk7FontObject) + { + fontObject = (Sdk7FontObject) font; + paint = fontObject.typeface.typefacePaint; + } + else + paint = ((Sdk7FontEntity) font).typeface.typefacePaint; + + paint.setTextSize (10); + + if (Character.isWhitespace (charCode)) + return 1; + + missingGlyphWidth = paint.measureText (TOFU_STRING); + emGlyphWidth = paint.measureText (EM_STRING); + width = paint.measureText ("" + charCode); + + if (width == 0f) + return 0; + + if (width != missingGlyphWidth) + return 1; + + rect1 = new Rect (); + rect2 = new Rect (); + + paint.getTextBounds (TOFU_STRING, 0, TOFU_STRING.length (), + rect1); + paint.getTextBounds ("" + charCode, 0, 1, rect2); + return rect1.equals (rect2) ? 1 : 0; + } + + private void + textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics, + 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. + + The origin is at 0, 0, and lbearing is the distance counting + rightwards from the origin to the left most pixel in the glyph + raster. rbearing is the distance between the origin and the + rightmost pixel in the glyph raster. ascent is the distance + counting upwards between the the topmost pixel in the glyph + raster. descent is the distance (once again counting + downwards) between the origin and the bottommost pixel in the + glyph raster. + + width is the distance between the origin and the origin of any + character to the right. */ + + metrics.lbearing = (short) bounds.left; + metrics.rbearing = (short) bounds.right; + metrics.ascent = (short) -bounds.top; + metrics.descent = (short) bounds.bottom; + metrics.width = (short) Math.round (width[0]); + } + + @Override + public void + textExtents (FontObject font, int code[], FontMetrics fontMetrics[]) + { + int i; + Paint paintCache; + Rect boundsCache; + Sdk7FontObject fontObject; + + 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], + paintCache, boundsCache); + } + + @Override + public int + encodeChar (FontObject fontObject, char charCode) + { + return charCode; + } +}; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java new file mode 100644 index 00000000000..311226e6f7e --- /dev/null +++ b/java/org/gnu/emacs/EmacsService.java @@ -0,0 +1,384 @@ +/* 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.Runnable; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +import android.graphics.Canvas; +import android.graphics.Bitmap; +import android.graphics.Point; + +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.util.Log; + +class Holder +{ + T thing; +}; + +/* EmacsService is the service that starts the thread running Emacs + and handles requests by that Emacs instance. */ + +public class EmacsService extends Service +{ + public static final String TAG = "EmacsService"; + public static final int MAX_PENDING_REQUESTS = 256; + public static volatile EmacsService SERVICE; + + private EmacsThread thread; + 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 (); + }; + + @Override + public int + onStartCommand (Intent intent, int flags, int startId) + { + return START_NOT_STICKY; + } + + @Override + public IBinder + onBind (Intent intent) + { + return null; + } + + @TargetApi (Build.VERSION_CODES.GINGERBREAD) + String + getLibraryDirectory () + { + int apiLevel; + Context context; + + context = getApplicationContext (); + apiLevel = android.os.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 () + { + AssetManager manager; + Context app_context; + String filesDir, libDir; + + SERVICE = this; + handler = new Handler (Looper.getMainLooper ()); + manager = getAssets (); + app_context = getApplicationContext (); + + try + { + /* Configure Emacs with the asset manager and other necessary + parameters. */ + filesDir = app_context.getFilesDir ().getCanonicalPath (); + libDir = getLibraryDirectory (); + + Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir + + " and libDir = " + libDir); + + EmacsNative.setEmacsParams (manager, filesDir, libDir, + this); + + /* Start the thread that runs Emacs. */ + thread = new EmacsThread (this); + thread.start (); + } + catch (IOException exception) + { + EmacsNative.emacsAbort (); + return; + } + } + + + + /* Functions from here on must only be called from the Emacs + thread. */ + + void + runOnUiThread (Runnable runnable) + { + handler.post (runnable); + } + + EmacsView + getEmacsView (final EmacsWindow window) + { + Runnable runnable; + final Holder view; + + view = new Holder (); + + runnable = new Runnable () { + public void + run () + { + synchronized (this) + { + view.thing = new EmacsView (window); + notify (); + } + } + }; + + synchronized (runnable) + { + runOnUiThread (runnable); + + try + { + runnable.wait (); + } + catch (InterruptedException e) + { + EmacsNative.emacsAbort (); + } + } + + 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 + 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 (); + } + + public void + fillPolygon (EmacsDrawable drawable, EmacsGC gc, + Point points[]) + { + EmacsPaintReq req; + + ensurePaintQueue (); + + req = new EmacsFillPolygon (drawable, points, + gc.immutableGC ()); + paintQueue.appendPaintOperation (req); + checkFlush (); + } + + public void + drawRectangle (EmacsDrawable drawable, EmacsGC gc, + int x, int y, int width, int height) + { + EmacsPaintReq req; + + 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 ()); + paintQueue.appendPaintOperation (req); + checkFlush (); + } + + 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 (); + } + + 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 (); + } + + public void + copyArea (EmacsDrawable srcDrawable, EmacsDrawable dstDrawable, + EmacsGC gc, + 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 (); + } + + public void + clearWindow (EmacsWindow window) + { + window.clearWindow (); + } + + public void + clearArea (EmacsWindow window, int x, int y, int width, + int height) + { + window.clearArea (x, y, width, height); + } +}; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java new file mode 100644 index 00000000000..194f6ad37a3 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -0,0 +1,83 @@ +/* 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.SurfaceView; +import android.view.SurfaceHolder; + +import android.graphics.Canvas; +import android.graphics.Rect; + +public class EmacsSurfaceView extends SurfaceView +{ + private boolean created; + + public + EmacsSurfaceView (final EmacsView view) + { + super (view.getContext ()); + + getHolder ().addCallback (new SurfaceHolder.Callback () { + @Override + public void + surfaceChanged (SurfaceHolder holder, int format, + int width, int height) + { + + } + + @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 (); + } + + @Override + public void + surfaceDestroyed (SurfaceHolder holder) + { + created = false; + } + }); + } + + public boolean + isCreated () + { + return created; + } + + public Canvas + lockCanvas (Rect damage) + { + return getHolder ().lockCanvas (damage); + } + + public void + unlockCanvasAndPost (Canvas canvas) + { + getHolder ().unlockCanvasAndPost (canvas); + } +}; diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java new file mode 100644 index 00000000000..e9281a13391 --- /dev/null +++ b/java/org/gnu/emacs/EmacsThread.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 java.lang.Thread; + +public class EmacsThread extends Thread +{ + EmacsService context; + + public + EmacsThread (EmacsService service) + { + context = service; + } + + public void + run () + { + String args[]; + + args = new String[] { "android-emacs", }; + + /* Run the native code now. */ + EmacsNative.initEmacs (args); + } +}; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java new file mode 100644 index 00000000000..237946d6366 --- /dev/null +++ b/java/org/gnu/emacs/EmacsView.java @@ -0,0 +1,211 @@ +/* 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.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; + +/* This is an Android view which has a back and front buffer. When + swapBuffers is called, the back buffer is swapped to the front + buffer, and any damage is invalidated. frontBitmap and backBitmap + are modified and used both from the UI and the Emacs thread. As a + result, there is a lock held during all drawing operations. + + It is also a ViewGroup, as it also lays out children. */ + +public class EmacsView extends ViewGroup +{ + public static final String TAG = "EmacsView"; + + /* The associated EmacsWindow. */ + public EmacsWindow window; + + /* The buffer bitmap. */ + public Bitmap bitmap; + + /* The associated canvases. */ + public Canvas canvas; + + /* The damage region. */ + public Region damageRegion; + + /* The paint. */ + public Paint paint; + + /* The associated surface view. */ + private EmacsSurfaceView surfaceView; + + public + EmacsView (EmacsWindow window) + { + super (EmacsService.SERVICE); + + this.window = window; + this.damageRegion = new Region (); + this.paint = new Paint (); + + /* Create the surface view. */ + this.surfaceView = new EmacsSurfaceView (this); + + setFocusable (FOCUSABLE); + addView (this.surfaceView); + } + + @Override + protected void + onMeasure (int widthMeasureSpec, int heightMeasureSpec) + { + Rect measurements; + int width, height; + + /* Return the width and height of the window regardless of what + the parent says. */ + measurements = window.getGeometry (); + + width = measurements.width (); + height = measurements.height (); + + /* Now apply any extra requirements in widthMeasureSpec and + heightMeasureSpec. */ + + if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.EXACTLY) + width = MeasureSpec.getSize (widthMeasureSpec); + else if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.AT_MOST + && width > MeasureSpec.getSize (widthMeasureSpec)) + width = MeasureSpec.getSize (widthMeasureSpec); + + if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.EXACTLY) + height = MeasureSpec.getSize (heightMeasureSpec); + else if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.AT_MOST + && height > MeasureSpec.getSize (heightMeasureSpec)) + height = MeasureSpec.getSize (heightMeasureSpec); + + super.setMeasuredDimension (width, height); + } + + @Override + protected void + onLayout (boolean changed, int left, int top, int right, + int bottom) + { + int count, i; + View child; + Rect windowRect; + + if (changed) + { + 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); + } + + count = getChildCount (); + + for (i = 0; i < count; ++i) + { + child = getChildAt (i); + + if (child == surfaceView) + /* The child is the surface view, so give it the entire + view. */ + child.layout (left, top, right, bottom); + else if (child.getVisibility () != GONE) + { + if (!(child instanceof EmacsView)) + continue; + + /* What to do: lay out the view precisely according to its + window rect. */ + windowRect = ((EmacsView) child).window.getGeometry (); + child.layout (windowRect.left, windowRect.top, + windowRect.right, windowRect.bottom); + } + } + } + + public void + damageRect (Rect damageRect) + { + damageRegion.union (damageRect); + } + + public void + swapBuffers () + { + Bitmap back; + Canvas canvas; + Rect damageRect; + + if (damageRegion.isEmpty ()) + return; + + if (!surfaceView.isCreated ()) + return; + + if (bitmap == null) + return; + + /* Lock the canvas with the specified damage. */ + damageRect = damageRegion.getBounds (); + canvas = surfaceView.lockCanvas (damageRect); + + /* Return if locking the canvas failed. */ + if (canvas == null) + return; + + /* Copy from the back buffer to the canvas. */ + canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + + /* Unlock the canvas and clear the damage. */ + surfaceView.unlockCanvasAndPost (canvas); + damageRegion.setEmpty (); + } + + @Override + public boolean + onKeyDown (int keyCode, KeyEvent event) + { + window.onKeyDown (keyCode, event); + return true; + } + + @Override + public boolean + onKeyUp (int keyCode, KeyEvent event) + { + window.onKeyUp (keyCode, event); + return true; + } +}; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java new file mode 100644 index 00000000000..28db04a261d --- /dev/null +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -0,0 +1,336 @@ +/* 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.IllegalStateException; +import java.util.ArrayList; +import java.util.List; + +import android.graphics.Rect; +import android.graphics.Canvas; +import android.graphics.Bitmap; +import android.graphics.Point; + +import android.view.View; +import android.view.ViewGroup; +import android.view.KeyEvent; + +/* This defines a window, which is a handle. Windows represent a + rectangular subset of the screen with their own contents. + + Windows either have a parent window, in which case their views are + attached to the parent's view, or are "floating", in which case + their views are attached to the parent activity (if any), else + nothing. + + Views are also drawables, meaning they can accept drawing + requests. */ + +public class EmacsWindow extends EmacsHandleObject + implements EmacsDrawable +{ + /* The view associated with the window. */ + public EmacsView view; + + /* The geometry of the window. */ + private Rect rect; + + /* The parent window, or null if it is the root window. */ + private EmacsWindow parent; + + /* List of all children in stacking order. This must be kept + consistent! */ + private ArrayList children; + + /* The EmacsActivity currently attached, if it exists. */ + private EmacsActivity attached; + + /* The window background scratch GC. foreground is always the + window background. */ + private EmacsGC scratchGC; + + public + EmacsWindow (short handle, final EmacsWindow parent, int x, int y, + int width, int height) + { + super (handle); + + rect = new Rect (x, y, x + width, y + height); + + /* Create the view from the context's UI thread. */ + view = EmacsService.SERVICE.getEmacsView (this); + this.parent = parent; + children = new ArrayList (); + + /* The window is unmapped by default. */ + view.setVisibility (View.GONE); + + /* 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 + { + /* 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); + } + }); + } + } + + scratchGC = new EmacsGC ((short) 0); + } + + public void + changeWindowBackground (int pixel) + { + /* scratchGC is used as the argument to a FillRectangles req. */ + scratchGC.foreground = pixel; + scratchGC.markDirty (); + } + + public Rect + getGeometry () + { + synchronized (this) + { + /* Huh, this is it. */ + return rect; + } + } + + @Override + public void + destroyHandle () throws IllegalStateException + { + synchronized (this) + { + if (!children.isEmpty ()) + throw new IllegalStateException ("Trying to destroy window with " + + "children!"); + } + + /* Notice that the child has been destroyed. */ + EmacsService.SERVICE.noticeChildDestroyed (this); + + /* Remove the view from its parent and make it invisible. */ + view.post (new Runnable () { + public void + run () + { + view.setVisibility (View.GONE); + + if (view.getParent () != null) + ((ViewGroup) view.getParent ()).removeView (view); + + if (attached != null) + attached.makeAvailable (); + } + }); + + super.destroyHandle (); + } + + public void + setActivity (EmacsActivity activity) + { + synchronized (this) + { + activity = activity; + } + } + + public void + viewLayout (int left, int top, int right, int bottom) + { + 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 ()); + } + } + + public void + requestViewLayout () + { + view.post (new Runnable () { + @Override + public void + run () + { + view.requestLayout (); + } + }); + } + + public void + resizeWindow (int width, int height) + { + synchronized (this) + { + rect.right = rect.left + width; + rect.bottom = rect.top + height; + } + } + + public void + moveWindow (int x, int y) + { + int width, height; + + synchronized (this) + { + width = rect.width (); + height = rect.height (); + + rect.left = x; + rect.top = y; + rect.right = x + width; + rect.bottom = y + height; + + requestViewLayout (); + } + } + + public void + mapWindow () + { + view.post (new Runnable () { + @Override + public void + run () + { + view.setVisibility (View.VISIBLE); + } + }); + } + + public void + unmapWindow () + { + view.post (new Runnable () { + @Override + public void + run () + { + view.setVisibility (View.GONE); + } + }); + } + + @Override + public Canvas + lockCanvas () + { + if (view.canvas != null) + return view.canvas; + + return null; + } + + @Override + public void + unlockCanvas () + { + + } + + @Override + public void + damageRect (Rect damageRect) + { + view.damageRect (damageRect); + } + + 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 (); + } + }); + } + + public void + clearWindow () + { + synchronized (this) + { + EmacsService.SERVICE.fillRectangle (this, scratchGC, + 0, 0, rect.width (), + rect.height ()); + } + } + + public void + clearArea (int x, int y, int width, int height) + { + EmacsService.SERVICE.fillRectangle (this, scratchGC, + x, y, width, height); + } + + @Override + public Bitmap + getBitmap () + { + return view.bitmap; + } + + public void + onKeyDown (int keyCode, KeyEvent event) + { + EmacsNative.sendKeyPress (this.handle, + event.getEventTime (), + event.getModifiers (), + keyCode); + } + + public void + onKeyUp (int keyCode, KeyEvent event) + { + EmacsNative.sendKeyRelease (this.handle, + event.getEventTime (), + event.getModifiers (), + keyCode); + } +}; -- cgit v1.2.1 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. --- 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 ++++++++++++++++++ 15 files changed, 895 insertions(+), 228 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') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index cacd5f13e60..67f411a38c3 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -32,65 +32,114 @@ import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; public class EmacsActivity extends Activity + implements EmacsWindowAttachmentManager.WindowConsumer { public static final String TAG = "EmacsActivity"; - /* List of all activities that do not have an associated - EmacsWindow. */ - public static List 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 (); + } + } +}; -- cgit v1.2.1 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') 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.2.1 From 86fe89312893bbc8aa47605afbf8da8cd5a12faf Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 8 Jan 2023 15:39:28 +0800 Subject: Delete unused files * java/org/gnu/emacs/EmacsPaintQueue.java * java/org/gnu/emacs/EmacsPaintReq.java: Remove files. --- java/org/gnu/emacs/EmacsPaintQueue.java | 124 -------------------------------- java/org/gnu/emacs/EmacsPaintReq.java | 33 --------- 2 files changed, 157 deletions(-) delete mode 100644 java/org/gnu/emacs/EmacsPaintQueue.java delete mode 100644 java/org/gnu/emacs/EmacsPaintReq.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsPaintQueue.java b/java/org/gnu/emacs/EmacsPaintQueue.java deleted file mode 100644 index f4840dbf5ae..00000000000 --- a/java/org/gnu/emacs/EmacsPaintQueue.java +++ /dev/null @@ -1,124 +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 java.util.LinkedList; -import java.util.List; - -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; - -public class EmacsPaintQueue -{ - /* Queue of paint operations. This is modified from the Emacs - thread, and entire paint queues are periodically flushed to the - application thread where it is executed. */ - private List paintOperations; - - /* Number of operations in this queue. */ - public int numRequests; - - public - EmacsPaintQueue () - { - paintOperations = new LinkedList (); - } - - public void - run () - { - EmacsDrawable drawable, last; - Canvas canvas; - EmacsGC gc; - int i; - Paint paint; - Rect rect, offsetRect, copyRect; - - canvas = null; - last = null; - gc = null; - paint = new Paint (); - - for (EmacsPaintReq req : paintOperations) - { - drawable = req.getDrawable (); - canvas = drawable.lockCanvas (); - - if (canvas == null) - /* No canvas is currently available. */ - continue; - - gc = req.getGC (); - rect = req.getRect (); - - drawable.damageRect (rect); - - if (gc.clip_rects == null) - { - /* No clipping is applied. Just draw and continue. */ - req.paintTo (canvas, paint, gc); - continue; - } - - if (gc.clip_rects != null && gc.clip_rects.length > 0) - { - 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 - { - /* There are multiple clip rects. Android doesn't let - programs use RegionOp.UNION on the clip rectangle, - so Emacs must iterate over each intersection and - paint it manually. This seems inefficient but - thankfully Emacs never seems to use more than one - clip rect. */ - - for (i = 0; i < gc.clip_rects.length; ++i) - { - copyRect = new Rect (gc.clip_rects[i]); - - if (copyRect.intersect (rect)) - { - canvas.save (); - canvas.clipRect (copyRect); - req.paintTo (canvas, paint, gc); - canvas.restore (); - } - } - } - } - } - } - - public void - appendPaintOperation (EmacsPaintReq req) - { - paintOperations.add (req); - numRequests++; - } -}; diff --git a/java/org/gnu/emacs/EmacsPaintReq.java b/java/org/gnu/emacs/EmacsPaintReq.java deleted file mode 100644 index 5b14b005093..00000000000 --- a/java/org/gnu/emacs/EmacsPaintReq.java +++ /dev/null @@ -1,33 +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.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; - -public interface EmacsPaintReq -{ - public EmacsDrawable getDrawable (); - public EmacsGC getGC (); - public void paintTo (Canvas canvas, Paint paint, - EmacsGC immutableGC); - public Rect getRect (); -}; -- cgit v1.2.1 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. --- 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 ++- 8 files changed, 856 insertions(+), 92 deletions(-) (limited to 'java/org/gnu') 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); } }; -- cgit v1.2.1 From 24910d3f375a11360c66b742e1054b55e9e25ccc Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 13 Jan 2023 16:08:11 +0800 Subject: Fix crashes in Android port * java/org/gnu/emacs/EmacsService.java (queryTree): Fix NULL pointer dereference. * src/android.c (android_query_tree): Set *nchildren_return. --- java/org/gnu/emacs/EmacsService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 01a1695f385..c008300dd3a 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -304,7 +304,9 @@ public class EmacsService extends Service array = new short[windowList.size () + 1]; i = 1; - array[0] = window.parent != null ? 0 : window.parent.handle; + array[0] = (window == null + ? 0 : (window.parent != null + ? window.parent.handle : 0)); for (EmacsWindow treeWindow : windowList) array[i++] = treeWindow.handle; -- cgit v1.2.1 From 28a9baccd4c8e997895d3adb3cfce4a11fa29896 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 14 Jan 2023 09:34:53 +0800 Subject: Improve reliability of Android build system * .gitignore: Add new files. * INSTALL.android: New file. * Makefile.in (clean_dirs): Clean xcompile as well. * admin/merge-gnulib (avoided_flags): Import gnulib into Android directory as well. * doc/emacs/android.texi (Android): * doc/emacs/emacs.texi (Top): New node `Android'. * java/org/gnu/emacs/EmacsThread.java (run): Use right executable name. * lib/Makefile.in (ANDROID_CFLAGS): Use better way to refer to /src. (vpath): Delete ugly block of vpath statements. (mostlyclean): Remove Makefile.android. * lib/fpending.c (__fpending): * lib/open.c: * lib/unistd.c (_GL_UNISTD_INLINE): Revert changes to gnulib in lib/. * src/android.h: * src/androidterm.c: Fix build. * xcompile/Makefile.in (LIB_SRCDIR): (LIBSRC_BINARIES, src/verbose.mk): (PRE_BUILD_DEPS, PHONY): Use gnulib in xcompile/lib/ as opposed to lib/. * xcompile/README: Adjust README. --- java/org/gnu/emacs/EmacsThread.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index e9281a13391..0882753747f 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -36,7 +36,7 @@ public class EmacsThread extends Thread { String args[]; - args = new String[] { "android-emacs", }; + args = new String[] { "libandroid-emacs.so", }; /* Run the native code now. */ EmacsNative.initEmacs (args); -- cgit v1.2.1 From 2b87ab7b27163fbd7b6b64c5a44e26b0e692c00a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 14 Jan 2023 22:12:16 +0800 Subject: Update Android port * java/Makefile.in (clean): Fix distclean and bootstrap-clean rules. * java/debug.sh (jdb_port): (attach_existing): (num_pids): (line): Add new options to upload a gdbserver binary to the device. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): Make focusedActivities public. * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu): New class. * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): Fix bounds computation. * java/org/gnu/emacs/EmacsGC.java (markDirty): Set stroke width explicitly. * java/org/gnu/emacs/EmacsService.java (EmacsService) (getLocationOnScreen, nameKeysym): New functions. * java/org/gnu/emacs/EmacsView.java (EmacsView): Disable focus highlight. (onCreateContextMenu, popupMenu, cancelPopupMenu): New functions. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow): Implement a kind of ``override redirect'' window for tooltips. * src/android.c (struct android_emacs_service): New method `name_keysym'. (android_run_select_thread, android_init_events): (android_select): Release select thread on semaphores instead of signals to avoid one nasty race on SIGUSR2 delivery. (android_init_emacs_service): Initialize new method. (android_create_window): Handle CW_OVERRIDE_REDIRECT. (android_move_resize_window, android_map_raised) (android_translate_coordinates, android_get_keysym_name) (android_build_string, android_exception_check): New functions. * src/android.h: Update prototypes. * src/androidfns.c (android_set_parent_frame, Fx_create_frame) (unwind_create_tip_frame, android_create_tip_frame) (android_hide_tip, compute_tip_xy, Fx_show_tip, Fx_hide_tip) (syms_of_androidfns): Implement tooltips and iconification reporting. * src/androidgui.h (enum android_window_value_mask): Add CWOverrideRedirect. (struct android_set_window_attributes): Add `override_redirect'. (ANDROID_IS_MODIFIER_KEY): Recognize Caps Lock. * src/androidmenu.c (struct android_emacs_context_menu): New struct. (android_init_emacs_context_menu, android_unwind_local_frame) (android_push_local_frame, android_menu_show, init_androidmenu): New functions. * src/androidterm.c (handle_one_android_event): Fix NULL pointer dereference. (android_fullscreen_hook): Handle fullscreen correctly. (android_draw_box_rect): Fix top line. (get_keysym_name): Implement function. (android_create_terminal): Remove scroll bar stubs and add menu hook. * src/androidterm.h: Update prototypes. * src/emacs.c (android_emacs_init): Initialize androidmenu.c. * xcompile/Makefile.in: Fix clean rules. --- java/org/gnu/emacs/EmacsActivity.java | 2 +- java/org/gnu/emacs/EmacsContextMenu.java | 213 +++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsDrawRectangle.java | 2 +- java/org/gnu/emacs/EmacsGC.java | 1 + java/org/gnu/emacs/EmacsService.java | 43 +++++- java/org/gnu/emacs/EmacsView.java | 47 +++++++ java/org/gnu/emacs/EmacsWindow.java | 161 +++++++++++++++++++--- 7 files changed, 448 insertions(+), 21 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsContextMenu.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 2b661024842..4cd286d1e89 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -43,7 +43,7 @@ public class EmacsActivity extends Activity private FrameLayout layout; /* List of activities with focus. */ - private static List focusedActivities; + public static List focusedActivities; /* The currently focused window. */ public static EmacsWindow focusedWindow; diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java new file mode 100644 index 00000000000..8d7ae08b257 --- /dev/null +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -0,0 +1,213 @@ +/* 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.List; +import java.util.ArrayList; + +import android.content.Context; +import android.content.Intent; + +import android.os.Bundle; + +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +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) + menu. */ + +public class EmacsContextMenu +{ + private class Item + { + public int itemID; + public String itemName; + public EmacsContextMenu subMenu; + public boolean isEnabled; + }; + + public List menuItems; + public String title; + private EmacsContextMenu parent; + + /* Create a context menu with no items inside and the title TITLE, + which may be NULL. */ + + public static EmacsContextMenu + createContextMenu (String title) + { + EmacsContextMenu menu; + + menu = new EmacsContextMenu (); + menu.menuItems = new ArrayList (); + menu.title = title; + + return menu; + } + + /* Add a normal menu item to the context menu with the id ITEMID and + the name ITEMNAME. Enable it if ISENABLED, else keep it + disabled. */ + + public void + addItem (int itemID, String itemName, boolean isEnabled) + { + Item item; + + item = new Item (); + item.itemID = itemID; + item.itemName = itemName; + item.isEnabled = isEnabled; + + menuItems.add (item); + } + + /* Create a disabled menu item with the name ITEMNAME. */ + + public void + addPane (String itemName) + { + Item item; + + item = new Item (); + item.itemName = itemName; + + menuItems.add (item); + } + + /* Add a submenu to the context menu with the specified title and + item name. */ + + public EmacsContextMenu + addSubmenu (String itemName, String title) + { + EmacsContextMenu submenu; + Item item; + + item = new Item (); + item.itemID = 0; + item.itemName = itemName; + item.subMenu = createContextMenu (title); + item.subMenu.parent = this; + + menuItems.add (item); + return item.subMenu; + } + + /* Add the contents of this menu to MENU. */ + + private void + inflateMenuItems (Menu menu) + { + Intent intent; + MenuItem menuItem; + Menu submenu; + + for (Item item : menuItems) + { + if (item.subMenu != null) + { + /* This is a submenu. Create the submenu and add the + contents of the menu to it. */ + submenu = menu.addSubMenu (item.itemName); + inflateMenuItems (submenu); + } + else + { + menuItem = menu.add (item.itemName); + + /* If the item ID is zero, then disable the item. */ + if (item.itemID == 0 || !item.isEnabled) + menuItem.setEnabled (false); + } + } + } + + /* Enter the items in this context menu to MENU. Create each menu + item with an Intent containing a Bundle, where the key + "emacs:menu_item_hi" maps to the high 16 bits of the + corresponding item ID, and the key "emacs:menu_item_low" maps to + the low 16 bits of the item ID. */ + + public void + expandTo (Menu menu) + { + inflateMenuItems (menu); + } + + /* Return the parent or NULL. */ + + public EmacsContextMenu + parent () + { + return parent; + } + + /* Like display, but does the actual work and runs in the main + thread. */ + + private boolean + display1 (EmacsWindow window, int xPosition, int yPosition) + { + return window.view.popupMenu (this, xPosition, yPosition); + } + + /* Display this context menu on WINDOW, at xPosition and + yPosition. */ + + public boolean + display (final EmacsWindow window, final int xPosition, + final int yPosition) + { + Runnable runnable; + final Holder rc; + + rc = new Holder (); + + runnable = new Runnable () { + @Override + public void + run () + { + synchronized (this) + { + rc.thing = display1 (window, xPosition, yPosition); + notify (); + } + } + }; + + try + { + runnable.wait (); + } + catch (InterruptedException e) + { + EmacsNative.emacsAbort (); + } + + return rc.thing; + } +}; diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index b42e9556e8c..84ff498847b 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, y, x + width, y + height); + rect = new Rect (x + 1, y + 1, x + width, y + height); paint.setStyle (Paint.Style.STROKE); diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java index caa5c91edd4..c579625f3f7 100644 --- a/java/org/gnu/emacs/EmacsGC.java +++ b/java/org/gnu/emacs/EmacsGC.java @@ -93,6 +93,7 @@ public class EmacsGC extends EmacsHandleObject else real_clip_rects = clip_rects; + gcPaint.setStrokeWidth (1f); gcPaint.setColor (foreground | 0xff000000); gcPaint.setXfermode (function == GC_XOR ? xorAlu : srcInAlu); diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index c008300dd3a..f935b63fa0d 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -29,6 +29,7 @@ import android.graphics.Point; import android.view.View; import android.view.InputDevice; +import android.view.KeyEvent; import android.annotation.TargetApi; import android.app.Service; @@ -150,13 +151,13 @@ public class EmacsService extends Service /* Functions from here on must only be called from the Emacs thread. */ - void + public void runOnUiThread (Runnable runnable) { handler.post (runnable); } - EmacsView + public EmacsView getEmacsView (final EmacsWindow window, final int visibility, final boolean isFocusedByDefault) { @@ -196,6 +197,38 @@ public class EmacsService extends Service return view.thing; } + public void + getLocationOnScreen (final EmacsView view, final int[] coordinates) + { + Runnable runnable; + + runnable = new Runnable () { + public void + run () + { + synchronized (this) + { + view.getLocationOnScreen (coordinates); + notify (); + } + } + }; + + synchronized (runnable) + { + runOnUiThread (runnable); + + try + { + runnable.wait (); + } + catch (InterruptedException e) + { + EmacsNative.emacsAbort (); + } + } + } + public void fillRectangle (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) @@ -368,4 +401,10 @@ public class EmacsService extends Service return false; } + + public String + nameKeysym (int keysym) + { + return KeyEvent.keyCodeToString (keysym); + } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 41acabab97b..1391f630be0 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -21,6 +21,7 @@ package org.gnu.emacs; import android.content.res.ColorStateList; +import android.view.ContextMenu; import android.view.View; import android.view.KeyEvent; import android.view.MotionEvent; @@ -73,6 +74,12 @@ public class EmacsView extends ViewGroup next call to getBitmap. */ private Rect bitmapDirty; + /* Whether or not a popup is active. */ + private boolean popupActive; + + /* The current context menu. */ + private EmacsContextMenu contextMenu; + public EmacsView (EmacsWindow window) { @@ -98,6 +105,10 @@ public class EmacsView extends ViewGroup /* Get rid of the foreground and background tint. */ setBackgroundTintList (null); setForegroundTintList (null); + + /* Get rid of the default focus highlight. */ + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) + setDefaultFocusHighlightEnabled (false); } private void @@ -423,4 +434,40 @@ public class EmacsView extends ViewGroup removeView (surfaceView); addView (surfaceView, 0); } + + @Override + protected void + onCreateContextMenu (ContextMenu menu) + { + if (contextMenu == null) + return; + + contextMenu.expandTo (menu); + } + + public boolean + popupMenu (EmacsContextMenu menu, int xPosition, + int yPosition) + { + if (popupActive) + return false; + + contextMenu = menu; + + /* On API 21 or later, use showContextMenu (float, float). */ + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) + return showContextMenu ((float) xPosition, (float) yPosition); + else + return showContextMenu (); + } + + public void + cancelPopupMenu () + { + if (!popupActive) + throw new IllegalStateException ("cancelPopupMenu called without" + + " popupActive set"); + + contextMenu = null; + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 1f8596dba50..6effa79d1a4 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -24,16 +24,22 @@ import java.util.ArrayList; import java.util.List; import java.util.HashMap; +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; @@ -110,9 +116,17 @@ public class EmacsWindow extends EmacsHandleObject not the window should be focusable. */ private boolean dontFocusOnMap, dontAcceptFocus; + /* Whether or not the window is override-redirect. An + override-redirect window always has its own system window. */ + private boolean overrideRedirect; + + /* The window manager that is the parent of this window. NULL if + there is no such window manager. */ + private WindowManager windowManager; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, - int width, int height) + int width, int height, boolean overrideRedirect) { super (handle); @@ -124,6 +138,7 @@ public class EmacsWindow extends EmacsHandleObject view = EmacsService.SERVICE.getEmacsView (this, View.GONE, parent == null); this.parent = parent; + this.overrideRedirect = overrideRedirect; /* Create the list of children. */ children = new ArrayList (); @@ -180,7 +195,7 @@ public class EmacsWindow extends EmacsHandleObject public void run () { - View parent; + ViewManager parent; EmacsWindowAttachmentManager manager; if (EmacsActivity.focusedWindow == EmacsWindow.this) @@ -189,10 +204,15 @@ public class EmacsWindow extends EmacsHandleObject manager = EmacsWindowAttachmentManager.MANAGER; view.setVisibility (View.GONE); - parent = (View) view.getParent (); + /* If the window manager is set, use that instead. */ + if (windowManager != null) + parent = windowManager; + else + parent = (ViewManager) view.getParent (); + windowManager = null; if (parent != null) - ((ViewGroup) parent).removeView (view); + parent.removeView (view); manager.detachWindow (EmacsWindow.this); } @@ -247,6 +267,10 @@ public class EmacsWindow extends EmacsHandleObject public void run () { + if (overrideRedirect) + /* Set the layout parameters again. */ + view.setLayoutParams (getWindowLayoutParams ()); + view.mustReportLayout = true; view.requestLayout (); } @@ -284,6 +308,39 @@ public class EmacsWindow extends EmacsHandleObject } } + private WindowManager.LayoutParams + getWindowLayoutParams () + { + WindowManager.LayoutParams params; + int flags, type; + Rect rect; + + flags = 0; + rect = getGeometry (); + flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; + + params + = new WindowManager.LayoutParams (rect.width (), rect.height (), + rect.left, rect.top, + type, flags, + PixelFormat.RGBA_8888); + params.gravity = Gravity.TOP | Gravity.LEFT; + return params; + } + + private Context + findSuitableActivityContext () + { + /* Find a recently focused activity. */ + if (!EmacsActivity.focusedActivities.isEmpty ()) + return EmacsActivity.focusedActivities.get (0); + + /* Return the service context, which probably won't work. */ + return EmacsService.SERVICE; + } + public void mapWindow () { @@ -300,20 +357,60 @@ public class EmacsWindow extends EmacsHandleObject run () { EmacsWindowAttachmentManager manager; + WindowManager windowManager; + Context ctx; + Object tem; + WindowManager.LayoutParams params; /* 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 (); + if (!overrideRedirect) + { + 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 + { + /* But if the window is an override-redirect window, + then: + + - Find an activity that is currently active. + + - Map the window as a panel on top of that + activity using the system window manager. */ + + ctx = findSuitableActivityContext (); + tem = ctx.getSystemService (Context.WINDOW_SERVICE); + windowManager = (WindowManager) tem; + + /* Calculate layout parameters. */ + params = getWindowLayoutParams (); + view.setLayoutParams (params); + + /* Attach the view. */ + try + { + windowManager.addView (view, params); + + /* Record the window manager being used in the + EmacsWindow object. */ + EmacsWindow.this.windowManager = windowManager; + } + catch (Exception e) + { + Log.w (TAG, + "failed to attach override-redirect window, " + e); + } + } } }); } @@ -355,6 +452,11 @@ public class EmacsWindow extends EmacsHandleObject view.setVisibility (View.GONE); + /* Detach the view from the window manager if possible. */ + if (windowManager != null) + windowManager.removeView (view); + windowManager = null; + /* Now that the window is unmapped, unregister it as well. */ manager.detachWindow (EmacsWindow.this); @@ -756,17 +858,23 @@ public class EmacsWindow extends EmacsHandleObject run () { EmacsWindowAttachmentManager manager; - View parent; + ViewManager parent; /* First, detach this window if necessary. */ manager = EmacsWindowAttachmentManager.MANAGER; manager.detachWindow (EmacsWindow.this); /* Also unparent this view. */ - parent = (View) view.getParent (); + + /* If the window manager is set, use that instead. */ + if (windowManager != null) + parent = windowManager; + else + parent = (ViewManager) view.getParent (); + windowManager = null; if (parent != null) - ((ViewGroup) parent).removeView (view); + parent.removeView (view); /* Next, either add this window as a child of the new parent's view, or make it available again. */ @@ -899,4 +1007,23 @@ public class EmacsWindow extends EmacsHandleObject { return dontFocusOnMap; } + + public int[] + translateCoordinates (int x, int y) + { + int[] array; + + /* This is supposed to translate coordinates to the root + window. */ + array = new int[2]; + EmacsService.SERVICE.getLocationOnScreen (view, array); + + /* Now, the coordinates of the view should be in array. Offset X + and Y by them. */ + array[0] += x; + array[1] += y; + + /* Return the resulting coordinates. */ + return array; + } }; -- cgit v1.2.1 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 + 4 files changed, 77 insertions(+), 7 deletions(-) (limited to 'java/org/gnu') 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; } }; -- cgit v1.2.1 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 ++++++++++++++++++++++++++++-- 5 files changed, 78 insertions(+), 7 deletions(-) (limited to 'java/org/gnu') 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 (); + } + } + } }; -- cgit v1.2.1 From ad59d8986aee4498c0427449e024341d1a195a2d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 16 Jan 2023 19:50:02 +0800 Subject: Update Android port * doc/emacs/android.texi (Android, Android Environment): Improve documentation. * doc/lispref/commands.texi (Touchscreen Events): Document changes to touchscreen support. * doc/lispref/display.texi (Defining Faces, Window Systems): * doc/lispref/frames.texi (Frame Layout, Font and Color Parameters): * doc/lispref/os.texi (System Environment): Document Android in various places. * java/org/gnu/emacs/EmacsWindow.java (figureChange): Fix crash. * lisp/loadup.el: ("touch-screen"): Load touch-screen.el. * lisp/pixel-scroll.el: Autoload two functions. * lisp/term/android-win.el: Add require 'touch-screen. * lisp/touch-screen.el (touch-screen-current-tool) (touch-screen-current-timer, touch-screen-delay) (touch-screen-relative-xy, touch-screen-handle-scroll) (touch-screen-handle-timeout, touch-screen-handle-point-update) (touch-screen-handle-point-up, touch-screen-handle-touch) (global-map, touch-screen): New file. * src/android.c (android_run_debug_thread): Fix build on 64 bit systems. (JNICALL, android_put_pixel): Likewise. (android_transform_coordinates, android_four_corners_bilinear) (android_fetch_pixel_bilinear, android_project_image_bilinear) (android_fetch_pixel_nearest_24, android_fetch_pixel_nearest_1) (android_project_image_nearest): New functions. * src/androidgui.h (struct android_transform): New structure. * src/androidterm.c (android_note_mouse_movement): Remove obsolete TODO. (android_get_scale_factor): New function. (android_draw_underwave): Scale underwave correctly. * src/dispextern.h: Support native image transforms on Android. * src/image.c (matrix_identity, matrix_rotate) (matrix_mirror_horizontal, matrix_translate): New functions. (image_set_transform): Implement native image transforms on Android. (Fimage_transforms_p): Implement on Android. * src/keyboard.c (make_lispy_event, syms_of_keyboard): Handle touch screen- menu bar events. * src/sfnt.c: Fix typo in comment. * src/sfntfont-android.c (sfntfont_android_blend, U255TO256) (sfntfont_android_put_glyphs): Avoid redundant swizzling. * src/sfntfont.c (sfntfont_lookup_char): Fix build on 64 bit systems. --- java/org/gnu/emacs/EmacsWindow.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 6effa79d1a4..7181bc89fea 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -637,8 +637,8 @@ public class EmacsWindow extends EmacsHandleObject pointerIndex = event.getActionIndex (); pointerID = event.getPointerId (pointerIndex); pointerMap.put (pointerID, - new Coordinate ((int) event.getX (pointerID), - (int) event.getY (pointerID))); + new Coordinate ((int) event.getX (pointerIndex), + (int) event.getY (pointerIndex))); break; case MotionEvent.ACTION_POINTER_UP: -- cgit v1.2.1 From 1b8258a1f2b6a080a4f0e819aa4a86c1ec2da89f Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 17 Jan 2023 22:10:43 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Fonts): Document that TTC format fonts are now supported. * doc/emacs/emacs.texi (Top): Fix menus. * doc/lispref/commands.texi (Touchscreen Events) (Key Sequence Input): Document changes to touchscreen events. * etc/DEBUG: Describe how to debug 64 bit binaries on Android. * java/org/gnu/emacs/EmacsCopyArea.java (perform): Explicitly recycle copy bitmap. * java/org/gnu/emacs/EmacsDialog.java (EmacsDialog): New class. * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): Use 5 point PolyLine like X, because Android behaves like Postscript on some devices and X elsewhere. * java/org/gnu/emacs/EmacsFillRectangle.java (perform): Explicitly recycle copy bitmap. * java/org/gnu/emacs/EmacsPixmap.java (destroyHandle): Explicitly recycle bitmap and GC if it is big. * java/org/gnu/emacs/EmacsView.java (EmacsView): Make `bitmapDirty' a boolean. (handleDirtyBitmap): Reimplement in terms of that boolean. Explicitly recycle old bitmap and GC. (onLayout): Fix lock up. (onDetachedFromWindow): Recycle bitmap and GC. * java/org/gnu/emacs/EmacsWindow.java (requestViewLayout): Update call to explicitlyDirtyBitmap. * src/android.c (android_run_select_thread, android_select): Really fix android_select. (android_build_jstring): New function. * src/android.h: Update prototypes. * src/androidmenu.c (android_process_events_for_menu): Totally unblock input before process_pending_signals. (android_menu_show): Remove redundant unblock_input and debugging code. (struct android_emacs_dialog, android_init_emacs_dialog) (android_dialog_show, android_popup_dialog, init_androidmenu): Implement popup dialogs on Android. * src/androidterm.c (android_update_tools) (handle_one_android_event, android_frame_up_to_date): Allow tapping tool bar items. (android_create_terminal): Add dialog hook. (android_wait_for_event): Adjust call to android_select. * src/androidterm.h (struct android_touch_point): New field `tool_bar_p'. * src/keyboard.c (read_key_sequence, head_table) (syms_of_keyboard): Prefix touchscreen events with posn. * src/keyboard.h (EVENT_HEAD): Handle touchscreen events. * src/process.c (wait_reading_process_output): Adjust call to android_select. * src/sfnt.c (sfnt_read_table_directory): If the first long turns out to be ttcf, return -1. (sfnt_read_ttc_header): New function. (main): Test TTC support. * src/sfnt.h (struct sfnt_ttc_header): New structure. (enum sfnt_ttc_tag): New enum. * src/sfntfont-android.c (struct sfntfont_android_scanline_buffer): New structure. (GET_SCANLINE_BUFFER): New macro. Try to avoid so much malloc upon accessing the scanline buffer. (sfntfont_android_put_glyphs): Do not use SAFE_ALLOCA to allocate the scaline buffer. (Fandroid_enumerate_fonts): Enumerate ttc fonts too. * src/sfntfont.c (struct sfnt_font_desc): New field `offset'. (sfnt_enum_font_1): Split out enumeration code from sfnt_enum_font. (sfnt_enum_font): Read TTC tables and enumerate each font therein. (sfntfont_open): Seek to the offset specified. * xcompile/Makefile.in (maintainer-clean): Fix depends here. --- java/org/gnu/emacs/EmacsCopyArea.java | 4 + java/org/gnu/emacs/EmacsDialog.java | 333 +++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsDrawRectangle.java | 32 ++- java/org/gnu/emacs/EmacsFillRectangle.java | 3 + java/org/gnu/emacs/EmacsPixmap.java | 23 ++ java/org/gnu/emacs/EmacsView.java | 84 ++++++-- java/org/gnu/emacs/EmacsWindow.java | 2 +- 7 files changed, 452 insertions(+), 29 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsDialog.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 00e817bb97d..5d72a7860c8 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -116,6 +116,7 @@ public class EmacsCopyArea src_x, src_y, width, height); canvas.drawBitmap (bitmap, null, rect, paint); + bitmap.recycle (); } else { @@ -183,6 +184,9 @@ public class EmacsCopyArea paint.setXfermode (overAlu); canvas.drawBitmap (maskBitmap, null, maskRect, paint); gc.resetXfermode (); + + /* Recycle this unused bitmap. */ + maskBitmap.recycle (); } canvas.restore (); diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java new file mode 100644 index 00000000000..5bc8efa5978 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -0,0 +1,333 @@ +/* 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.List; +import java.util.ArrayList; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Context; +import android.util.Log; + +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.FrameLayout; + +import android.view.View; +import android.view.ViewGroup; + +/* Toolkit dialog implementation. This object is built from JNI and + describes a single alert dialog. Then, `inflate' turns it into + AlertDialog. */ + +public class EmacsDialog implements DialogInterface.OnDismissListener +{ + private static final String TAG = "EmacsDialog"; + + /* List of buttons in this dialog. */ + private List buttons; + + /* Dialog title. */ + private String title; + + /* Dialog text. */ + private String text; + + /* Whether or not a selection has already been made. */ + private boolean wasButtonClicked; + + /* Dialog to dismiss after click. */ + private AlertDialog dismissDialog; + + private class EmacsButton implements View.OnClickListener, + DialogInterface.OnClickListener + { + /* Name of this button. */ + public String name; + + /* ID of this button. */ + public int id; + + /* Whether or not the button is enabled. */ + public boolean enabled; + + @Override + public void + onClick (View view) + { + Log.d (TAG, "onClicked " + this); + + wasButtonClicked = true; + EmacsNative.sendContextMenu ((short) 0, id); + dismissDialog.dismiss (); + } + + @Override + public void + onClick (DialogInterface dialog, int which) + { + Log.d (TAG, "onClicked " + this); + + wasButtonClicked = true; + EmacsNative.sendContextMenu ((short) 0, id); + } + }; + + /* Create a popup dialog with the title TITLE and the text TEXT. + TITLE may be NULL. */ + + public static EmacsDialog + createDialog (String title, String text) + { + EmacsDialog dialog; + + dialog = new EmacsDialog (); + dialog.buttons = new ArrayList (); + dialog.title = title; + dialog.text = text; + + return dialog; + } + + /* Add a button named NAME, with the identifier ID. If DISABLE, + disable the button. */ + + public void + addButton (String name, int id, boolean disable) + { + EmacsButton button; + + button = new EmacsButton (); + button.name = name; + button.id = id; + button.enabled = !disable; + buttons.add (button); + } + + /* Turn this dialog into an AlertDialog for the specified + CONTEXT. + + Upon a button being selected, the dialog will send an + ANDROID_CONTEXT_MENU event with the id of that button. + + Upon the dialog being dismissed, an ANDROID_CONTEXT_MENU event + will be sent with an id of 0. */ + + public AlertDialog + toAlertDialog (Context context) + { + AlertDialog dialog; + int size; + EmacsButton button; + LinearLayout layout; + Button buttonView; + ViewGroup.LayoutParams layoutParams; + + size = buttons.size (); + + if (size <= 3) + { + dialog = new AlertDialog.Builder (context).create (); + dialog.setMessage (text); + dialog.setCancelable (true); + dialog.setOnDismissListener (this); + + if (title != null) + dialog.setTitle (title); + + /* There are less than 4 buttons. Add the buttons the way + Android intends them to be added. */ + + if (size >= 1) + { + button = buttons.get (0); + dialog.setButton (DialogInterface.BUTTON_POSITIVE, + button.name, button); + } + + if (size >= 2) + { + button = buttons.get (1); + dialog.setButton (DialogInterface.BUTTON_NEUTRAL, + button.name, button); + buttonView + = dialog.getButton (DialogInterface.BUTTON_NEUTRAL); + buttonView.setEnabled (button.enabled); + } + + if (size >= 3) + { + button = buttons.get (2); + dialog.setButton (DialogInterface.BUTTON_NEGATIVE, + button.name, button); + } + } + else + { + /* There are more than 4 buttons. Add them all to a + LinearLayout. */ + layout = new LinearLayout (context); + layoutParams + = new LinearLayout.LayoutParams (ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + + for (EmacsButton emacsButton : buttons) + { + buttonView = new Button (context); + buttonView.setText (emacsButton.name); + buttonView.setOnClickListener (emacsButton); + buttonView.setLayoutParams (layoutParams); + buttonView.setEnabled (emacsButton.enabled); + layout.addView (buttonView); + } + + layoutParams + = new FrameLayout.LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + layout.setLayoutParams (layoutParams); + + /* Add that layout to the dialog's custom view. + + android.R.id.custom is documented to work. But looking it + up returns NULL, so setView must be used instead. */ + + dialog = new AlertDialog.Builder (context).setView (layout).create (); + dialog.setMessage (text); + dialog.setCancelable (true); + dialog.setOnDismissListener (this); + + if (title != null) + dialog.setTitle (title); + } + + return dialog; + } + + /* Internal helper for display run on the main thread. */ + + private boolean + display1 () + { + EmacsActivity activity; + int size; + Button buttonView; + EmacsButton button; + AlertDialog dialog; + + if (EmacsActivity.focusedActivities.isEmpty ()) + return false; + + activity = EmacsActivity.focusedActivities.get (0); + dialog = dismissDialog = toAlertDialog (activity); + dismissDialog.show (); + + /* If there are less than four buttons, then they must be + individually enabled or disabled after the dialog is + displayed. */ + size = buttons.size (); + + if (size <= 3) + { + if (size >= 1) + { + button = buttons.get (0); + buttonView + = dialog.getButton (DialogInterface.BUTTON_POSITIVE); + buttonView.setEnabled (button.enabled); + } + + if (size >= 2) + { + button = buttons.get (1); + dialog.setButton (DialogInterface.BUTTON_NEUTRAL, + button.name, button); + buttonView + = dialog.getButton (DialogInterface.BUTTON_NEUTRAL); + buttonView.setEnabled (button.enabled); + } + + if (size >= 3) + { + button = buttons.get (2); + buttonView + = dialog.getButton (DialogInterface.BUTTON_NEGATIVE); + buttonView.setEnabled (button.enabled); + } + } + + return true; + } + + /* Display this dialog for a suitable activity. + Value is false if the dialog could not be displayed, + and true otherwise. */ + + public boolean + display () + { + Runnable runnable; + final Holder rc; + + rc = new Holder (); + runnable = new Runnable () { + @Override + public void + run () + { + synchronized (this) + { + rc.thing = display1 (); + notify (); + } + } + }; + + synchronized (runnable) + { + EmacsService.SERVICE.runOnUiThread (runnable); + + try + { + runnable.wait (); + } + catch (InterruptedException e) + { + EmacsNative.emacsAbort (); + } + } + + return rc.thing; + } + + + + @Override + public void + onDismiss (DialogInterface dialog) + { + Log.d (TAG, "onDismiss: " + this); + + if (wasButtonClicked) + return; + + EmacsNative.sendContextMenu ((short) 0, 0); + } +}; diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index b42e9556e8c..c29d413f66e 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -36,10 +36,10 @@ public class EmacsDrawRectangle Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; - Rect rect; Rect maskRect, dstRect; Canvas canvas; Bitmap clipBitmap; + Rect clipRect; /* TODO implement stippling. */ if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) @@ -58,13 +58,29 @@ public class EmacsDrawRectangle canvas.clipRect (gc.real_clip_rects[i]); } - paint = gc.gcPaint; - rect = new Rect (x, y, x + width, y + height); + /* Clip to the clipRect because some versions of Android draw an + overly wide line. */ + clipRect = new Rect (x, y, x + width + 1, + y + height + 1); + canvas.clipRect (clipRect); - paint.setStyle (Paint.Style.STROKE); + paint = gc.gcPaint; if (gc.clip_mask == null) - canvas.drawRect (rect, paint); + { + /* canvas.drawRect just doesn't work on Android, producing + different results on various devices. Do a 5 point + PolyLine instead. */ + canvas.drawLine ((float) x, (float) y, (float) x + width, + (float) y, paint); + canvas.drawLine ((float) x + width, (float) y, + (float) x + width, (float) y + height, + paint); + canvas.drawLine ((float) x + width, (float) y + height, + (float) x, (float) y + height, paint); + canvas.drawLine ((float) x, (float) y + height, + (float) x, (float) y, paint); + } else { /* Drawing with a clip mask involves calculating the @@ -116,10 +132,12 @@ public class EmacsDrawRectangle /* Finally, draw the mask bitmap to the destination. */ paint.setXfermode (null); canvas.drawBitmap (maskBitmap, null, maskRect, paint); + + /* Recycle this unused bitmap. */ + maskBitmap.recycle (); } canvas.restore (); - drawable.damageRect (new Rect (x, y, x + width + 1, - y + height + 1)); + drawable.damageRect (clipRect); } } diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java index b733b417d6b..7cc55d3db96 100644 --- a/java/org/gnu/emacs/EmacsFillRectangle.java +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -115,6 +115,9 @@ public class EmacsFillRectangle /* Finally, draw the mask bitmap to the destination. */ paint.setXfermode (null); canvas.drawBitmap (maskBitmap, null, maskRect, paint); + + /* Recycle this unused bitmap. */ + maskBitmap.recycle (); } canvas.restore (); diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index 15452f007c4..85931c2abd4 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -25,6 +25,8 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; +import android.os.Build; + /* Drawable backed by bitmap. */ public class EmacsPixmap extends EmacsHandleObject @@ -123,4 +125,25 @@ public class EmacsPixmap extends EmacsHandleObject { return bitmap; } + + @Override + public void + destroyHandle () + { + boolean needCollect; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) + needCollect = (bitmap.getByteCount () + >= 1024 * 512); + else + needCollect = (bitmap.getAllocationByteCount () + >= 1024 * 512); + + bitmap.recycle (); + bitmap = null; + + /* Collect the bitmap storage if the bitmap is big. */ + if (needCollect) + Runtime.getRuntime ().gc (); + } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 445d8ffa023..6137fd74a7f 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -70,9 +70,9 @@ public class EmacsView extends ViewGroup 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; + /* Whether or not bitmaps must be recreated upon the next call to + getBitmap. */ + private boolean bitmapDirty; /* Whether or not a popup is active. */ private boolean popupActive; @@ -80,6 +80,9 @@ public class EmacsView extends ViewGroup /* The current context menu. */ private EmacsContextMenu contextMenu; + /* The last measured width and height. */ + private int measuredWidth, measuredHeight; + public EmacsView (EmacsWindow window) { @@ -116,13 +119,27 @@ public class EmacsView extends ViewGroup { Bitmap oldBitmap; + if (measuredWidth == 0 || measuredHeight == 0) + 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. */ + if (bitmap != null + && (bitmap.getWidth () == measuredWidth + && bitmap.getHeight () == measuredHeight)) + { + bitmapDirty = false; + return; + } + /* Save the old bitmap. */ oldBitmap = bitmap; /* Recreate the front and back buffer bitmaps. */ bitmap - = Bitmap.createBitmap (bitmapDirty.width (), - bitmapDirty.height (), + = Bitmap.createBitmap (measuredWidth, + measuredHeight, Bitmap.Config.ARGB_8888); bitmap.eraseColor (0xffffffff); @@ -133,23 +150,27 @@ public class EmacsView extends ViewGroup if (oldBitmap != null) canvas.drawBitmap (oldBitmap, 0f, 0f, new Paint ()); - bitmapDirty = null; + bitmapDirty = false; + + /* Explicitly free the old bitmap's memory. */ + if (oldBitmap != null) + oldBitmap.recycle (); + + /* Some Android versions still don't free the bitmap until the + next GC. */ + Runtime.getRuntime ().gc (); } public synchronized void - explicitlyDirtyBitmap (Rect rect) + explicitlyDirtyBitmap () { - if (bitmapDirty == null - && (bitmap == null - || rect.width () != bitmap.getWidth () - || rect.height () != bitmap.getHeight ())) - bitmapDirty = rect; + bitmapDirty = true; } public synchronized Bitmap getBitmap () { - if (bitmapDirty != null) + if (bitmapDirty || bitmap == null) handleDirtyBitmap (); return bitmap; @@ -158,7 +179,7 @@ public class EmacsView extends ViewGroup public synchronized Canvas getCanvas () { - if (bitmapDirty != null) + if (bitmapDirty || bitmap == null) handleDirtyBitmap (); return canvas; @@ -196,8 +217,12 @@ public class EmacsView extends ViewGroup super.setMeasuredDimension (width, height); } + /* Note that the monitor lock for the window must never be held from + within the lock for the view, because the window also locks the + other way around. */ + @Override - protected synchronized void + protected void onLayout (boolean changed, int left, int top, int right, int bottom) { @@ -213,12 +238,13 @@ public class EmacsView extends ViewGroup window.viewLayout (left, top, right, bottom); } - 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); + measuredWidth = right - left; + measuredHeight = bottom - top; + + /* Dirty the back buffer. */ + + if (changed) + explicitlyDirtyBitmap (); for (i = 0; i < count; ++i) { @@ -472,4 +498,20 @@ public class EmacsView extends ViewGroup contextMenu = null; popupActive = false; } + + @Override + public synchronized void + onDetachedFromWindow () + { + synchronized (this) + { + /* Recycle the bitmap and call GC. */ + bitmap.recycle (); + bitmap = null; + canvas = null; + + /* Collect the bitmap storage; it could be large. */ + Runtime.getRuntime ().gc (); + } + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 7181bc89fea..f5b50f11f14 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -260,7 +260,7 @@ public class EmacsWindow extends EmacsHandleObject { /* This is necessary because otherwise subsequent drawing on the Emacs thread may be lost. */ - view.explicitlyDirtyBitmap (rect); + view.explicitlyDirtyBitmap (); EmacsService.SERVICE.runOnUiThread (new Runnable () { @Override -- cgit v1.2.1 From a496509cedb17109d0e6297a74e2ff8ed526333c Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 19 Jan 2023 22:19:06 +0800 Subject: Update Android port * .gitignore: Add new files. * INSTALL.android: Explain how to build Emacs for ancient versions of Android. * admin/merge-gnulib (GNULIB_MODULES): Add getdelim. * build-aux/config.guess (timestamp, version): * build-aux/config.sub (timestamp, version): Autoupdate. * configure.ac (BUILD_DETAILS, ANDROID_MIN_SDK): (ANDROID_STUBIFY): Allow specifying CFLAGS via ANDROID_CFLAGS. Add new configure tests for Android API version when not explicitly specified. * doc/emacs/android.texi (Android): Add reference to ``Other Input Devices''. (Android File System): Remove restrictions on directory-files on the assets directory. * doc/emacs/emacs.texi (Top): Add Other Input Devices to menu. * doc/emacs/input.texi (Other Input Devices): New node. * doc/lispref/commands.texi (Touchscreen Events): Document changes to touchscreen input events. * doc/lispref/frames.texi (Pop-Up Menus): Likewise. * etc/NEWS: Announce changes. * java/Makefile.in: Use lib-src/asset-directory-tool to generate an `directory-tree' file placed in /assets. * java/debug.sh: Large adjustments to support Android 2.2 and later. * java/org/gnu/emacs/EmacsContextMenu.java (inflateMenuItems): * java/org/gnu/emacs/EmacsCopyArea.java (perform): * java/org/gnu/emacs/EmacsDialog.java (toAlertDialog): * java/org/gnu/emacs/EmacsDrawLine.java (perform): * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): * java/org/gnu/emacs/EmacsDrawable.java (EmacsDrawable): * java/org/gnu/emacs/EmacsFillPolygon.java (perform): * java/org/gnu/emacs/EmacsFillRectangle.java (perform): * java/org/gnu/emacs/EmacsGC.java (EmacsGC): * java/org/gnu/emacs/EmacsPixmap.java (EmacsPixmap): (destroyHandle): * java/org/gnu/emacs/EmacsSdk7FontDriver.java (draw): Avoid redundant canvas saves and restores. * java/org/gnu/emacs/EmacsService.java (run): * java/org/gnu/emacs/EmacsView.java (EmacsView): (handleDirtyBitmap): * java/org/gnu/emacs/EmacsWindow.java (changeWindowBackground) (EmacsWindow): Make compatible with Android 2.2 and later. * lib-src/Makefile.in (DONT_INSTALL): Add asset-directory-tool on Android.:(asset-directory-tool{EXEEXT}): New target. * lib-src/asset-directory-tool.c (struct directory_tree, xmalloc) (main_1, main_2, main): New file. * lib, m4: Merge from gnulib. This will be reverted before merging to master. * lisp/button.el (button-map): (push-button): * lisp/frame.el (display-popup-menus-p): Improve touchscreen support. * lisp/subr.el (event-start): (event-end): Handle touchscreen events. * lisp/touch-screen.el (touch-screen-handle-timeout): (touch-screen-handle-point-update): (touch-screen-handle-point-up): (touch-screen-track-tap): (touch-screen-track-drag): (touch-screen-drag-mode-line-1): (touch-screen-drag-mode-line): New functions. ([mode-line touchscreen-begin]): ([bottom-divider touchscreen-begin]): Bind new events. * lisp/wid-edit.el (widget-event-point): (widget-keymap): (widget-event-start): (widget-button--check-and-call-button): (widget-button-click): Improve touchscreen support. * src/alloc.c (make_lisp_symbol): Avoid ICE on Android NDK GCC. (mark_pinned_symbols): Likewise. * src/android.c (struct android_emacs_window): New struct. (window_class): New variable. (android_run_select_thread): Add workaround for Android platform bug. (android_extract_long, android_scan_directory_tree): New functions. (android_file_access_p): Use those functions instead. (android_init_emacs_window): New function. (android_init_emacs_gc_class): Update signature of `markDirty'. (android_change_gc, android_set_clip_rectangles): Tell the GC whether or not clip rects were dirtied. (android_swap_buffers): Do not look up method every time. (struct android_dir): Adjust for new directory tree lookup. (android_opendir, android_readdir, android_closedir): Likewise. (android_four_corners_bilinear): Fix coding style. (android_ftruncate): New function. * src/android.h: Update prototypes. Replace ftruncate with android_ftruncate when necessary. * src/androidterm.c (handle_one_android_event): Pacify GCC. Fix touch screen tool bar bug. * src/emacs.c (using_utf8): Fix compilation error. * src/fileio.c (Ffile_system_info): Return Qnil when fsusage.o is not built. * src/filelock.c (BOOT_TIME_FILE): Fix definition for Android. * src/frame.c (Fx_parse_geometry): Fix uninitialized variable uses. * src/keyboard.c (lispy_function_keys): Fix `back'. * src/menu.c (x_popup_menu_1): Handle touch screen events. (Fx_popup_menu): Document changes. * src/sfnt.c (main): Improve tests. * src/sfntfont-android.c (sfntfont_android_put_glyphs): Fix minor problem. (init_sfntfont_android): Check for HAVE_DECL_ANDROID_GET_DEVICE_API_LEVEL. * src/sfntfont.c (struct sfnt_font_desc): New fields `adstyle' and `languages'. (sfnt_parse_style): Append tokens to adstyle. (sfnt_parse_languages): New function. (sfnt_enum_font_1): Parse supported languages and adstyle. (sfntfont_list_1): Handle new fields. (sfntfont_text_extents): Fix uninitialized variable use. (syms_of_sfntfont, mark_sfntfont): Adjust accordingly. --- java/org/gnu/emacs/EmacsContextMenu.java | 20 ++++++-- java/org/gnu/emacs/EmacsCopyArea.java | 11 +--- java/org/gnu/emacs/EmacsDialog.java | 3 -- java/org/gnu/emacs/EmacsDrawLine.java | 11 +--- java/org/gnu/emacs/EmacsDrawRectangle.java | 45 +++++----------- java/org/gnu/emacs/EmacsDrawable.java | 2 +- java/org/gnu/emacs/EmacsFillPolygon.java | 11 +--- java/org/gnu/emacs/EmacsFillRectangle.java | 12 +---- java/org/gnu/emacs/EmacsGC.java | 33 ++++++++---- java/org/gnu/emacs/EmacsPixmap.java | 77 +++++++++++++++++++++------ java/org/gnu/emacs/EmacsSdk7FontDriver.java | 11 +--- java/org/gnu/emacs/EmacsService.java | 7 ++- java/org/gnu/emacs/EmacsView.java | 34 ++++++++++-- java/org/gnu/emacs/EmacsWindow.java | 80 +++++++++++++++++++++-------- 14 files changed, 212 insertions(+), 145 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 00e204c9949..ac67ebe4aa0 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -168,10 +168,22 @@ public class EmacsContextMenu { if (item.subMenu != null) { - /* This is a submenu. Create the submenu and add the - contents of the menu to it. */ - submenu = menu.addSubMenu (item.itemName); - item.subMenu.inflateMenuItems (submenu); + try + { + /* This is a submenu. On versions of Android which + support doing so, create the submenu and add the + contents of the menu to it. */ + submenu = menu.addSubMenu (item.itemName); + } + catch (UnsupportedOperationException exception) + { + /* This version of Android has a restriction + preventing submenus from being added to submenus. + Inflate everything into the parent menu + instead. */ + item.subMenu.inflateMenuItems (menu); + continue; + } /* This is still needed to set wasSubmenuSelected. */ menuItem = submenu.getItem (); diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 5d72a7860c8..7a97d706794 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -66,19 +66,11 @@ public class EmacsCopyArea paint = gc.gcPaint; - canvas = destination.lockCanvas (); + canvas = destination.lockCanvas (gc); 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. */ srcBitmap = source.getBitmap (); @@ -189,7 +181,6 @@ public class EmacsCopyArea maskBitmap.recycle (); } - canvas.restore (); destination.damageRect (rect); } } diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 5bc8efa5978..7d88a23c58f 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -168,9 +168,6 @@ public class EmacsDialog implements DialogInterface.OnDismissListener button = buttons.get (1); dialog.setButton (DialogInterface.BUTTON_NEUTRAL, button.name, button); - buttonView - = dialog.getButton (DialogInterface.BUTTON_NEUTRAL); - buttonView.setEnabled (button.enabled); } if (size >= 3) diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 8941d4c217f..827feb96dfb 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -49,19 +49,11 @@ public class EmacsDrawLine Math.min (y, y2 + 1), Math.max (x2 + 1, x), Math.max (y2 + 1, y)); - canvas = drawable.lockCanvas (); + canvas = drawable.lockCanvas (gc); 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]); - } - paint.setStyle (Paint.Style.STROKE); if (gc.clip_mask == null) @@ -71,7 +63,6 @@ public class EmacsDrawLine /* DrawLine with clip mask not implemented; it is not used by Emacs. */ - canvas.restore (); drawable.damageRect (rect); } } diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index c29d413f66e..695a8c6ea44 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -23,6 +23,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; +import android.graphics.RectF; import android.util.Log; @@ -36,51 +37,31 @@ public class EmacsDrawRectangle Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; + Rect rect; Rect maskRect, dstRect; Canvas canvas; Bitmap clipBitmap; - Rect clipRect; /* TODO implement stippling. */ if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; - canvas = drawable.lockCanvas (); + canvas = drawable.lockCanvas (gc); 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]); - } - - /* Clip to the clipRect because some versions of Android draw an - overly wide line. */ - clipRect = new Rect (x, y, x + width + 1, - y + height + 1); - canvas.clipRect (clipRect); - paint = gc.gcPaint; + paint.setStyle (Paint.Style.STROKE); + rect = new Rect (x, y, x + width, y + height); if (gc.clip_mask == null) - { - /* canvas.drawRect just doesn't work on Android, producing - different results on various devices. Do a 5 point - PolyLine instead. */ - canvas.drawLine ((float) x, (float) y, (float) x + width, - (float) y, paint); - canvas.drawLine ((float) x + width, (float) y, - (float) x + width, (float) y + height, - paint); - canvas.drawLine ((float) x + width, (float) y + height, - (float) x, (float) y + height, paint); - canvas.drawLine ((float) x, (float) y + height, - (float) x, (float) y, paint); - } + /* Use canvas.drawRect with a RectF. That seems to reliably + get PostScript behavior. */ + canvas.drawRect (new RectF (x + 0.5f, y + 0.5f, + x + width + 0.5f, + y + height + 0.5f), + paint); else { /* Drawing with a clip mask involves calculating the @@ -137,7 +118,7 @@ public class EmacsDrawRectangle maskBitmap.recycle (); } - canvas.restore (); - drawable.damageRect (clipRect); + 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 6a6199ff214..f2f8885e976 100644 --- a/java/org/gnu/emacs/EmacsDrawable.java +++ b/java/org/gnu/emacs/EmacsDrawable.java @@ -25,7 +25,7 @@ import android.graphics.Canvas; public interface EmacsDrawable { - public Canvas lockCanvas (); + public Canvas lockCanvas (EmacsGC gc); 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 42b73886dff..22e2dd0d8a9 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -41,21 +41,13 @@ public class EmacsFillPolygon RectF rectF; int i; - canvas = drawable.lockCanvas (); + canvas = drawable.lockCanvas (gc); if (canvas == null) return; paint = gc.gcPaint; - 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]); - } - /* Build the path from the given array of points. */ path = new Path (); @@ -83,7 +75,6 @@ public class EmacsFillPolygon if (gc.clip_mask == null) canvas.drawPath (path, paint); - canvas.restore (); drawable.damageRect (rect); /* FillPolygon with clip mask not implemented; it is not used by diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java index 7cc55d3db96..aed0a540c8f 100644 --- a/java/org/gnu/emacs/EmacsFillRectangle.java +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -32,7 +32,6 @@ public class EmacsFillRectangle perform (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { - int i; Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; @@ -45,19 +44,11 @@ public class EmacsFillRectangle if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; - canvas = drawable.lockCanvas (); + canvas = drawable.lockCanvas (gc); 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]); - } - paint = gc.gcPaint; rect = new Rect (x, y, x + width, y + height); @@ -120,7 +111,6 @@ public class EmacsFillRectangle maskBitmap.recycle (); } - canvas.restore (); drawable.damageRect (rect); } } diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java index c579625f3f7..bdc27a1ca5b 100644 --- a/java/org/gnu/emacs/EmacsGC.java +++ b/java/org/gnu/emacs/EmacsGC.java @@ -47,6 +47,14 @@ public class EmacsGC extends EmacsHandleObject public EmacsPixmap clip_mask, stipple; public Paint gcPaint; + /* ID incremented every time the clipping rectangles of any GC + changes. */ + private static long clip_serial; + + /* The value of clipRectID after the last time this GCs clip + rectangles changed. 0 if there are no clip rectangles. */ + public long clipRectID; + static { xorAlu = new PorterDuffXfermode (Mode.XOR); @@ -75,23 +83,28 @@ public class EmacsGC extends EmacsHandleObject recompute real_clip_rects. */ public void - markDirty () + markDirty (boolean clipRectsChanged) { int i; - if ((ts_origin_x != 0 || ts_origin_y != 0) - && clip_rects != null) + if (clipRectsChanged) { - real_clip_rects = new Rect[clip_rects.length]; - - for (i = 0; i < clip_rects.length; ++i) + if ((ts_origin_x != 0 || ts_origin_y != 0) + && clip_rects != null) { - real_clip_rects[i] = new Rect (clip_rects[i]); - real_clip_rects[i].offset (ts_origin_x, ts_origin_y); + real_clip_rects = new Rect[clip_rects.length]; + + for (i = 0; i < clip_rects.length; ++i) + { + 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; + + clipRectID = ++clip_serial; } - else - real_clip_rects = clip_rects; gcPaint.setStrokeWidth (1f); gcPaint.setColor (foreground | 0xff000000); diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index 85931c2abd4..a83d8f25542 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -42,6 +42,14 @@ public class EmacsPixmap extends EmacsHandleObject /* The canvas used to draw to BITMAP. */ public Canvas canvas; + /* Whether or not GC should be explicitly triggered upon + release. */ + private boolean needCollect; + + /* ID used to determine whether or not the GC clip rects + changed. */ + private long gcClipRectID; + public EmacsPixmap (short handle, int colors[], int width, int height, int depth) @@ -83,18 +91,41 @@ public class EmacsPixmap extends EmacsHandleObject switch (depth) { case 1: - bitmap = Bitmap.createBitmap (width, height, - Bitmap.Config.ALPHA_8, - false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + bitmap = Bitmap.createBitmap (width, height, + Bitmap.Config.ALPHA_8, + false); + else + bitmap = Bitmap.createBitmap (width, height, + Bitmap.Config.ALPHA_8); break; case 24: - bitmap = Bitmap.createBitmap (width, height, - Bitmap.Config.ARGB_8888, - false); + + /* Emacs doesn't just use the first kind of `createBitmap' + because the latter allows specifying that the pixmap is + always opaque, which really increases efficiency. */ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + bitmap = Bitmap.createBitmap (width, height, + Bitmap.Config.ARGB_8888); + else + bitmap = Bitmap.createBitmap (width, height, + Bitmap.Config.ARGB_8888, + false); break; } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1) + /* On these old versions of Android, Bitmap.recycle frees bitmap + contents immediately. */ + needCollect = false; + else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) + needCollect = (bitmap.getByteCount () + >= 1024 * 512); + else + needCollect = (bitmap.getAllocationByteCount () + >= 1024 * 512); + bitmap.eraseColor (0xff000000); this.width = width; @@ -104,11 +135,32 @@ public class EmacsPixmap extends EmacsHandleObject @Override public Canvas - lockCanvas () + lockCanvas (EmacsGC gc) { + int i; + if (canvas == null) - canvas = new Canvas (bitmap); + { + canvas = new Canvas (bitmap); + canvas.save (); + } + /* Now see if clipping has to be redone. */ + if (gc.clipRectID == gcClipRectID) + return canvas; + + /* It does have to be redone. Reapply gc.real_clip_rects. */ + canvas.restore (); + 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]); + } + + /* Save the clip rect ID again. */ + gcClipRectID = gc.clipRectID; return canvas; } @@ -130,15 +182,6 @@ public class EmacsPixmap extends EmacsHandleObject public void destroyHandle () { - boolean needCollect; - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) - needCollect = (bitmap.getByteCount () - >= 1024 * 512); - else - needCollect = (bitmap.getAllocationByteCount () - >= 1024 * 512); - bitmap.recycle (); bitmap = null; diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index c0f24c7433a..a964cadb74c 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -510,20 +510,12 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver backgroundRect.right = x + backgroundWidth; backgroundRect.bottom = y + sdk7FontObject.descent; - canvas = drawable.lockCanvas (); + canvas = drawable.lockCanvas (gc); 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) @@ -538,7 +530,6 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver 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); diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index ca38f93dc98..bcf8d9ff6e8 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -175,7 +175,12 @@ public class EmacsService extends Service { view.thing = new EmacsView (window); view.thing.setVisibility (visibility); - view.thing.setFocusedByDefault (isFocusedByDefault); + + /* The following function is only present on Android 26 + or later. */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + view.thing.setFocusedByDefault (isFocusedByDefault); + notify (); } } diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 6137fd74a7f..82f44acaebe 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -83,6 +83,9 @@ public class EmacsView extends ViewGroup /* The last measured width and height. */ private int measuredWidth, measuredHeight; + /* The serial of the last clip rectangle change. */ + private long lastClipSerial; + public EmacsView (EmacsWindow window) { @@ -105,10 +108,6 @@ public class EmacsView extends ViewGroup on Android? */ setChildrenDrawingOrderEnabled (true); - /* Get rid of the foreground and background tint. */ - setBackgroundTintList (null); - setForegroundTintList (null); - /* Get rid of the default focus highlight. */ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) setDefaultFocusHighlightEnabled (false); @@ -145,6 +144,11 @@ public class EmacsView extends ViewGroup /* And canvases. */ canvas = new Canvas (bitmap); + canvas.save (); + + /* Since the clip rectangles have been cleared, clear the clip + rectangle ID. */ + lastClipSerial = 0; /* Copy over the contents of the old bitmap. */ if (oldBitmap != null) @@ -177,11 +181,31 @@ public class EmacsView extends ViewGroup } public synchronized Canvas - getCanvas () + getCanvas (EmacsGC gc) { + int i; + if (bitmapDirty || bitmap == null) handleDirtyBitmap (); + if (canvas == null) + return null; + + /* Update clip rectangles if necessary. */ + if (gc.clipRectID != lastClipSerial) + { + canvas.restore (); + 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]); + } + + lastClipSerial = gc.clipRectID; + } + return canvas; } diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index f5b50f11f14..c5b1522086c 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -164,7 +164,7 @@ public class EmacsWindow extends EmacsHandleObject { /* scratchGC is used as the argument to a FillRectangles req. */ scratchGC.foreground = pixel; - scratchGC.markDirty (); + scratchGC.markDirty (false); } public Rect @@ -466,9 +466,9 @@ public class EmacsWindow extends EmacsHandleObject @Override public Canvas - lockCanvas () + lockCanvas (EmacsGC gc) { - return view.getCanvas (); + return view.getCanvas (gc); } @Override @@ -512,37 +512,75 @@ public class EmacsWindow extends EmacsHandleObject public void onKeyDown (int keyCode, KeyEvent event) { - int state; + int state, state_1; - state = event.getModifiers (); - state &= ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) + state = event.getModifiers (); + else + { + /* Replace this with getMetaState and manual + normalization. */ + state = event.getMetaState (); + + /* Normalize the state by setting the generic modifier bit if + either a left or right modifier is pressed. */ + + if ((state & KeyEvent.META_ALT_LEFT_ON) != 0 + || (state & KeyEvent.META_ALT_RIGHT_ON) != 0) + state |= KeyEvent.META_ALT_MASK; + + if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0 + || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0) + state |= KeyEvent.META_CTRL_MASK; + } + + /* Ignore meta-state understood by Emacs for now, or Ctrl+C will + not be recognized as an ASCII key press event. */ + state_1 + = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); EmacsNative.sendKeyPress (this.handle, event.getEventTime (), - event.getModifiers (), - 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)); - lastModifiers = event.getModifiers (); + state, keyCode, + event.getUnicodeChar (state_1)); + lastModifiers = state; } public void onKeyUp (int keyCode, KeyEvent event) { - int state; + int state, state_1; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) + state = event.getModifiers (); + else + { + /* Replace this with getMetaState and manual + normalization. */ + state = event.getMetaState (); + + /* Normalize the state by setting the generic modifier bit if + either a left or right modifier is pressed. */ + + if ((state & KeyEvent.META_ALT_LEFT_ON) != 0 + || (state & KeyEvent.META_ALT_RIGHT_ON) != 0) + state |= KeyEvent.META_ALT_MASK; + + if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0 + || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0) + state |= KeyEvent.META_CTRL_MASK; + } - state = event.getModifiers (); - state &= ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); + /* Ignore meta-state understood by Emacs for now, or Ctrl+C will + not be recognized as an ASCII key press event. */ + state_1 + = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); EmacsNative.sendKeyRelease (this.handle, event.getEventTime (), - event.getModifiers (), - keyCode, - event.getUnicodeChar (state)); - lastModifiers = event.getModifiers (); + state, keyCode, + event.getUnicodeChar (state_1)); + lastModifiers = state; } public void -- cgit v1.2.1 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. --- 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 +- 10 files changed, 400 insertions(+), 76 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsPreferencesActivity.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 79c0991a5d3..d377cf982ef 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -161,6 +161,13 @@ public class EmacsActivity extends Activity onCreate (Bundle savedInstanceState) { FrameLayout.LayoutParams params; + Intent intent; + + /* See if Emacs should be started with -Q. */ + intent = getIntent (); + EmacsService.needDashQ + = intent.getBooleanExtra ("org.gnu.emacs.START_DASH_Q", + false); /* Set the theme to one without a title bar. */ @@ -179,9 +186,8 @@ public class EmacsActivity extends Activity /* Set it as the content view. */ setContentView (layout); - if (EmacsService.SERVICE == null) - /* Start the Emacs service now. */ - startService (new Intent (this, EmacsService.class)); + /* Maybe start the Emacs service if necessary. */ + EmacsService.startEmacsService (this); /* Add this activity to the list of available activities. */ EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this); @@ -193,10 +199,16 @@ public class EmacsActivity extends Activity public void onDestroy () { + EmacsWindowAttachmentManager manager; + boolean isMultitask; + + manager = EmacsWindowAttachmentManager.MANAGER; + /* The activity will die shortly hereafter. If there is a window attached, close it now. */ Log.d (TAG, "onDestroy " + this); - EmacsWindowAttachmentManager.MANAGER.removeWindowConsumer (this); + isMultitask = this instanceof EmacsMultitaskActivity; + manager.removeWindowConsumer (this, isMultitask || isFinishing ()); focusedActivities.remove (this); invalidateFocus (); super.onDestroy (); diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index ac67ebe4aa0..056d8fb692c 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -174,6 +174,7 @@ public class EmacsContextMenu support doing so, create the submenu and add the contents of the menu to it. */ submenu = menu.addSubMenu (item.itemName); + item.subMenu.inflateMenuItems (submenu); } catch (UnsupportedOperationException exception) { diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 2f3a732ea7c..3efdc0cff9a 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -61,75 +61,76 @@ public class EmacsNative /* 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, + /* 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, 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, + public static native long sendKeyPress (short window, long time, int state, int keyCode, int unicodeChar); /* Send an ANDROID_KEY_RELEASE event. */ - public static native void sendKeyRelease (short window, long time, int state, + public static native long sendKeyRelease (short window, long time, int state, int keyCode, int unicodeChar); /* Send an ANDROID_FOCUS_IN event. */ - public static native void sendFocusIn (short window, long time); + public static native long sendFocusIn (short window, long time); /* Send an ANDROID_FOCUS_OUT event. */ - public static native void sendFocusOut (short window, long time); + public static native long sendFocusOut (short window, long time); /* Send an ANDROID_WINDOW_ACTION event. */ - public static native void sendWindowAction (short window, int action); + public static native long sendWindowAction (short window, int action); /* Send an ANDROID_ENTER_NOTIFY event. */ - public static native void sendEnterNotify (short window, int x, int y, + public static native long 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, + public static native long 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, + public static native long 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, + public static native long 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, + public static native long 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, + public static native long 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, + public static native long 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, + public static native long 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, + public static native long 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); + public static native long sendIconified (short window); /* Send an ANDROID_DEICONIFIED event. */ - public static native void sendDeiconified (short window); + public static native long sendDeiconified (short window); /* Send an ANDROID_CONTEXT_MENU event. */ - public static native void sendContextMenu (short window, int menuEventID); + public static native long sendContextMenu (short window, int menuEventID); static { diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java new file mode 100644 index 00000000000..0db983984fd --- /dev/null +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -0,0 +1,98 @@ +/* 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.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); -- cgit v1.2.1 From aaacf24ca25fc284038ec9f17be358067309a8cf Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 21 Jan 2023 20:03:37 +0800 Subject: Update Android port * doc/emacs/android.texi (Android File System): Document that ls-lisp is now used by default. * java/org/gnu/emacs/EmacsThread.java (EmacsThread): Name the thread something meaningful. * lisp/loadup.el (featurep): Load ls-lisp on Android. * lisp/ls-lisp.el (ls-lisp-use-insert-directory-program): Default to off on Android. * src/android.c (android_is_directory): New fucntion. (android_fstatat): Handle directories created by `android_opendir'. (android_open): Return meaningful file mode. (struct android_dir): New fields `next', `asset_file' and `fd'. (android_opendir): Populate those fields. (android_dirfd): New function. (android_closedir): Close file descriptor if set. (android_lookup_asset_directory_fd): New function. * src/android.h: Update prototypes. * src/androidfont.c (androidfont_check_init): New function. (androidfont_list, androidfont_match, androidfont_draw) (androidfont_open_font, androidfont_close_font) (androidfont_has_char, androidfont_encode_char) (androidfont_text_extents, androidfont_list_family): Initialize font driver if necessary. (init_androidfont): Don't initialize Java font if necessary. * src/dired.c (open_directory): Return android_dirfd if appropriate. (directory_files_internal, file_name_completion_dirp): Implement correctly for Android. * src/fileio.c (check_mutable_filename): New function. (Fcopy_file, Fdelete_directory_internal, Fdelete_file) (Frename_file, Fadd_name_to_file, Fmake_symbolic_link) (Fset_file_modes, Fset_file_times, Ffile_newer_than_file_p) (Fverify_visited_file_modtime, Fset_visited_file_modtime): Check that files being written to do not lie in /assets. * src/sfntfont-android.c (GET_SCANLINE_BUFFER) (sfntfont_android_u255to256, sfntfont_android_over_8888_1) (sfntfont_android_over_8888, sfntfont_android_composite_bitmap): Optimize on 64-bit ARM devices. (sfntfont_android_put_glyphs): Optimize away memset if background need not be filled. --- java/org/gnu/emacs/EmacsThread.java | 1 + 1 file changed, 1 insertion(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index f9bc132f354..21d8612703a 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -29,6 +29,7 @@ public class EmacsThread extends Thread public EmacsThread (EmacsService service, boolean startDashQ) { + super ("Emacs main thread"); this.startDashQ = startDashQ; } -- cgit v1.2.1 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. --- 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 ++++ 6 files changed, 346 insertions(+) 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 (limited to 'java/org/gnu') 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 () { -- cgit v1.2.1 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. --- 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 +++++++-- 5 files changed, 124 insertions(+), 5 deletions(-) (limited to 'java/org/gnu') 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); -- cgit v1.2.1 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. --- 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 +--- 5 files changed, 243 insertions(+), 46 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsNoninteractive.java (limited to 'java/org/gnu') 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); } }; -- cgit v1.2.1 From 888d3514967a90423d60dfa82bfd59b7f3df6c90 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 25 Jan 2023 19:15:30 +0800 Subject: Minor fixes to Android port * java/Makefile.in: (emacs.apk-in): Don't call cp with empty args. * java/org/gnu/emacs/EmacsDrawLine.java (perform): Fix for PostScript filling semantics. * src/Makefile.in (android-emacs): Build android-emacs directly. --- java/org/gnu/emacs/EmacsDrawLine.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 827feb96dfb..717e2279a7d 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -56,9 +56,12 @@ public class EmacsDrawLine paint.setStyle (Paint.Style.STROKE); + /* Since drawLine has PostScript style behavior, adjust the + coordinates appropriately. */ + if (gc.clip_mask == null) - canvas.drawLine ((float) x, (float) y, - (float) x2, (float) y2, + canvas.drawLine ((float) x + 0.5f, (float) y + 0.5f, + (float) x2 + 0.5f, (float) y2 + 0.5f, paint); /* DrawLine with clip mask not implemented; it is not used by -- cgit v1.2.1 From 0b1ef9ea31ce039001546b3ed34494332e5e3629 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 25 Jan 2023 22:07:51 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsDrawLine.java: Fix this again. Gosh, how does Android do this. * java/org/gnu/emacs/EmacsNoninteractive.java (main): Port to Android 2.3.3. * java/org/gnu/emacs/EmacsSdk11Clipboard.java (EmacsSdk11Clipboard): Port to Android 4.0.3. * java/org/gnu/emacs/EmacsService.java (getClipboardManager): New function. * src/alloc.c (find_string_data_in_pure): Fix Android alignment issue. * src/android-emacs.c (main): Port to Android 4.4. * src/android.c (initEmacs): Align stack to 32 bytes, so it ends up aligned to 16 even though gcc thinks the stack is already aligned to 16 bytes. * src/callproc.c (init_callproc): Use /system/bin/sh instead of /bin/sh by default. --- java/org/gnu/emacs/EmacsDrawLine.java | 2 +- java/org/gnu/emacs/EmacsNoninteractive.java | 30 +++++++++++++++---- java/org/gnu/emacs/EmacsSdk11Clipboard.java | 8 +---- java/org/gnu/emacs/EmacsService.java | 46 +++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 14 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 717e2279a7d..c6e5123bfca 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -60,7 +60,7 @@ public class EmacsDrawLine coordinates appropriately. */ if (gc.clip_mask == null) - canvas.drawLine ((float) x + 0.5f, (float) y + 0.5f, + canvas.drawLine ((float) x, (float) y + 0.5f, (float) x2 + 0.5f, (float) y2 + 0.5f, paint); diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index a3aefee5e0b..b4854d8323f 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -95,7 +95,7 @@ public class EmacsNoninteractive On Android 2.3.3 and earlier, there is no ``compatibilityInfo'' argument to getPackageInfo. */ - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { method = activityThreadClass.getMethod ("getPackageInfo", @@ -123,11 +123,29 @@ public class EmacsNoninteractive /* 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); + + try + { + method = contextImplClass.getDeclaredMethod ("createAppContext", + activityThreadClass, + loadedApkClass); + method.setAccessible (true); + context = (Context) method.invoke (null, activityThread, loadedApk); + } + catch (NoSuchMethodException exception) + { + /* Older Android versions don't have createAppContext, but + instead require creating a ContextImpl, and then + calling createPackageContext. */ + method = activityThreadClass.getDeclaredMethod ("getSystemContext"); + context = (Context) method.invoke (activityThread); + method = contextImplClass.getDeclaredMethod ("createPackageContext", + String.class, + int.class); + method.setAccessible (true); + context = (Context) method.invoke (context, "org.gnu.emacs", + 0); + } /* Don't actually start the looper or anything. Instead, obtain an AssetManager. */ diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java index 0a725200723..2df2015c9c1 100644 --- a/java/org/gnu/emacs/EmacsSdk11Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java @@ -42,13 +42,7 @@ public class EmacsSdk11Clipboard extends EmacsClipboard public EmacsSdk11Clipboard () { - String what; - Context context; - - what = Context.CLIPBOARD_SERVICE; - context = EmacsService.SERVICE; - manager - = (ClipboardManager) context.getSystemService (what); + manager = EmacsService.SERVICE.getClipboardManager (); manager.addPrimaryClipChangedListener (this); } diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 91db76b08e3..eb9b61dd876 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -39,6 +39,7 @@ import android.app.NotificationChannel; import android.app.PendingIntent; import android.app.Service; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -565,4 +566,49 @@ public class EmacsService extends Service return null; } + + /* Get a SDK 11 ClipboardManager. + + Android 4.0.x requires that this be called from the main + thread. */ + + public ClipboardManager + getClipboardManager () + { + final Holder manager; + Runnable runnable; + + manager = new Holder (); + + runnable = new Runnable () { + public void + run () + { + Object tem; + + synchronized (this) + { + tem = getSystemService (Context.CLIPBOARD_SERVICE); + manager.thing = (ClipboardManager) tem; + notify (); + } + } + }; + + synchronized (runnable) + { + runOnUiThread (runnable); + + try + { + runnable.wait (); + } + catch (InterruptedException e) + { + EmacsNative.emacsAbort (); + } + } + + return manager.thing; + } }; -- cgit v1.2.1 From 4255d7f0514c5fa1badded6b0bc445ec2d2764c0 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 26 Jan 2023 15:37:04 +0800 Subject: Update Android port * .gitignore: Ignore lib/math.h. * INSTALL.android: Update accordingly. * build-aux/ndk-build-helper-1.mk: * build-aux/ndk-build-helper-2.mk: * build-aux/ndk-build-helper.mk: * build-aux/ndk-module-extract.awk: Handle C++ modules. * configure.ac: Enable libxml2 on Android. * cross/ndk-build/Makefile.in: * cross/ndk-build/ndk-build-shared-library.mk: * cross/ndk-build/ndk-build-static-library.mk: * cross/ndk-build/ndk-build.mk.in: * cross/ndk-build/ndk-resolve.mk: Fix dependency resolution of includes. * java/org/gnu/emacs/EmacsView.java (popupMenu): Fix minimum SDK version for actual popup menus. * lib/math.h: Delete file. * m4/ndk-build.m4 (ndk_SEARCH_MODULE, ndk_CHECK_MODULES): Look for nasm and C++ libraries. * src/android.c (faccessat): Rename to `android_faccessat'. * src/android.h: Update prototypes. * src/dired.c (file_name_completion_dirp): * src/fileio.c (file_access_p, Faccess_file, file_directory_p): * src/lisp.h: * src/lread.c (openp): * src/process.c (allocate_pty): Use sys_faccessat. * src/sysdep.c (sys_faccessat): New function. --- java/org/gnu/emacs/EmacsView.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 881cbc363ba..a1953f683bd 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -531,8 +531,9 @@ public class EmacsView extends ViewGroup contextMenu = menu; popupActive = true; - /* On API 21 or later, use showContextMenu (float, float). */ - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) + /* Use showContextMenu (float, float) on N to get actual popup + behavior. */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) return showContextMenu ((float) xPosition, (float) yPosition); else return showContextMenu (); -- cgit v1.2.1 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. --- java/org/gnu/emacs/EmacsNative.java | 7 +++++-- java/org/gnu/emacs/EmacsNoninteractive.java | 3 ++- java/org/gnu/emacs/EmacsThread.java | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index a772b965301..7bf8b5f6081 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -65,8 +65,11 @@ public class EmacsNative 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); + loadup.el itself. + + APILEVEL is the version of Android being used. */ + public static native void initEmacs (String argv[], String dumpFile, + int apiLevel); /* 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 b4854d8323f..4da82f2f894 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -177,6 +177,7 @@ public class EmacsNoninteractive EmacsApplication.findDumpFile (context); /* Start Emacs. */ - EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName, + Build.VERSION.SDK_INT); } }; diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index f5e9d54044a..2724d838d41 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -21,6 +21,8 @@ package org.gnu.emacs; import java.lang.Thread; +import android.os.Build; + public class EmacsThread extends Thread { /* Whether or not Emacs should be started -Q. */ @@ -45,6 +47,7 @@ public class EmacsThread extends Thread args = new String[] { "libandroid-emacs.so", "-Q", }; /* Run the native code now. */ - EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName, + Build.VERSION.SDK_INT); } }; -- cgit v1.2.1 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'. --- java/org/gnu/emacs/EmacsNative.java | 4 ++++ java/org/gnu/emacs/EmacsWindow.java | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) (limited to 'java/org/gnu') 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 -- cgit v1.2.1 From e3b50ec8ec1b8d1246aa2496f3fe8bf98d354545 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 28 Jan 2023 14:29:51 +0800 Subject: Update Android port * INSTALL.android: Document support for gnutls and libgmp. * build-aux/ndk-build-helper-1.mk (NDK_SO_NAMES, NDK_INCLUDES) (SYSTEM_LIBRARIES): * build-aux/ndk-build-helper-2.mk: Recursively resolve and add shared library dependencies; even those of static libraries. * build-aux/ndk-module-extract.awk: Fix makefile_imports code. * configure.ac (ANDROID_SDK_18_OR_EARLIER, XCONFIGURE) (LIBGMP_CFLAGS): Enable GMP and gnutls on Android. * cross/ndk-build/Makefile.in (LOCAL_EXPORT_C_INCLUDES): * cross/ndk-build/ndk-build-shared-library.mk: ($(call objname,$(LOCAL_MODULE),$(basename $(1))))::($$(error Unsupported suffix)::($(LOCAL_MODULE_FILENAME)): * cross/ndk-build/ndk-build-static-library.mk: ($(call objname,$(LOCAL_MODULE),$(basename $(1))))::($$(error Unsupported suffix): * cross/ndk-build/ndk-clear-vars.mk: * cross/ndk-build/ndk-resolve.mk (NDK_SYSTEM_LIBRARIES): (NDK_LOCAL_EXPORT_C_INCLUDES_$(LOCAL_MODULE)): (NDK_SO_EXTRA_FLAGS_$(LOCAL_MODULE)): Implement ``LOCAL_ASM_RULE'' and ``LOCAL_C_ADDITIONAL_FLAGS'' extensions for libgmp. * doc/emacs/input.texi (Touchscreens): Document how to horizontally scroll. * java/org/gnu/emacs/EmacsActivity.java (attachWindow): Give the view focus again if necessary. (onPause): Call right super function. * java/org/gnu/emacs/EmacsPreferencesActivity.java (onClick): Clear dumpFileName lest Emacs try to load a nonexistent dump file. * java/org/gnu/emacs/EmacsView.java (onDetachedFromWindow) (onAttachedToWindow): Call super functions. (onCreateInputConnection): Make sure the IME never obscures Emacs. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, onKeyDown) (onKeyUp): Improve tracking of quit keys. * lisp/isearch.el (isearch-mode): Bring up the onscreen keyboard. * lisp/touch-screen.el (touch-screen-current-tool): Add three fields. (touch-screen-handle-scroll): Allow hscrolling as well. (touch-screen-handle-touch): Add additional fields to `touch-screen-current-tool'. * src/Makefile.in (LIBGMP_CFLAGS, EMACS_CFLAGS): Add new variable. * src/android.c (android_run_select_thread): (android_write_event): Use pthread_cond_broadcast because pthread_cond_signal does nothing on some Android versions/devices? --- java/org/gnu/emacs/EmacsActivity.java | 6 +++- java/org/gnu/emacs/EmacsPreferencesActivity.java | 5 ++++ java/org/gnu/emacs/EmacsView.java | 21 ++++++++++++++ java/org/gnu/emacs/EmacsWindow.java | 36 ++++++++++++------------ 4 files changed, 49 insertions(+), 19 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index d377cf982ef..30eaf85805a 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -134,6 +134,10 @@ public class EmacsActivity extends Activity layout.addView (window.view); child.setConsumer (this); + /* If the window isn't no-focus-on-map, focus its view. */ + if (!child.getDontFocusOnMap ()) + window.view.requestFocus (); + /* If the activity is iconified, send that to the window. */ if (isPaused) window.noticeIconified (); @@ -233,7 +237,7 @@ public class EmacsActivity extends Activity isPaused = true; EmacsWindowAttachmentManager.MANAGER.noticeIconified (this); - super.onResume (); + super.onPause (); } @Override diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index ed1db68f732..6cef7c37516 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -116,6 +116,11 @@ public class EmacsPreferencesActivity extends Activity if (file.exists ()) file.delete (); + + /* Make sure to clear EmacsApplication.dumpFileName, or + starting Emacs without restarting this program will + make Emacs try to load a nonexistent dump file. */ + EmacsApplication.dumpFileName = null; } }); layout.addView (textView); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index a1953f683bd..730ed02d4f1 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -22,12 +22,16 @@ package org.gnu.emacs; import android.content.Context; import android.content.res.ColorStateList; +import android.text.InputType; + import android.view.ContextMenu; import android.view.View; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.graphics.Bitmap; @@ -566,6 +570,8 @@ public class EmacsView extends ViewGroup /* Collect the bitmap storage; it could be large. */ Runtime.getRuntime ().gc (); } + + super.onDetachedFromWindow (); } @Override @@ -581,6 +587,8 @@ public class EmacsView extends ViewGroup /* Now expose the view contents again. */ EmacsNative.sendExpose (this.window.handle, 0, 0, measuredWidth, measuredHeight); + + super.onAttachedToWindow (); } public void @@ -615,4 +623,17 @@ public class EmacsView extends ViewGroup drawingFinished = null; } } + + @Override + public InputConnection + onCreateInputConnection (EditorInfo info) + { + /* Make sure the input method never displays a full screen input + 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. */ + return null; + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 39eaf2fff80..5c2b77b0125 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -124,9 +124,9 @@ 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; + /* The time of the last KEYCODE_VOLUME_DOWN release. This is used + to quit Emacs. */ + private long lastVolumeButtonRelease; public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, @@ -517,7 +517,6 @@ 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 (); @@ -549,26 +548,13 @@ 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 onKeyUp (int keyCode, KeyEvent event) { int state, state_1; + long time; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -600,6 +586,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 (time - lastVolumeButtonRelease < 350) + EmacsNative.quit (); + + lastVolumeButtonRelease = time; + } } public void -- cgit v1.2.1 From 198b8160cfeeb178d3b2073c8d03afdafe338908 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 28 Jan 2023 16:29:22 +0800 Subject: Update Android port * doc/emacs/android.texi (Android File System): Describe an easier way to disable scoped storage. * java/AndroidManifest.xml.in: Add new permission to allow that. * java/README: Add more text describing Java. * java/org/gnu/emacs/EmacsContextMenu.java (Item): New fields `isCheckable' and `isChecked'. (EmacsContextMenu, addItem): New arguments. (inflateMenuItems): Set checked status as appropriate. * java/org/gnu/emacs/EmacsCopyArea.java (perform): Disallow operations where width and height are less than or equal to zero. * lisp/menu-bar.el (menu-bar-edit-menu): Make execute-extended-command available as a menu item. * src/androidmenu.c (android_init_emacs_context_menu) (android_menu_show): * src/menu.c (have_boxes): Implement menu check boxes. --- java/org/gnu/emacs/EmacsContextMenu.java | 22 +++++++++++++++++++--- java/org/gnu/emacs/EmacsCopyArea.java | 6 ++++++ 2 files changed, 25 insertions(+), 3 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 056d8fb692c..92429410d03 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -56,7 +56,7 @@ public class EmacsContextMenu public int itemID; public String itemName; public EmacsContextMenu subMenu; - public boolean isEnabled; + public boolean isEnabled, isCheckable, isChecked; @Override public boolean @@ -108,10 +108,15 @@ public class EmacsContextMenu /* Add a normal menu item to the context menu with the id ITEMID and the name ITEMNAME. Enable it if ISENABLED, else keep it - disabled. */ + disabled. + + If this is not a submenu and ISCHECKABLE is set, make the item + checkable. Likewise, if ISCHECKED is set, make the item + checked. */ public void - addItem (int itemID, String itemName, boolean isEnabled) + addItem (int itemID, String itemName, boolean isEnabled, + boolean isCheckable, boolean isChecked) { Item item; @@ -119,6 +124,8 @@ public class EmacsContextMenu item.itemID = itemID; item.itemName = itemName; item.isEnabled = isEnabled; + item.isCheckable = isCheckable; + item.isChecked = isChecked; menuItems.add (item); } @@ -198,6 +205,15 @@ public class EmacsContextMenu /* If the item ID is zero, then disable the item. */ if (item.itemID == 0 || !item.isEnabled) menuItem.setEnabled (false); + + /* Now make the menu item display a checkmark as + appropriate. */ + + if (item.isCheckable) + menuItem.setCheckable (true); + + if (item.isChecked) + menuItem.setChecked (true); } } } diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 7a97d706794..f8974e17c2e 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -99,6 +99,12 @@ public class EmacsCopyArea if (src_y + height > srcBitmap.getHeight ()) height = srcBitmap.getHeight () - src_y; + /* If width and height are empty or negative, then skip the entire + CopyArea operation lest createBitmap throw an exception. */ + + if (width <= 0 || height <= 0) + return; + rect = new Rect (dest_x, dest_y, dest_x + width, dest_y + height); -- cgit v1.2.1 From f9e32ce1575da69cc3a9e4690b6df2dbee41d14d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 28 Jan 2023 21:21:45 +0800 Subject: Implement `restart-emacs' on Android * java/org/gnu/emacs/EmacsService.java (restartEmacs): New function. * src/android.c (struct android_emacs_service) (android_init_emacs_service): Add new method. (android_restart_emacs): New function. * src/android.h: Update prototypes. * src/emacs.c (Fkill_emacs): Call android_restart_emacs whenever appropriate. --- java/org/gnu/emacs/EmacsService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index eb9b61dd876..d17f6d1286c 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -611,4 +611,16 @@ public class EmacsService extends Service return manager.thing; } + + public void + restartEmacs () + { + Intent intent; + + intent = new Intent (this, EmacsActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity (intent); + System.exit (0); + } }; -- cgit v1.2.1 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. --- java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsOpenActivity.java | 357 ++++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsService.java | 1 - 3 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 java/org/gnu/emacs/EmacsOpenActivity.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 4e91a7be322..aba356051cd 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -153,6 +153,10 @@ public class EmacsNative public static native long sendExpose (short window, int x, int y, int width, int height); + /* Return the file name associated with the specified file + descriptor, or NULL if there is none. */ + public static native byte[] getProcName (int fd); + static { System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java new file mode 100644 index 00000000000..268a9abd7b1 --- /dev/null +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -0,0 +1,357 @@ +/* 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; + +/* 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 () { -- cgit v1.2.1 From 98c90135fe5f3729a3fa6e9a724d04a78b2cd514 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 5 Feb 2023 23:02:14 +0800 Subject: Update Android port * INSTALL.android: Explain how to build selinux. * configure.ac: Enable selinux on Android. * cross/ndk-build/ndk-build-shared-library.mk: ($(call objname,$(LOCAL_MODULE),$(basename $(1))))::($$(error Unsupported suffix)::(NDK_CFLAGS_$(LOCAL_MODULE)): * cross/ndk-build/ndk-build-static-library.mk: ($(call objname,$(LOCAL_MODULE),$(basename $(1))))::($$(error Unsupported suffix)::(NDK_CFLAGS_$(LOCAL_MODULE)): Correctly handle files with a .cc suffix, and clang-specific asflags. * cross/ndk-build/ndk-clear-vars.mk: Handle AOSP extensions LOCAL_ADDITIONAL_DEPENDENCIES, LOCAL_CLANG_ASFLAGS_$(NDK_BUILD_ARCH) and LOCAL_IS_HOST_MODULE. * doc/emacs/android.texi (Android Startup): Explain emacsclient wrapper. * java/org/gnu/emacs/EmacsView.java (EmacsView): New flag `isCurrentlyTextEditor'. (showOnScreenKeyboard, hideOnScreenKeyboard): Set as appropriate. (onCheckIsTextEditor): Return its value. * lisp/touch-screen.el (touch-screen-handle-scroll): Don't ding at buffer limits. * m4/ndk-build.m4: Improve doc. * src/Makefile.in (LIBSELINUX_CFLAGS): New variable. (EMACS_CFLAGS): Add it. --- java/org/gnu/emacs/EmacsView.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 730ed02d4f1..873124c86d1 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -106,6 +106,10 @@ public class EmacsView extends ViewGroup /* Whether or not this view is attached to a window. */ public boolean isAttachedToWindow; + /* Whether or not this view should have the on screen keyboard + displayed whenever possible. */ + public boolean isCurrentlyTextEditor; + public EmacsView (EmacsWindow window) { @@ -597,6 +601,7 @@ public class EmacsView extends ViewGroup /* Specifying no flags at all tells the system the user asked for the input method to be displayed. */ imManager.showSoftInput (this, 0); + isCurrentlyTextEditor = true; } public void @@ -604,6 +609,7 @@ public class EmacsView extends ViewGroup { imManager.hideSoftInputFromWindow (this.getWindowToken (), 0); + isCurrentlyTextEditor = false; } public void @@ -636,4 +642,13 @@ public class EmacsView extends ViewGroup screen keyboard to work correctly. */ return null; } + + @Override + public boolean + onCheckIsTextEditor () + { + /* If value is true, then the system will display the on screen + keyboard. */ + return isCurrentlyTextEditor; + } }; -- cgit v1.2.1 From fc82efc1fe99415d60d9aa06f2ff8e7e92566870 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 6 Feb 2023 22:00:08 +0800 Subject: Update Android port * java/AndroidManifest.xml.in: Prevent the Emacs activity from being overlayed by the emacsclient wrapper. * java/org/gnu/emacs/EmacsOpenActivity.java (run): Likewise. (onCreate): Set an appropriate theme on ICS and up. * java/org/gnu/emacs/EmacsWindow.java (onTouchEvent): Handle ACTION_CANCEL correctly. --- java/org/gnu/emacs/EmacsOpenActivity.java | 9 ++++++++- java/org/gnu/emacs/EmacsWindow.java | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index 268a9abd7b1..e987e067a73 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -184,7 +184,9 @@ public class EmacsOpenActivity extends Activity intent = new Intent (EmacsOpenActivity.this, EmacsActivity.class); - intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + + /* This means only an existing frame will be displayed. */ + intent.addFlags (Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); startActivity (intent); EmacsOpenActivity.this.finish (); @@ -285,6 +287,11 @@ public class EmacsOpenActivity extends Activity return; } + /* Set an appropriate theme. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme (android.R.style.Theme_DeviceDefault); + /* Now see if the action specified is supported by Emacs. */ if (action.equals ("android.intent.action.VIEW") diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 5c2b77b0125..e921b972c2c 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -794,7 +794,10 @@ public class EmacsWindow extends EmacsHandleObject case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: - /* Touch up event. */ + 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. */ EmacsNative.sendTouchUp (this.handle, (int) event.getX (index), (int) event.getY (index), event.getEventTime (), pointerID); -- cgit v1.2.1 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') 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.2.1 From 0bd4b7fdab2fdf437c4a759d53dfdc9f667aefb1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 8 Feb 2023 22:40:10 +0800 Subject: Update Android port * doc/lispref/frames.texi (On-Screen Keyboards): Describe return value of `frame-toggle-on-screen-keyboard'. * java/org/gnu/emacs/EmacsSurfaceView.java (surfaceChanged) (surfaceCreated, EmacsSurfaceView): Remove unuseful synchronization code. The framework doesn't seem to look at this at all. * java/org/gnu/emacs/EmacsView.java (EmacsView): (onLayout): Lay out the window after children. (swapBuffers): Properly implement `force'. (windowUpdated): Delete function. * lisp/frame.el (frame-toggle-on-screen-keyboard): Return whether or not the on screen keyboard might've been displayed. * lisp/minibuffer.el (minibuffer-on-screen-keyboard-timer): (minibuffer-on-screen-keyboard-displayed): (minibuffer-setup-on-screen-keyboard): (minibuffer-exit-on-screen-keyboard): Improve OSK dismissal when there are consecutive minibuffers. * lisp/touch-screen.el (touch-screen-window-selection-changed): New function. (touch-screen-handle-point-up): Register it as a window selection changed function. * src/android.c (struct android_emacs_window) (android_init_emacs_window): Remove references to `windowUpdated'. (android_window_updated): Delete function. * src/android.h (struct android_output): Remove `last_configure_serial'. * src/androidterm.c (handle_one_android_event) (android_frame_up_to_date): * src/androidterm.h (struct android_output): Remove frame synchronization, as that does not work on Android. --- java/org/gnu/emacs/EmacsSurfaceView.java | 54 +++-------------------------- java/org/gnu/emacs/EmacsView.java | 58 ++++++++++++-------------------- 2 files changed, 27 insertions(+), 85 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 2fe9e103b2b..f6cb77bb2b8 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -45,14 +45,11 @@ public class EmacsSurfaceView extends SurfaceView surfaceChanged (SurfaceHolder holder, int format, int width, int height) { - Log.d (TAG, "surfaceChanged: " + view + ", " + view.pendingConfigure); + Canvas canvas; - /* Make sure not to swap buffers if there is pending - configuration, because otherwise the redraw callback will not - run correctly. */ + Log.d (TAG, "surfaceChanged: " + view + ", "); - if (view.pendingConfigure == 0) - view.swapBuffers (); + view.swapBuffers (true); } @Override @@ -67,7 +64,7 @@ public class EmacsSurfaceView extends SurfaceView /* Drop the lock when doing this, or a deadlock can result. */ - view.swapBuffers (); + view.swapBuffers (true); } @Override @@ -82,44 +79,6 @@ public class EmacsSurfaceView extends SurfaceView } } - /* 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) { @@ -128,10 +87,7 @@ public class EmacsSurfaceView extends SurfaceView 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 ()); + getHolder ().addCallback (new Callback ()); } public boolean diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 873124c86d1..fac11870ebf 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -96,13 +96,6 @@ public class EmacsView extends ViewGroup /* 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; - /* Whether or not this view is attached to a window. */ public boolean isAttachedToWindow; @@ -110,6 +103,14 @@ public class EmacsView extends ViewGroup displayed whenever possible. */ public boolean isCurrentlyTextEditor; + /* An empty rectangle. */ + public static final Rect emptyRect; + + static + { + emptyRect = new Rect (); + }; + public EmacsView (EmacsWindow window) { @@ -286,16 +287,10 @@ public class EmacsView extends ViewGroup int count, i; View child; Rect windowRect; + int wantedWidth, wantedHeight; count = getChildCount (); - if (changed || mustReportLayout) - { - mustReportLayout = false; - pendingConfigure - = window.viewLayout (left, top, right, bottom); - } - measuredWidth = right - left; measuredHeight = bottom - top; @@ -311,8 +306,6 @@ public class EmacsView extends ViewGroup Log.d (TAG, "onLayout: " + child); if (child == surfaceView) - /* The child is the surface view, so give it the entire - view. */ child.layout (0, 0, right - left, bottom - top); else if (child.getVisibility () != GONE) { @@ -326,6 +319,14 @@ public class EmacsView extends ViewGroup windowRect.right, windowRect.bottom); } } + + /* Now report the layout change to the window. */ + + if (changed || mustReportLayout) + { + mustReportLayout = false; + window.viewLayout (left, top, right, bottom); + } } public void @@ -352,7 +353,7 @@ public class EmacsView extends ViewGroup synchronized (damageRegion) { - if (damageRegion.isEmpty ()) + if (!force && damageRegion.isEmpty ()) return; bitmap = getBitmap (); @@ -363,7 +364,10 @@ public class EmacsView extends ViewGroup synchronized (surfaceView.surfaceChangeLock) { - damageRect = damageRegion.getBounds (); + if (!force) + damageRect = damageRegion.getBounds (); + else + damageRect = emptyRect; if (!surfaceView.isCreated ()) return; @@ -612,24 +616,6 @@ public class EmacsView extends ViewGroup isCurrentlyTextEditor = false; } - 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; - } - } - @Override public InputConnection onCreateInputConnection (EditorInfo info) -- cgit v1.2.1 From 209ae003b7444d2e9b195db9475ddbdefa8f9c64 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 9 Feb 2023 22:56:41 +0800 Subject: Allow other text editors to edit files in Emacs' home directory * java/AndroidManifest.xml.in: Declare the new documents provider. * java/README: Describe the meaning of files in res/values. * java/org/gnu/emacs/EmacsDocumentsProvider.java (EmacsDocumentsProvider): New file. * java/res/values-v19/bool.xml: * java/res/values/bool.xml: New files. --- java/org/gnu/emacs/EmacsDocumentsProvider.java | 381 +++++++++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 java/org/gnu/emacs/EmacsDocumentsProvider.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java new file mode 100644 index 00000000000..f12b302ff84 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -0,0 +1,381 @@ +/* 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.Context; + +import android.database.Cursor; +import android.database.MatrixCursor; + +import android.os.Build; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; + +import android.provider.DocumentsContract.Document; +import android.provider.DocumentsContract.Root; +import static android.provider.DocumentsContract.buildChildDocumentsUri; +import android.provider.DocumentsProvider; + +import android.webkit.MimeTypeMap; + +import android.net.Uri; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +/* ``Documents provider''. This allows Emacs's home directory to be + modified by other programs holding permissions to manage system + storage, which is useful to (for example) correct misconfigurations + which prevent Emacs from starting up. + + This functionality is only available on Android 19 and later. */ + +public class EmacsDocumentsProvider extends DocumentsProvider +{ + /* Home directory. This is the directory whose contents are + initially returned to requesting applications. */ + private File baseDir; + + /* The default projection for requests for the root directory. */ + private static final String[] DEFAULT_ROOT_PROJECTION; + + /* The default projection for requests for a file. */ + private static final String[] DEFAULT_DOCUMENT_PROJECTION; + + static + { + DEFAULT_ROOT_PROJECTION = new String[] { + Root.COLUMN_ROOT_ID, + Root.COLUMN_MIME_TYPES, + Root.COLUMN_FLAGS, + Root.COLUMN_TITLE, + Root.COLUMN_SUMMARY, + Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_AVAILABLE_BYTES, + }; + + DEFAULT_DOCUMENT_PROJECTION = new String[] { + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_FLAGS, + Document.COLUMN_SIZE, + }; + } + + @Override + public boolean + onCreate () + { + /* Set the base directory to Emacs's files directory. */ + baseDir = getContext ().getFilesDir (); + return true; + } + + @Override + public Cursor + queryRoots (String[] projection) + { + MatrixCursor result; + MatrixCursor.RowBuilder row; + + /* If the requestor asked for nothing at all, then it wants some + data by default. */ + + if (projection == null) + projection = DEFAULT_ROOT_PROJECTION; + + result = new MatrixCursor (projection); + row = result.newRow (); + + /* Now create and add a row for each file in the base + directory. */ + row.add (Root.COLUMN_ROOT_ID, baseDir.getAbsolutePath ()); + row.add (Root.COLUMN_SUMMARY, "Emacs home directory"); + + /* Add the appropriate flags. */ + + row.add (Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE); + row.add (Root.FLAG_LOCAL_ONLY); + row.add (Root.COLUMN_TITLE, "Emacs"); + row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ()); + + return result; + } + + /* Return the MIME type of a file FILE. */ + + private String + getMimeType (File file) + { + String name, extension, mime; + int extensionSeparator; + + if (file.isDirectory ()) + return Document.MIME_TYPE_DIR; + + /* Abuse WebView stuff to get the file's MIME type. */ + name = file.getName (); + extensionSeparator = name.lastIndexOf ('.'); + + if (extensionSeparator > 0) + { + extension = name.substring (extensionSeparator + 1); + mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension (extension); + + if (mime != null) + return mime; + } + + return "application/octet-stream"; + } + + /* Append the specified FILE to the query result RESULT. + Handle both directories and ordinary files. */ + + private void + queryDocument1 (MatrixCursor result, File file) + { + MatrixCursor.RowBuilder row; + String fileName, displayName, mimeType; + int flags; + + row = result.newRow (); + flags = 0; + + /* fileName is a string that the system will ask for some time in + the future. Here, it is just the absolute name of the file. */ + fileName = file.getAbsolutePath (); + + /* If file is a directory, add the right flags for that. */ + + if (file.isDirectory ()) + { + if (file.canWrite ()) + { + flags |= Document.FLAG_DIR_SUPPORTS_CREATE; + flags |= Document.FLAG_SUPPORTS_DELETE; + } + } + else if (file.canWrite ()) + { + /* Apply the correct flags for a writable file. */ + flags |= Document.FLAG_SUPPORTS_WRITE; + flags |= Document.FLAG_SUPPORTS_DELETE; + + /* TODO: implement these + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + flags |= Document.FLAG_SUPPORTS_RENAME; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + flags |= Document.FLAG_SUPPORTS_REMOVE; */ + } + + displayName = file.getName (); + mimeType = getMimeType (file); + + row.add (Document.COLUMN_DOCUMENT_ID, fileName); + row.add (Document.COLUMN_DISPLAY_NAME, displayName); + row.add (Document.COLUMN_SIZE, file.length ()); + row.add (Document.COLUMN_MIME_TYPE, mimeType); + row.add (Document.COLUMN_LAST_MODIFIED, file.lastModified ()); + row.add (Document.COLUMN_FLAGS, flags); + } + + @Override + public Cursor + queryDocument (String documentId, String[] projection) + throws FileNotFoundException + { + MatrixCursor result; + + if (projection == null) + projection = DEFAULT_DOCUMENT_PROJECTION; + + result = new MatrixCursor (projection); + queryDocument1 (result, new File (documentId)); + + return result; + } + + @Override + public Cursor + queryChildDocuments (String parentDocumentId, String[] projection, + String sortOrder) throws FileNotFoundException + { + MatrixCursor result; + File directory; + + if (projection == null) + projection = DEFAULT_DOCUMENT_PROJECTION; + + result = new MatrixCursor (projection); + + /* Try to open the file corresponding to the location being + requested. */ + directory = new File (parentDocumentId); + + /* Now add each child. */ + for (File child : directory.listFiles ()) + queryDocument1 (result, child); + + return result; + } + + @Override + public ParcelFileDescriptor + openDocument (String documentId, String mode, + CancellationSignal signal) throws FileNotFoundException + { + return ParcelFileDescriptor.open (new File (documentId), + ParcelFileDescriptor.parseMode (mode)); + } + + @Override + public String + createDocument (String documentId, String mimeType, + String displayName) throws FileNotFoundException + { + File file; + boolean rc; + Uri updatedUri; + Context context; + + context = getContext (); + file = new File (documentId, displayName); + + try + { + rc = false; + + if (Document.MIME_TYPE_DIR.equals (mimeType)) + { + file.mkdirs (); + + if (file.isDirectory ()) + rc = true; + } + else + { + file.createNewFile (); + + if (file.isFile () + && file.setWritable (true) + && file.setReadable (true)) + rc = true; + } + + if (!rc) + throw new FileNotFoundException ("rc != 1"); + } + catch (IOException e) + { + throw new FileNotFoundException (e.toString ()); + } + + updatedUri + = buildChildDocumentsUri ("org.gnu.emacs", documentId); + /* Tell the system about the change. */ + context.getContentResolver ().notifyChange (updatedUri, null); + + return file.getAbsolutePath (); + } + + private void + deleteDocument1 (File child) + { + File[] children; + + /* Don't delete symlinks recursively. + + Calling readlink or stat is problematic due to file name + encoding problems, so try to delete the file first, and only + try to delete files recursively afterword. */ + + if (child.delete ()) + return; + + children = child.listFiles (); + + if (children != null) + { + for (File file : children) + deleteDocument1 (file); + } + + child.delete (); + } + + @Override + public void + deleteDocument (String documentId) + throws FileNotFoundException + { + File file, parent; + File[] children; + Uri updatedUri; + Context context; + + /* Java makes recursively deleting a file hard. File name + encoding issues also prevent easily calling into C... */ + + context = getContext (); + file = new File (documentId); + parent = file.getParentFile (); + + if (parent == null) + throw new RuntimeException ("trying to delete file without" + + " parent!"); + + updatedUri + = buildChildDocumentsUri ("org.gnu.emacs", + parent.getAbsolutePath ()); + + if (file.delete ()) + { + /* Tell the system about the change. */ + context.getContentResolver ().notifyChange (updatedUri, null); + return; + } + + children = file.listFiles (); + + if (children != null) + { + for (File child : children) + deleteDocument1 (child); + } + + if (file.delete ()) + /* Tell the system about the change. */ + context.getContentResolver ().notifyChange (updatedUri, null); + } + + @Override + public void + removeDocument (String documentId, String parentDocumentId) + throws FileNotFoundException + { + deleteDocument (documentId); + } +} -- cgit v1.2.1 From a1941cd7a7dc9a6f6b7239ec7d4bd3bdf5d55fc9 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Feb 2023 18:57:51 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Windowing): Remove yet another limitation. * java/debug.sh: Make this work on systems which prohibit attaching to app processes from adbd. * java/org/gnu/emacs/EmacsCopyArea.java (perform): Avoid creating copies whenever possible. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): Remove SurfaceView based implementation and use manual double buffering with invalidate instead. * java/org/gnu/emacs/EmacsView.java (EmacsView, handleDirtyBitmap) (raise, lower, onDetachedFromWindow): Adjust accordingly. * java/org/gnu/emacs/EmacsWindow.java (windowUpdated): Remove function. * src/sfntfont.c (sfntfont_open): Set font->max_width correctly. --- java/org/gnu/emacs/EmacsCopyArea.java | 24 ++++- java/org/gnu/emacs/EmacsSurfaceView.java | 159 ++++++++++++++----------------- java/org/gnu/emacs/EmacsView.java | 93 +++--------------- java/org/gnu/emacs/EmacsWindow.java | 23 ----- 4 files changed, 104 insertions(+), 195 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index f8974e17c2e..11dc22e0456 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -110,11 +110,25 @@ public class EmacsCopyArea if (gc.clip_mask == null) { - bitmap = Bitmap.createBitmap (srcBitmap, - src_x, src_y, width, - height); - canvas.drawBitmap (bitmap, null, rect, paint); - bitmap.recycle (); + if (source == destination) + { + /* Create a copy of the bitmap, since Android can't handle + overlapping copies. */ + bitmap = Bitmap.createBitmap (srcBitmap, + src_x, src_y, width, + height); + canvas.drawBitmap (bitmap, null, rect, paint); + bitmap.recycle (); + } + else + { + /* But here the bitmaps are known to not overlap, so avoid + that extra consing overhead. */ + + srcRect = new Rect (src_x, src_y, src_x + width, + src_y + height); + canvas.drawBitmap (srcBitmap, null, rect, paint); + } } else { diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index f6cb77bb2b8..e9bae623930 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -19,127 +19,114 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import android.view.SurfaceView; -import android.view.SurfaceHolder; +import android.view.View; import android.os.Build; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; +import android.graphics.Paint; -import android.util.Log; +/* This originally extended SurfaceView. However, doing so proved to + be too slow, and Android's surface view keeps up to three of its + own back buffers, which use too much memory (up to 96 MB for a + single frame.) */ -public class EmacsSurfaceView extends SurfaceView +public class EmacsSurfaceView extends View { private static final String TAG = "EmacsSurfaceView"; - public Object surfaceChangeLock; - private boolean created; private EmacsView view; - - /* This is the callback used on Android 8 to 25. */ - - private class Callback implements SurfaceHolder.Callback - { - @Override - public void - surfaceChanged (SurfaceHolder holder, int format, - int width, int height) - { - Canvas canvas; - - Log.d (TAG, "surfaceChanged: " + view + ", "); - - view.swapBuffers (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 (true); - } - - @Override - public void - surfaceDestroyed (SurfaceHolder holder) - { - synchronized (surfaceChangeLock) - { - Log.d (TAG, "surfaceDestroyed: " + view); - created = false; - } - } - } + private Bitmap frontBuffer; + private Canvas bitmapCanvas; + private Bitmap bitmap; + private Paint bitmapPaint; public EmacsSurfaceView (final EmacsView view) { super (view.getContext ()); - this.surfaceChangeLock = new Object (); this.view = view; - - getHolder ().addCallback (new Callback ()); + this.bitmapPaint = new Paint (); } - public boolean - isCreated () + private void + copyToFrontBuffer (Rect damageRect) { - return created; + if (damageRect != null) + bitmapCanvas.drawBitmap (bitmap, damageRect, damageRect, + bitmapPaint); + else + bitmapCanvas.drawBitmap (bitmap, 0f, 0f, bitmapPaint); } - public Canvas - lockCanvas (Rect damage) + private void + reconfigureFrontBuffer (Bitmap bitmap) { - SurfaceHolder holder; - - holder = getHolder (); + /* First, remove the old front buffer. */ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + if (frontBuffer != null) { - damage.setEmpty (); - return holder.lockHardwareCanvas (); + frontBuffer.recycle (); + frontBuffer = null; + bitmapCanvas = null; } - return holder.lockCanvas (damage); - } + this.bitmap = bitmap; - @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 ())); - } + /* Next, create the new front buffer if necessary. */ - /* This method is only used during debugging when it seems damage - isn't working correctly. */ + if (bitmap != null && frontBuffer == null) + { + frontBuffer = Bitmap.createBitmap (bitmap.getWidth (), + bitmap.getHeight (), + Bitmap.Config.ARGB_8888, + false); + bitmapCanvas = new Canvas (frontBuffer); + + /* And copy over the bitmap contents. */ + copyToFrontBuffer (null); + } + else if (bitmap != null) + /* Just copy over the bitmap contents. */ + copyToFrontBuffer (null); + } - public Canvas - lockCanvas () + public synchronized void + setBitmap (Bitmap bitmap, Rect damageRect) { - SurfaceHolder holder; + if (bitmap != this.bitmap) + reconfigureFrontBuffer (bitmap); + else if (bitmap != null) + copyToFrontBuffer (damageRect); - holder = getHolder (); - return holder.lockCanvas (); + if (bitmap != null) + { + /* In newer versions of Android, the invalid rectangle is + supposedly internally calculated by the system. How that + is done is unknown, but calling `invalidateRect' is now + deprecated. + + Fortunately, nobody has deprecated the version of + `postInvalidate' that accepts a dirty rectangle. */ + + if (damageRect != null) + postInvalidate (damageRect.left, damageRect.top, + damageRect.right, damageRect.bottom); + else + postInvalidate (); + } } - public void - unlockCanvasAndPost (Canvas canvas) + @Override + public synchronized void + onDraw (Canvas canvas) { - SurfaceHolder holder; + /* Paint the view's bitmap; the bitmap might be recycled right + now. */ - holder = getHolder (); - holder.unlockCanvasAndPost (canvas); + if (frontBuffer != null) + canvas.drawBitmap (frontBuffer, 0f, 0f, bitmapPaint); } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index fac11870ebf..0416301101c 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -103,14 +103,6 @@ public class EmacsView extends ViewGroup displayed whenever possible. */ public boolean isCurrentlyTextEditor; - /* An empty rectangle. */ - public static final Rect emptyRect; - - static - { - emptyRect = new Rect (); - }; - public EmacsView (EmacsWindow window) { @@ -127,14 +119,8 @@ 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 default focus highlight. */ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) setDefaultFocusHighlightEnabled (false); @@ -191,8 +177,12 @@ public class EmacsView extends ViewGroup bitmapDirty = false; /* Explicitly free the old bitmap's memory. */ + if (oldBitmap != null) - oldBitmap.recycle (); + { + oldBitmap.recycle (); + surfaceView.setBitmap (null, null); + } /* Some Android versions still don't free the bitmap until the next GC. */ @@ -342,67 +332,27 @@ public class EmacsView extends ViewGroup thread. */ public void - swapBuffers (boolean force) + swapBuffers () { Canvas canvas; Rect damageRect; Bitmap bitmap; - /* Code must always take damageRegion, and then surfaceChangeLock, - never the other way around! */ + damageRect = null; synchronized (damageRegion) { - if (!force && damageRegion.isEmpty ()) + if (damageRegion.isEmpty ()) return; bitmap = getBitmap (); - /* 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) - { - if (!force) - damageRect = damageRegion.getBounds (); - else - damageRect = emptyRect; - - if (!surfaceView.isCreated ()) - return; - - if (bitmap == null) - return; - - /* Lock the canvas with the specified damage. */ - canvas = surfaceView.lockCanvas (damageRect); - - /* Return if locking the canvas failed. */ - if (canvas == null) - return; - - /* 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 (); - } + /* Transfer the bitmap to the surface view, then invalidate + it. */ + surfaceView.setBitmap (bitmap, damageRect); } } - public void - swapBuffers () - { - swapBuffers (false); - } - @Override public boolean onKeyDown (int keyCode, KeyEvent event) @@ -486,16 +436,6 @@ public class EmacsView extends ViewGroup 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 @@ -511,16 +451,6 @@ public class EmacsView extends ViewGroup 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); } @Override @@ -574,6 +504,7 @@ public class EmacsView extends ViewGroup bitmap.recycle (); bitmap = null; canvas = null; + surfaceView.setBitmap (null, null); /* Collect the bitmap storage; it could be large. */ Runtime.getRuntime ().gc (); diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index e921b972c2c..9e2f2f53270 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -57,12 +57,6 @@ 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 { @@ -1111,21 +1105,4 @@ public class EmacsWindow extends EmacsHandleObject } }); } - - /* 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); - } - }); - } }; -- cgit v1.2.1 From 664140fc26240726f6ec9babe7deb633dd8c0bd2 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Feb 2023 19:06:38 +0800 Subject: Fix buffer swapping on Android 7.1 and earlier * java/org/gnu/emacs/EmacsSurfaceView.java (reconfigureFrontBuffer): Don't use function only present on Android 8.0 and later. --- java/org/gnu/emacs/EmacsSurfaceView.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index e9bae623930..2d80be0881a 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -79,10 +79,16 @@ public class EmacsSurfaceView extends View if (bitmap != null && frontBuffer == null) { - frontBuffer = Bitmap.createBitmap (bitmap.getWidth (), - bitmap.getHeight (), - Bitmap.Config.ARGB_8888, - false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + frontBuffer = Bitmap.createBitmap (bitmap.getWidth (), + bitmap.getHeight (), + Bitmap.Config.ARGB_8888, + false); + else + frontBuffer = Bitmap.createBitmap (bitmap.getWidth (), + bitmap.getHeight (), + Bitmap.Config.ARGB_8888); + bitmapCanvas = new Canvas (frontBuffer); /* And copy over the bitmap contents. */ -- cgit v1.2.1 From 9b50500b22ff1f71e80bf6aee98c44493c5ebcce Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Feb 2023 19:25:10 +0800 Subject: ; * java/org/gnu/emacs/EmacsCopyArea.java (perform): Fix typo. --- java/org/gnu/emacs/EmacsCopyArea.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 11dc22e0456..1daa2190542 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -127,7 +127,7 @@ public class EmacsCopyArea srcRect = new Rect (src_x, src_y, src_x + width, src_y + height); - canvas.drawBitmap (srcBitmap, null, rect, paint); + canvas.drawBitmap (srcBitmap, srcRect, rect, paint); } } else -- cgit v1.2.1 From 2489126e6856bf1b06a26127b73e4bfff857f68f Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Feb 2023 21:07:14 +0800 Subject: Implement more features for the Emacs ``documents provider'' * java/org/gnu/emacs/EmacsDocumentsProvider.java (queryRoots): Implement isChild. (getNotificationUri, notifyChange): New functions. (queryDocument1): Set rename and remove flags. (queryDocument, queryChildDocuments): Allow the requester to detect changes in the directory hierarchy. (createDocument, deleteDocument, removeDocument): Signal changes to the directory hierarchy. --- java/org/gnu/emacs/EmacsDocumentsProvider.java | 124 ++++++++++++++++++++----- 1 file changed, 101 insertions(+), 23 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index f12b302ff84..3c3c7ead3c5 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -114,7 +114,8 @@ public class EmacsDocumentsProvider extends DocumentsProvider /* Add the appropriate flags. */ - row.add (Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE); + row.add (Root.COLUMN_FLAGS, (Root.FLAG_SUPPORTS_CREATE + | Root.FLAG_SUPPORTS_IS_CHILD)); row.add (Root.FLAG_LOCAL_ONLY); row.add (Root.COLUMN_TITLE, "Emacs"); row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ()); @@ -122,6 +123,36 @@ public class EmacsDocumentsProvider extends DocumentsProvider return result; } + private Uri + getNotificationUri (File file) + { + Uri updatedUri; + Context context; + + context = getContext (); + updatedUri + = buildChildDocumentsUri ("org.gnu.emacs", + file.getAbsolutePath ()); + + return updatedUri; + } + + /* Inform the system that FILE's contents (or FILE itself) has + changed. */ + + private void + notifyChange (File file) + { + Uri updatedUri; + Context context; + + context = getContext (); + updatedUri + = buildChildDocumentsUri ("org.gnu.emacs", + file.getAbsolutePath ()); + context.getContentResolver ().notifyChange (updatedUri, null); + } + /* Return the MIME type of a file FILE. */ private String @@ -174,6 +205,9 @@ public class EmacsDocumentsProvider extends DocumentsProvider { flags |= Document.FLAG_DIR_SUPPORTS_CREATE; flags |= Document.FLAG_SUPPORTS_DELETE; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + flags |= Document.FLAG_SUPPORTS_RENAME; } } else if (file.canWrite ()) @@ -182,13 +216,11 @@ public class EmacsDocumentsProvider extends DocumentsProvider flags |= Document.FLAG_SUPPORTS_WRITE; flags |= Document.FLAG_SUPPORTS_DELETE; - /* TODO: implement these + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + flags |= Document.FLAG_SUPPORTS_RENAME; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - flags |= Document.FLAG_SUPPORTS_RENAME; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - flags |= Document.FLAG_SUPPORTS_REMOVE; */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + flags |= Document.FLAG_SUPPORTS_REMOVE; } displayName = file.getName (); @@ -208,12 +240,21 @@ public class EmacsDocumentsProvider extends DocumentsProvider throws FileNotFoundException { MatrixCursor result; + File file; + Context context; + + file = new File (documentId); + context = getContext (); if (projection == null) projection = DEFAULT_DOCUMENT_PROJECTION; result = new MatrixCursor (projection); - queryDocument1 (result, new File (documentId)); + queryDocument1 (result, file); + + /* Now allow interested applications to detect changes. */ + result.setNotificationUri (context.getContentResolver (), + getNotificationUri (file)); return result; } @@ -225,6 +266,7 @@ public class EmacsDocumentsProvider extends DocumentsProvider { MatrixCursor result; File directory; + Context context; if (projection == null) projection = DEFAULT_DOCUMENT_PROJECTION; @@ -239,6 +281,12 @@ public class EmacsDocumentsProvider extends DocumentsProvider for (File child : directory.listFiles ()) queryDocument1 (result, child); + context = getContext (); + + /* Now allow interested applications to detect changes. */ + result.setNotificationUri (context.getContentResolver (), + getNotificationUri (directory)); + return result; } @@ -256,12 +304,9 @@ public class EmacsDocumentsProvider extends DocumentsProvider createDocument (String documentId, String mimeType, String displayName) throws FileNotFoundException { - File file; + File file, parentFile; boolean rc; - Uri updatedUri; - Context context; - context = getContext (); file = new File (documentId, displayName); try @@ -293,10 +338,10 @@ public class EmacsDocumentsProvider extends DocumentsProvider throw new FileNotFoundException (e.toString ()); } - updatedUri - = buildChildDocumentsUri ("org.gnu.emacs", documentId); - /* Tell the system about the change. */ - context.getContentResolver ().notifyChange (updatedUri, null); + parentFile = file.getParentFile (); + + if (parentFile != null) + notifyChange (parentFile); return file.getAbsolutePath (); } @@ -333,7 +378,6 @@ public class EmacsDocumentsProvider extends DocumentsProvider { File file, parent; File[] children; - Uri updatedUri; Context context; /* Java makes recursively deleting a file hard. File name @@ -347,14 +391,10 @@ public class EmacsDocumentsProvider extends DocumentsProvider throw new RuntimeException ("trying to delete file without" + " parent!"); - updatedUri - = buildChildDocumentsUri ("org.gnu.emacs", - parent.getAbsolutePath ()); - if (file.delete ()) { /* Tell the system about the change. */ - context.getContentResolver ().notifyChange (updatedUri, null); + notifyChange (parent); return; } @@ -368,7 +408,7 @@ public class EmacsDocumentsProvider extends DocumentsProvider if (file.delete ()) /* Tell the system about the change. */ - context.getContentResolver ().notifyChange (updatedUri, null); + notifyChange (parent); } @Override @@ -378,4 +418,42 @@ public class EmacsDocumentsProvider extends DocumentsProvider { deleteDocument (documentId); } + + @Override + public String + getDocumentType (String documentId) + { + return getMimeType (new File (documentId)); + } + + @Override + public String + renameDocument (String documentId, String displayName) + throws FileNotFoundException + { + File file, newName; + File parent; + + file = new File (documentId); + parent = file.getParentFile (); + newName = new File (parent, displayName); + + if (parent == null) + throw new FileNotFoundException ("parent is null"); + + file = new File (documentId); + + if (!file.renameTo (newName)) + return null; + + notifyChange (parent); + return newName.getAbsolutePath (); + } + + @Override + public boolean + isChildDocument (String parentDocumentId, String documentId) + { + return documentId.startsWith (parentDocumentId); + } } -- cgit v1.2.1 From dc120c7ad62d5f79fe50f72431d3b9bb2d7f1558 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Feb 2023 23:03:43 +0800 Subject: Improve appearance of the Android preferences screen * .gitignore: Add org/gnu/emacs/R.java. * cross/Makefile.in (top_builddir): Include verbose.mk. Rewrite rules to print nice looking statements. * doc/emacs/android.texi (Android, Android Startup) (Android Environment, Android Windowing, Android Fonts): * doc/emacs/emacs.texi (Top): Add an extra ``Android Troubleshooting'' node and move troubleshooting details there. * java/Makefile.in: Generate R.java; improve appearance by using verbose.mk. * java/org/gnu/emacs/EmacsPreferencesActivity.java: Reimplement in terms of PreferencesActivity. * java/org/gnu/emacs/EmacsView.java (handleDirtyBitmap): Avoid flicker. * java/res/xml/preferences.xml: New file. * src/verbose.mk.in (AM_V_AAPT, AM_V_SILENT): New variables. --- java/org/gnu/emacs/EmacsPreferencesActivity.java | 123 ++++++++++++----------- java/org/gnu/emacs/EmacsView.java | 5 +- 2 files changed, 71 insertions(+), 57 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index 6cef7c37516..85639fe9236 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -22,27 +22,28 @@ package org.gnu.emacs; import java.io.File; 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; +import android.widget.Toast; + +import android.preference.*; /* 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. */ + command line on Android. -public class EmacsPreferencesActivity extends Activity -{ - /* The linear layout associated with the activity. */ - private LinearLayout layout; + Android provides a preferences activity, but it is deprecated. + Unfortunately, there is no alternative that looks the same way. */ +@SuppressWarnings ("deprecation") +public 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. */ @@ -59,72 +60,82 @@ public class EmacsPreferencesActivity extends Activity System.exit (0); } + /* Erase Emacs's dump file. */ + + private void + eraseDumpFile () + { + String wantedDumpFile; + File file; + Toast toast; + + wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () + + ".pdmp"); + file = new File (getFilesDir (), wantedDumpFile); + + if (file.exists ()) + file.delete (); + + /* Make sure to clear EmacsApplication.dumpFileName, or + starting Emacs without restarting this program will + make Emacs try to load a nonexistent dump file. */ + EmacsApplication.dumpFileName = null; + + /* Display a message stating that the dump file has been + erased. */ + toast = Toast.makeText (this, "Dump file removed", + Toast.LENGTH_SHORT); + toast.show (); + } + @Override public void onCreate (Bundle savedInstanceState) { - LinearLayout layout; - TextView textView; - LinearLayout.LayoutParams params; - int resid; + Preference tem; + Preference.OnPreferenceClickListener listener; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - setTheme (R.style.Theme_DeviceDefault_Settings); + setTheme (android.R.style.Theme_DeviceDefault_Settings); else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) - setTheme (R.style.Theme_DeviceDefault); + setTheme (android.R.style.Theme_DeviceDefault); + + /* This must come before using any preference APIs. */ + super.onCreate (savedInstanceState); - layout = new LinearLayout (this); - layout.setOrientation (LinearLayout.VERTICAL); - setContentView (layout); + /* Add preferences from the XML file where they are defined. */ + addPreferencesFromResource (R.xml.preferences); - textView = new TextView (this); - textView.setPadding (8, 20, 20, 8); + /* Now, set up on click handlers for each of the preferences + items. */ - 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 () { + tem = findPreference ("start_quick"); + + listener = new Preference.OnPreferenceClickListener () { @Override - public void - onClick (View view) + public boolean + onPreferenceClick (Preference preference) { startEmacsQ (); + return true; } - }); - layout.addView (textView); + }; + + tem.setOnPreferenceClickListener (listener); - textView = new TextView (this); - textView.setPadding (8, 20, 20, 8); + tem = findPreference ("erase_dump"); - params = new LinearLayout.LayoutParams (LayoutParams.MATCH_PARENT, - LayoutParams.WRAP_CONTENT); - textView.setLayoutParams (params); - textView.setText ("Erase dump file"); - textView.setOnClickListener (new View.OnClickListener () { + listener = new Preference.OnPreferenceClickListener () { @Override - public void - onClick (View view) + public boolean + onPreferenceClick (Preference preference) { - String wantedDumpFile; - File file; - - wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () - + ".pdmp"); - file = new File (getFilesDir (), wantedDumpFile); - - if (file.exists ()) - file.delete (); - - /* Make sure to clear EmacsApplication.dumpFileName, or - starting Emacs without restarting this program will - make Emacs try to load a nonexistent dump file. */ - EmacsApplication.dumpFileName = null; + eraseDumpFile (); + return true; } - }); - layout.addView (textView); + }; - super.onCreate (savedInstanceState); + tem.setOnPreferenceClickListener (listener); } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 0416301101c..4fc8104e31f 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -181,7 +181,10 @@ public class EmacsView extends ViewGroup if (oldBitmap != null) { oldBitmap.recycle (); - surfaceView.setBitmap (null, null); + + /* Make sure to set the view's bitmap to the new bitmap, or + ugly flicker can result. */ + surfaceView.setBitmap (bitmap, null); } /* Some Android versions still don't free the bitmap until the -- cgit v1.2.1 From ab48881a2fb72df016f2a00bc107e5a35a411a9d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 11 Feb 2023 10:26:34 +0800 Subject: Fix displaying popup menus from a menu entry on Android * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity, onDestroy) (onWindowFocusChanged): Keep track of the last focused activity. * java/org/gnu/emacs/EmacsDialog.java (display1): Use it if there is no current focus. --- java/org/gnu/emacs/EmacsActivity.java | 13 ++++++++++++- java/org/gnu/emacs/EmacsDialog.java | 26 +++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 30eaf85805a..3156a144a0f 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -47,6 +47,9 @@ public class EmacsActivity extends Activity /* List of activities with focus. */ public static List focusedActivities; + /* The last activity to have been focused. */ + public static EmacsActivity lastFocusedActivity; + /* The currently focused window. */ public static EmacsWindow focusedWindow; @@ -215,6 +218,11 @@ public class EmacsActivity extends Activity manager.removeWindowConsumer (this, isMultitask || isFinishing ()); focusedActivities.remove (this); invalidateFocus (); + + /* Remove this activity from the static field, lest it leak. */ + if (lastFocusedActivity == this) + lastFocusedActivity = null; + super.onDestroy (); } @@ -223,7 +231,10 @@ public class EmacsActivity extends Activity onWindowFocusChanged (boolean isFocused) { if (isFocused && !focusedActivities.contains (this)) - focusedActivities.add (this); + { + focusedActivities.add (this); + lastFocusedActivity = this; + } else focusedActivities.remove (this); diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 7d88a23c58f..bd5e9ba8ee7 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -230,11 +230,31 @@ public class EmacsDialog implements DialogInterface.OnDismissListener AlertDialog dialog; if (EmacsActivity.focusedActivities.isEmpty ()) - return false; + { + /* If focusedActivities is empty then this dialog may have + been displayed immediately after a popup dialog is + dismissed. */ + + activity = EmacsActivity.lastFocusedActivity; + + if (activity == null) + return false; + } + else + activity = EmacsActivity.focusedActivities.get (0); - activity = EmacsActivity.focusedActivities.get (0); dialog = dismissDialog = toAlertDialog (activity); - dismissDialog.show (); + + try + { + dismissDialog.show (); + } + catch (Exception exception) + { + /* This can happen when the system decides Emacs is not in the + foreground any longer. */ + return false; + } /* If there are less than four buttons, then they must be individually enabled or disabled after the dialog is -- cgit v1.2.1 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. --- 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 ++++++++- 5 files changed, 588 insertions(+), 156 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsEditable.java create mode 100644 java/org/gnu/emacs/EmacsInputConnection.java (limited to 'java/org/gnu') 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; + } }; -- cgit v1.2.1 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. --- 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 ++++- 7 files changed, 267 insertions(+), 468 deletions(-) delete mode 100644 java/org/gnu/emacs/EmacsEditable.java (limited to 'java/org/gnu') 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; + } }; -- cgit v1.2.1 From cf24b61985c26cbf2e5a24cb0b64a8528aa3a9cc Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 15 Feb 2023 22:51:44 +0800 Subject: Update Android port * doc/emacs/input.texi (On-Screen Keyboards): * doc/lispref/commands.texi (Misc Events): Improve documentation of text conversion stuff. * java/org/gnu/emacs/EmacsInputConnection.java (beginBatchEdit) (endBatchEdit, commitCompletion, commitText, deleteSurroundingText) (finishComposingText, getSelectedText, getTextAfterCursor) (EmacsInputConnection, setComposingRegion, performEditorAction) (getExtractedText): Condition debug code on DEBUG_IC. * java/org/gnu/emacs/EmacsService.java (EmacsService, updateIC): Likewise. * lisp/bindings.el (global-map): * lisp/electric.el (global-map): Make `text-conversion' `analyze-text-conversion'. * lisp/progmodes/prog-mode.el (prog-mode): Enable text conversion in input methods. * lisp/simple.el (analyze-text-conversion): New function. * lisp/textmodes/text-mode.el (text-conversion-style) (text-mode): Likewise. * src/androidterm.c (android_handle_ime_event): Handle set_point_and_mark. (android_sync_edit): Give Emacs 100 ms instead. (android_perform_conversion_query): Skip the active region, not the conversion region. (getSelectedText): Implement properly. (android_update_selection): Expose mark to input methods. (android_reset_conversion): Handle `text-conversion-style'. * src/buffer.c (init_buffer_once, syms_of_buffer): Add buffer local variable `text-conversion-style'. * src/buffer.h (struct buffer, bset_text_conversion_style): New fields. * src/emacs.c (android_emacs_init): Call syms_of_textconv. * src/frame.h (enum text_conversion_operation): Rename TEXTCONV_SET_POINT. * src/lisp.h: Export syms_of_textconv. * src/marker.c (set_marker_internal): Force redisplay when the mark is set and the buffer is visible on builds that use text conversion. Explain why. * src/textconv.c (copy_buffer): Fix copying past gap. (get_mark): New function. (textconv_query): Implement new flag. (sync_overlay): New function. Display conversion text in an overlay. (record_buffer_change, really_commit_text) (really_set_composing_text, really_set_composing_region) (really_delete_surrounding_text, really_set_point) (handle_pending_conversion_events_1, decrement_inside) (handle_pending_conversion_events, textconv_set_point) (get_extracted_text, register_textconv_interface): Various fixes and improvements. * src/textconv.h (struct textconv_interface): Update documentation. * src/window.h (GCALIGNED_STRUCT): New field `prev_mark'. * src/xdisp.c (mark_window_display_accurate_1): Handle prev_mark. --- java/org/gnu/emacs/EmacsInputConnection.java | 68 ++++++++++++++++++++-------- java/org/gnu/emacs/EmacsService.java | 14 ++++-- 2 files changed, 59 insertions(+), 23 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 3cf4419838b..5eb56d5aa71 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -55,7 +55,9 @@ public class EmacsInputConnection extends BaseInputConnection public boolean beginBatchEdit () { - Log.d (TAG, "beginBatchEdit"); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "beginBatchEdit"); + EmacsNative.beginBatchEdit (windowHandle); return true; } @@ -64,7 +66,9 @@ public class EmacsInputConnection extends BaseInputConnection public boolean endBatchEdit () { - Log.d (TAG, "endBatchEdit"); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "endBatchEdit"); + EmacsNative.endBatchEdit (windowHandle); return true; } @@ -73,7 +77,9 @@ public class EmacsInputConnection extends BaseInputConnection public boolean commitCompletion (CompletionInfo info) { - Log.d (TAG, "commitCompletion: " + info); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "commitCompletion: " + info); + EmacsNative.commitCompletion (windowHandle, info.getText ().toString (), info.getPosition ()); @@ -84,7 +90,9 @@ public class EmacsInputConnection extends BaseInputConnection public boolean commitText (CharSequence text, int newCursorPosition) { - Log.d (TAG, "commitText: " + text + " " + newCursorPosition); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "commitText: " + text + " " + newCursorPosition); + EmacsNative.commitText (windowHandle, text.toString (), newCursorPosition); return true; @@ -94,8 +102,10 @@ public class EmacsInputConnection extends BaseInputConnection public boolean deleteSurroundingText (int leftLength, int rightLength) { - Log.d (TAG, ("deleteSurroundingText: " - + leftLength + " " + rightLength)); + if (EmacsService.DEBUG_IC) + Log.d (TAG, ("deleteSurroundingText: " + + leftLength + " " + rightLength)); + EmacsNative.deleteSurroundingText (windowHandle, leftLength, rightLength); return true; @@ -105,7 +115,8 @@ public class EmacsInputConnection extends BaseInputConnection public boolean finishComposingText () { - Log.d (TAG, "finishComposingText"); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "finishComposingText"); EmacsNative.finishComposingText (windowHandle); return true; @@ -115,7 +126,8 @@ public class EmacsInputConnection extends BaseInputConnection public String getSelectedText (int flags) { - Log.d (TAG, "getSelectedText: " + flags); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getSelectedText: " + flags); return EmacsNative.getSelectedText (windowHandle, flags); } @@ -124,27 +136,44 @@ public class EmacsInputConnection extends BaseInputConnection public String getTextAfterCursor (int length, int flags) { - Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); + String string; + + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); - return EmacsNative.getTextAfterCursor (windowHandle, length, - flags); + string = EmacsNative.getTextAfterCursor (windowHandle, length, + flags); + + if (EmacsService.DEBUG_IC) + Log.d (TAG, " --> " + string); + + return string; } @Override public String getTextBeforeCursor (int length, int flags) { - Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); + String string; + + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); + + string = EmacsNative.getTextBeforeCursor (windowHandle, length, + flags); + + if (EmacsService.DEBUG_IC) + Log.d (TAG, " --> " + string); - return EmacsNative.getTextBeforeCursor (windowHandle, length, - flags); + return string; } @Override public boolean setComposingText (CharSequence text, int newCursorPosition) { - Log.d (TAG, "setComposingText: " + newCursorPosition); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "setComposingText: " + newCursorPosition); EmacsNative.setComposingText (windowHandle, text.toString (), newCursorPosition); @@ -155,7 +184,8 @@ public class EmacsInputConnection extends BaseInputConnection public boolean setComposingRegion (int start, int end) { - Log.d (TAG, "setComposingRegion: " + start + " " + end); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "setComposingRegion: " + start + " " + end); EmacsNative.setComposingRegion (windowHandle, start, end); return true; @@ -165,7 +195,8 @@ public class EmacsInputConnection extends BaseInputConnection public boolean performEditorAction (int editorAction) { - Log.d (TAG, "performEditorAction: " + editorAction); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "performEditorAction: " + editorAction); EmacsNative.performEditorAction (windowHandle, editorAction); return true; @@ -175,7 +206,8 @@ public class EmacsInputConnection extends BaseInputConnection public ExtractedText getExtractedText (ExtractedTextRequest request, int flags) { - Log.d (TAG, "getExtractedText: " + request + " " + flags); + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getExtractedText: " + request + " " + flags); return EmacsNative.getExtractedText (windowHandle, request, flags); diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 855a738a30f..2acb3ead086 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -88,6 +88,8 @@ public class EmacsService extends Service /* Display metrics used by font backends. */ public DisplayMetrics metrics; + public static final boolean DEBUG_IC = false; + @Override public int onStartCommand (Intent intent, int flags, int startId) @@ -612,10 +614,11 @@ public class EmacsService extends Service int newSelectionEnd, int composingRegionStart, int composingRegionEnd) { - Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart - + " " + newSelectionEnd + " " - + composingRegionStart + " " - + composingRegionEnd)); + if (DEBUG_IC) + Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart + + " " + newSelectionEnd + " " + + composingRegionStart + " " + + composingRegionEnd)); window.view.imManager.updateSelection (window.view, newSelectionStart, newSelectionEnd, @@ -626,7 +629,8 @@ public class EmacsService extends Service public void resetIC (EmacsWindow window, int icMode) { - Log.d (TAG, "resetIC: " + window); + if (DEBUG_IC) + Log.d (TAG, "resetIC: " + window); window.view.setICMode (icMode); window.view.imManager.restartInput (window.view); -- cgit v1.2.1 From 2dcce30290dc7782e9de3b4adf59f38b42408c98 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 16 Feb 2023 23:57:01 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Fonts): * doc/emacs/input.texi (On-Screen Keyboards): * doc/lispref/commands.texi (Misc Events): Update documentation. * java/org/gnu/emacs/EmacsInputConnection.java (setSelection): New function. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView) (reconfigureFrontBuffer): Make bitmap references weak references. * java/org/gnu/emacs/EmacsView.java (handleDirtyBitmap): Don't clear surfaceView bitmap. * lisp/comint.el (comint-mode): * lisp/international/fontset.el (script-representative-chars) (setup-default-fontset): Improve detection of CJK fonts. * lisp/isearch.el (set-text-conversion-style): New variable. (isearch-mode, isearch-done): Save and restore the text conversion style. * lisp/minibuffer.el (minibuffer-mode): Set an appropriate text conversion style. * lisp/simple.el (analyze-text-conversion): Run post-self-insert-hook properly. * lisp/subr.el (read-char-from-minibuffer): Disable text conversion when reading character. * src/androidterm.c (show_back_buffer): Don't check that F is not garbaged. (android_update_selection, android_reset_conversion): Use the ephemeral last point and handle text conversion being disabled. * src/buffer.c (syms_of_buffer): Convert old style DEFVAR. * src/keyboard.c (kbd_buffer_get_event): Handle text conversion first. * src/lisp.h: Update prototypes. * src/lread.c (read_filtered_event): Temporarily disable text conversion. * src/sfnt.c (sfnt_decompose_glyph_1, sfnt_decompose_glyph_2): New functions. (sfnt_decompose_glyph, sfnt_decompose_instructed_outline): Refactor contour decomposition to those two functions. (main): Update tests. * src/sfntfont-android.c (system_font_directories): Add empty field. (Fandroid_enumerate_fonts, init_sfntfont_android): Enumerate fonts in a user fonts directory. * src/sfntfont.c (struct sfnt_font_desc): New field `num_glyphs'. (sfnt_enum_font_1): Set num_glyphs and avoid duplicate fonts. (sfntfont_glyph_valid): New function. (sfntfont_lookup_char, sfntfont_list_1): Make sure glyphs found are valid. * src/textconv.c (sync_overlay, really_commit_text) (really_set_composing_text, really_set_composing_region) (really_delete_surrounding_text, really_set_point_and_mark) (handle_pending_conversion_events_1) (handle_pending_conversion_events, conversion_disabled_p) (disable_text_conversion, resume_text_conversion) (Fset_text_conversion_style, syms_of_textconv): Update to respect new options. * src/textconv.h: * src/window.h (GCALIGNED_STRUCT): New field `ephemeral_last_point'. * src/xdisp.c (mark_window_display_accurate_1): Set it. --- java/org/gnu/emacs/EmacsInputConnection.java | 14 +++++++++++++- java/org/gnu/emacs/EmacsSurfaceView.java | 17 ++++++++++------- java/org/gnu/emacs/EmacsView.java | 8 +------- 3 files changed, 24 insertions(+), 15 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 5eb56d5aa71..e2a15894695 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -173,7 +173,8 @@ public class EmacsInputConnection extends BaseInputConnection setComposingText (CharSequence text, int newCursorPosition) { if (EmacsService.DEBUG_IC) - Log.d (TAG, "setComposingText: " + newCursorPosition); + Log.d (TAG, ("setComposingText: " + + text + " ## " + newCursorPosition)); EmacsNative.setComposingText (windowHandle, text.toString (), newCursorPosition); @@ -213,6 +214,17 @@ public class EmacsInputConnection extends BaseInputConnection flags); } + @Override + public boolean + setSelection (int start, int end) + { + if (EmacsService.DEBUG_IC) + Log.d (TAG, "setSelection: " + start + " " + end); + + EmacsNative.setSelection (windowHandle, start, end); + return true; + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 2d80be0881a..62e927094e4 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -28,6 +28,8 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Paint; +import java.lang.ref.WeakReference; + /* This originally extended SurfaceView. However, doing so proved to be too slow, and Android's surface view keeps up to three of its own back buffers, which use too much memory (up to 96 MB for a @@ -39,7 +41,7 @@ public class EmacsSurfaceView extends View private EmacsView view; private Bitmap frontBuffer; private Canvas bitmapCanvas; - private Bitmap bitmap; + private WeakReference bitmap; private Paint bitmapPaint; public @@ -49,10 +51,11 @@ public class EmacsSurfaceView extends View this.view = view; this.bitmapPaint = new Paint (); + this.bitmap = new WeakReference (null); } private void - copyToFrontBuffer (Rect damageRect) + copyToFrontBuffer (Bitmap bitmap, Rect damageRect) { if (damageRect != null) bitmapCanvas.drawBitmap (bitmap, damageRect, damageRect, @@ -73,7 +76,7 @@ public class EmacsSurfaceView extends View bitmapCanvas = null; } - this.bitmap = bitmap; + this.bitmap = new WeakReference (bitmap); /* Next, create the new front buffer if necessary. */ @@ -92,20 +95,20 @@ public class EmacsSurfaceView extends View bitmapCanvas = new Canvas (frontBuffer); /* And copy over the bitmap contents. */ - copyToFrontBuffer (null); + copyToFrontBuffer (bitmap, null); } else if (bitmap != null) /* Just copy over the bitmap contents. */ - copyToFrontBuffer (null); + copyToFrontBuffer (bitmap, null); } public synchronized void setBitmap (Bitmap bitmap, Rect damageRect) { - if (bitmap != this.bitmap) + if (bitmap != this.bitmap.get ()) reconfigureFrontBuffer (bitmap); else if (bitmap != null) - copyToFrontBuffer (damageRect); + copyToFrontBuffer (bitmap, damageRect); if (bitmap != null) { diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 52da6d41f7d..5ea8b0dcc0e 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -186,13 +186,7 @@ public class EmacsView extends ViewGroup /* Explicitly free the old bitmap's memory. */ if (oldBitmap != null) - { - oldBitmap.recycle (); - - /* Make sure to set the view's bitmap to the new bitmap, or - ugly flicker can result. */ - surfaceView.setBitmap (bitmap, null); - } + oldBitmap.recycle (); /* Some Android versions still don't free the bitmap until the next GC. */ -- cgit v1.2.1 From 88afd96e36e62017c9c1f2229e2748b6dfbdb39a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 17 Feb 2023 16:27:00 +0800 Subject: Fix build and running on Android 2.2 * INSTALL.android: Document that Android 2.2 is now supported, with caveats. * configure.ac (ANDROID_MIN_SDK, ANDROID_SDK_18_OR_EARLIER) (SYSTEM_TYPE, ANDROID_STUBIFY, SIZEOF_LONG): Correctly detect things missing on Android 2.2. * java/Makefile.in (ANDROID_JAR, JARSIGNER_FLAGS): * java/debug.sh (jdb, gdbserver, line): * java/org/gnu/emacs/EmacsApplication.java (findDumpFile): * java/org/gnu/emacs/EmacsService.java (onCreate): * java/org/gnu/emacs/EmacsThread.java (EmacsThread, run): Run parameter initialization on main thread. * src/android-asset.h (struct android_asset_manager) (struct android_asset, AAssetManager_fromJava, AAssetManager_open) (AAsset_close, android_asset_create_stream) (android_asset_read_internal, AAsset_openFileDescriptor) (AAsset_getLength, AAsset_getBuffer, AAsset_read): New file. * src/android.c (android_user_full_name, android_hack_asset_fd) (android_check_compressed_file): Implement for Android 2.2. * src/process.c (Fprocess_send_eof): Don't call tcdrain if unavailable. * src/sfntfont-android.c (system_font_directories): Fix compiler warning. * src/sfntfont.c (sfntfont_read_cmap): Correctly test rc of emacs_open. * src/textconv.c (handle_pending_conversion_events_1): Mark buffer UNINIT. --- java/org/gnu/emacs/EmacsApplication.java | 1 + java/org/gnu/emacs/EmacsService.java | 25 +++++++++++++++---------- java/org/gnu/emacs/EmacsThread.java | 9 ++++++++- 3 files changed, 24 insertions(+), 11 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index 96328b99d1c..6a065165eb1 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -49,6 +49,7 @@ public class EmacsApplication extends Application for a file named ``emacs-.pdmp'' and delete the rest. */ filesDirectory = context.getFilesDir (); + allFiles = filesDirectory.listFiles (new FileFilter () { @Override public boolean diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 2acb3ead086..4d373937ab0 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -180,11 +180,11 @@ public class EmacsService extends Service public void onCreate () { - AssetManager manager; + final AssetManager manager; Context app_context; - String filesDir, libDir, cacheDir, classPath; - double pixelDensityX; - double pixelDensityY; + final String filesDir, libDir, cacheDir, classPath; + final double pixelDensityX; + final double pixelDensityY; SERVICE = this; handler = new Handler (Looper.getMainLooper ()); @@ -210,13 +210,18 @@ public class EmacsService extends Service Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir + ", libDir = " + libDir + ", and classPath = " + classPath); - EmacsNative.setEmacsParams (manager, filesDir, libDir, - cacheDir, (float) pixelDensityX, - (float) pixelDensityY, - classPath, this); - /* Start the thread that runs Emacs. */ - thread = new EmacsThread (this, needDashQ); + thread = new EmacsThread (this, new Runnable () { + @Override + public void + run () + { + EmacsNative.setEmacsParams (manager, filesDir, libDir, + cacheDir, (float) pixelDensityX, + (float) pixelDensityY, + classPath, EmacsService.this); + } + }, needDashQ); thread.start (); } catch (IOException exception) diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 2724d838d41..30484710651 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -28,11 +28,16 @@ public class EmacsThread extends Thread /* Whether or not Emacs should be started -Q. */ private boolean startDashQ; + /* Runnable run to initialize Emacs. */ + private Runnable paramsClosure; + public - EmacsThread (EmacsService service, boolean startDashQ) + EmacsThread (EmacsService service, Runnable paramsClosure, + boolean startDashQ) { super ("Emacs main thread"); this.startDashQ = startDashQ; + this.paramsClosure = paramsClosure; } @Override @@ -46,6 +51,8 @@ public class EmacsThread extends Thread else args = new String[] { "libandroid-emacs.so", "-Q", }; + paramsClosure.run (); + /* Run the native code now. */ EmacsNative.initEmacs (args, EmacsApplication.dumpFileName, Build.VERSION.SDK_INT); -- cgit v1.2.1 From 60ed861b10fdc34bb9c888a59c6e6f75a27b77b4 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 17 Feb 2023 19:02:23 +0800 Subject: Fix crash on old versions of Android * java/org/gnu/emacs/EmacsService.java (nameKeysym): Implement stub on Android 3.0 and earlier. --- java/org/gnu/emacs/EmacsService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 4d373937ab0..ba6ec485d62 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -470,7 +470,10 @@ public class EmacsService extends Service public String nameKeysym (int keysym) { - return KeyEvent.keyCodeToString (keysym); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) + return KeyEvent.keyCodeToString (keysym); + + return String.valueOf (keysym); } public void -- cgit v1.2.1 From a61f9cb77ced0607e4607bc5d82ae06165e6138d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 18 Feb 2023 23:09:30 +0800 Subject: Update Android port * doc/emacs/input.texi (On-Screen Keyboards): Document `touch-screen-always-display'. * doc/lispref/commands.texi (Misc Events): Improve documentation of text conversion events. * java/org/gnu/emacs/EmacsDialog.java (toAlertDialog, display1): Reorder buttons to make more sense. * lisp/elec-pair.el (electric-pair-analyze-conversion): New function. * lisp/simple.el (analyze-text-conversion): Improve integration with electric pair modes. * lisp/term.el (term-mode): Always display the onscreen keyboard. * lisp/touch-screen.el (touch-screen-display-keyboard) (touch-screen-handle-point-up): Respect new options. * src/textconv.c (really_set_composing_text): Stop widenining unnecessarily. (really_delete_surrounding_text): Really delete surrounding text. Give text conversion analyzers the buffer text. (syms_of_textconv): Update doc string. --- java/org/gnu/emacs/EmacsDialog.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index bc0333e99b9..9f9124ce99c 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -166,14 +166,14 @@ public class EmacsDialog implements DialogInterface.OnDismissListener if (size >= 2) { button = buttons.get (1); - dialog.setButton (DialogInterface.BUTTON_NEUTRAL, + dialog.setButton (DialogInterface.BUTTON_NEGATIVE, button.name, button); } if (size >= 3) { button = buttons.get (2); - dialog.setButton (DialogInterface.BUTTON_NEGATIVE, + dialog.setButton (DialogInterface.BUTTON_NEUTRAL, button.name, button); } } @@ -274,10 +274,8 @@ public class EmacsDialog implements DialogInterface.OnDismissListener if (size >= 2) { button = buttons.get (1); - dialog.setButton (DialogInterface.BUTTON_NEUTRAL, - button.name, button); buttonView - = dialog.getButton (DialogInterface.BUTTON_NEUTRAL); + = dialog.getButton (DialogInterface.BUTTON_NEGATIVE); buttonView.setEnabled (button.enabled); } @@ -285,7 +283,7 @@ public class EmacsDialog implements DialogInterface.OnDismissListener { button = buttons.get (2); buttonView - = dialog.getButton (DialogInterface.BUTTON_NEGATIVE); + = dialog.getButton (DialogInterface.BUTTON_NEUTRAL); buttonView.setEnabled (button.enabled); } } -- cgit v1.2.1 From c8f49c9276d34741bfbe7752dd38391c0b8d782b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 19 Feb 2023 13:17:43 +0800 Subject: Implement `fullscreen' on Android 4.0 and later * doc/emacs/android.texi (Android Windowing): Document what new frame parameters are now supported. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): New field `isFullscreen'. (detachWindow, attachWindow): Sync fullscreen state. (onWindowFocusChanged): Add more logging. (onResume): Restore previous fullscreen state. (syncFullscreen): New function. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow) (setFullscreen): New function. * src/android.c (struct android_emacs_window): Add new method. (android_init_emacs_window): Look up new method. (android_set_fullscreen): New function. * src/androidgui.h: * src/androidterm.c (android_fullscreen_hook): Implement accordingly. --- java/org/gnu/emacs/EmacsActivity.java | 107 +++++++++++++++++++++++++++++++++- java/org/gnu/emacs/EmacsWindow.java | 26 +++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 3156a144a0f..7e09e608984 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -26,12 +26,16 @@ import java.util.ArrayList; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.os.Build; +import android.os.Bundle; import android.util.Log; -import android.widget.FrameLayout; -import android.widget.FrameLayout.LayoutParams; import android.view.Menu; +import android.view.View; +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 implements EmacsWindowAttachmentManager.WindowConsumer @@ -56,6 +60,9 @@ public class EmacsActivity extends Activity /* Whether or not this activity is paused. */ private boolean isPaused; + /* Whether or not this activity is fullscreen. */ + private boolean isFullscreen; + static { focusedActivities = new ArrayList (); @@ -104,6 +111,8 @@ public class EmacsActivity extends Activity public void detachWindow () { + syncFullscreenWith (null); + if (window == null) Log.w (TAG, "detachWindow called, but there is no window"); else @@ -131,6 +140,8 @@ public class EmacsActivity extends Activity throw new IllegalStateException ("trying to attach window when one" + " already exists"); + syncFullscreenWith (child); + /* Record and attach the view. */ window = child; @@ -230,6 +241,9 @@ public class EmacsActivity extends Activity public void onWindowFocusChanged (boolean isFocused) { + Log.d (TAG, ("onWindowFocusChanged: " + + (isFocused ? "YES" : "NO"))); + if (isFocused && !focusedActivities.contains (this)) { focusedActivities.add (this); @@ -257,6 +271,9 @@ public class EmacsActivity extends Activity { isPaused = false; + /* Update the window insets. */ + syncFullscreenWith (window); + EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this); super.onResume (); } @@ -279,4 +296,88 @@ public class EmacsActivity extends Activity super.onContextMenuClosed (menu); } + + @SuppressWarnings ("deprecation") + public void + syncFullscreenWith (EmacsWindow emacsWindow) + { + WindowInsetsController controller; + Window window; + int behavior, flags; + View view; + + if (emacsWindow != null) + isFullscreen = emacsWindow.fullscreen; + else + isFullscreen = false; + + /* On Android 11 or later, use the window insets controller to + control whether or not the view is fullscreen. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + { + window = getWindow (); + + /* If there is no attached window, return immediately. */ + if (window == null) + return; + + behavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; + controller = window.getInsetsController (); + controller.setSystemBarsBehavior (behavior); + + if (isFullscreen) + controller.hide (WindowInsets.Type.statusBars () + | WindowInsets.Type.navigationBars ()); + else + controller.show (WindowInsets.Type.statusBars () + | WindowInsets.Type.navigationBars ()); + + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) + { + /* On Android 4.1 or later, use `setSystemUiVisibility'. */ + + window = getWindow (); + + if (window == null) + return; + + view = window.getDecorView (); + + if (isFullscreen) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) + /* This flag means that Emacs will be full screen, but + the system will cancel the full screen state upon + switching to another program. */ + view.setSystemUiVisibility (View.SYSTEM_UI_FLAG_FULLSCREEN); + else + { + /* These flags means that Emacs will be full screen as + long as the state flag is set. */ + flags = 0; + flags |= View.SYSTEM_UI_FLAG_FULLSCREEN; + flags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + flags |= View.SYSTEM_UI_FLAG_IMMERSIVE; + flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + view.setSystemUiVisibility (flags); + } + } + else + view.setSystemUiVisibility (View.SYSTEM_UI_FLAG_VISIBLE); + } + } + + @Override + public void + onAttachedToWindow () + { + super.onAttachedToWindow (); + + /* Update the window insets. */ + syncFullscreenWith (window); + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 0eca35cec61..90fc4c44198 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -128,6 +128,9 @@ public class EmacsWindow extends EmacsHandleObject events. */ public LinkedHashMap eventStrings; + /* Whether or not this window is fullscreen. */ + public boolean fullscreen; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height, boolean overrideRedirect) @@ -1179,4 +1182,27 @@ public class EmacsWindow extends EmacsHandleObject return any; } + + public void + setFullscreen (final boolean isFullscreen) + { + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + EmacsActivity activity; + Object tem; + + fullscreen = isFullscreen; + tem = getAttachedConsumer (); + + if (tem != null) + { + activity = (EmacsActivity) getAttachedConsumer (); + activity.syncFullscreenWith (EmacsWindow.this); + } + } + }); + } }; -- cgit v1.2.1 From efc46330aa1e3c433246b8a008ffc7f2675369c2 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 19 Feb 2023 15:29:46 +0800 Subject: Allow opening more files in emacsclient on Android * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity) (checkReadableOrCopy): New function. (onCreate): If the file specified is not readable from C, read it into a temporary file and ask Emacs to open that. --- java/org/gnu/emacs/EmacsOpenActivity.java | 64 ++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index baf31039ecd..c8501d91025 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -58,8 +58,10 @@ import android.os.Bundle; import android.os.ParcelFileDescriptor; import java.io.File; -import java.io.FileReader; +import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; @@ -176,6 +178,50 @@ 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. */ + + private String + checkReadableOrCopy (String file, ParcelFileDescriptor fd) + throws IOException, FileNotFoundException + { + File inFile; + FileOutputStream outStream; + InputStream stream; + byte buffer[]; + int read; + + inFile = new File (file); + + if (inFile.setReadable (true)) + return file; + + /* inFile is now the file being written to. */ + inFile = new File (getCacheDir (), inFile.getName ()); + buffer = new byte[4098]; + outStream = new FileOutputStream (inFile); + stream = new FileInputStream (fd.getFileDescriptor ()); + + try + { + while ((read = stream.read (buffer)) >= 0) + outStream.write (buffer, 0, read); + } + finally + { + /* Note that this does not close FD. + + Keep in mind that execution is transferred to ``finally'' + even if an exception happens inside the while loop + above. */ + stream.close (); + outStream.close (); + } + + return inFile.getCanonicalPath (); + } + /* Finish this activity in response to emacsclient having successfully opened a file. @@ -340,17 +386,19 @@ public class EmacsOpenActivity extends Activity opening the file and doing readlink on its file descriptor in /proc/self/fd. */ resolver = getContentResolver (); + fd = null; 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"); + + fileName = checkReadableOrCopy (fileName, fd); } catch (FileNotFoundException exception) { @@ -360,6 +408,18 @@ public class EmacsOpenActivity extends Activity { /* Do nothing. */ } + + if (fd != null) + { + try + { + fd.close (); + } + catch (IOException exception) + { + /* Do nothing. */ + } + } } if (fileName == null) -- cgit v1.2.1 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 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'java/org/gnu') 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. */ -- cgit v1.2.1 From 57c19f477fddb542fa40747aeb7060fa9756756f Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 20 Feb 2023 22:14:29 +0800 Subject: Update Android port * INSTALL.android: Explain where to get tree-sitter. * configure.ac: Add support for dynamic modules and tree-sitter. * doc/emacs/android.texi (Android Windowing): * java/org/gnu/emacs/EmacsSdk11Clipboard.java (EmacsSdk11Clipboard, ownsClipboard): Improve clipboard handling and documentation. --- java/org/gnu/emacs/EmacsSdk11Clipboard.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java index 2df2015c9c1..ea35a463299 100644 --- a/java/org/gnu/emacs/EmacsSdk11Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java @@ -25,6 +25,8 @@ import android.content.ClipData; import android.util.Log; +import android.os.Build; + import java.io.UnsupportedEncodingException; /* This class implements EmacsClipboard for Android 3.0 and later @@ -43,7 +45,12 @@ public class EmacsSdk11Clipboard extends EmacsClipboard EmacsSdk11Clipboard () { manager = EmacsService.SERVICE.getClipboardManager (); - manager.addPrimaryClipChangedListener (this); + + /* The system forbids Emacs from reading clipboard data in the + background under Android 10 or later. */ + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) + manager.addPrimaryClipChangedListener (this); } @Override @@ -105,6 +112,9 @@ public class EmacsSdk11Clipboard extends EmacsClipboard public synchronized int ownsClipboard () { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + return -1; + return ownsClipboard ? 1 : 0; } -- cgit v1.2.1 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. --- java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsOpenActivity.java | 42 +++++++++-- java/org/gnu/emacs/EmacsService.java | 119 ++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 6 deletions(-) (limited to 'java/org/gnu') 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; + } + } }; -- cgit v1.2.1 From 77feba74564c4d58b472b82fec0137091bb5c7e1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 21 Feb 2023 21:07:57 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu) (addSubmenu, inflateMenuItems): Handle tooltips correctly. * src/android.c (android_scan_directory_tree): Fix limit generation for root directory. * src/androidmenu.c (android_init_emacs_context_menu) (android_menu_show): Implement menu item help text on Android 8.0 and later. --- java/org/gnu/emacs/EmacsContextMenu.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 184c611bf9d..6b3ae0c6de9 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.os.Build; import android.view.Menu; import android.view.MenuItem; @@ -54,7 +55,7 @@ public class EmacsContextMenu private class Item implements MenuItem.OnMenuItemClickListener { public int itemID; - public String itemName; + public String itemName, tooltip; public EmacsContextMenu subMenu; public boolean isEnabled, isCheckable, isChecked; @@ -147,7 +148,7 @@ public class EmacsContextMenu item name. */ public EmacsContextMenu - addSubmenu (String itemName, String title) + addSubmenu (String itemName, String title, String tooltip) { EmacsContextMenu submenu; Item item; @@ -155,6 +156,7 @@ public class EmacsContextMenu item = new Item (); item.itemID = 0; item.itemName = itemName; + item.tooltip = tooltip; item.subMenu = createContextMenu (title); item.subMenu.parent = this; @@ -214,6 +216,13 @@ public class EmacsContextMenu if (item.isChecked) menuItem.setChecked (true); + + /* If the tooltip text is set and the system is new enough + to support menu item tooltips, set it on the item. */ + + if (item.tooltip != null + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + menuItem.setTooltipText (item.tooltip); } } } -- cgit v1.2.1 From 1e6f957c0dbb7e4a5e04c20fcb797be1d98df3d8 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 22 Feb 2023 14:59:27 +0800 Subject: Update Android port * doc/emacs/input.texi (On-Screen Keyboards): Document changes to text conversion. * java/org/gnu/emacs/EmacsInputConnection.java (getExtractedText) (EmacsInputConnection): * src/keyboard.c (read_key_sequence): Disable text conversion after reading prefix key. * src/textconv.c (get_extracted_text): Fix returned value when request length is zero. --- java/org/gnu/emacs/EmacsInputConnection.java | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index e2a15894695..834c2226c82 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -207,11 +207,19 @@ public class EmacsInputConnection extends BaseInputConnection public ExtractedText getExtractedText (ExtractedTextRequest request, int flags) { + ExtractedText text; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + request + " " + flags); - return EmacsNative.getExtractedText (windowHandle, request, + text = EmacsNative.getExtractedText (windowHandle, request, flags); + + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getExtractedText: " + text.text + " @" + + text.startOffset + ":" + text.selectionStart); + + return text; } @Override @@ -225,6 +233,16 @@ public class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + sendKeyEvent (KeyEvent key) + { + if (EmacsService.DEBUG_IC) + Log.d (TAG, "sendKeyEvent: " + key); + + return super.sendKeyEvent (key); + } + /* Override functions which are not implemented. */ -- cgit v1.2.1 From ea74f3c0678d0729a8d6307e35c2d228f665afa3 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 24 Feb 2023 22:25:48 +0800 Subject: Improve Android configury * configure.ac (JAVA_PUSH_LINT): New macro. (JAVAFLAGS): New variable. Check for various lint flags and macros and enable them. * java/Makefile.in (ANDROID_ABI): * java/org/gnu/emacs/EmacsSdk7FontDriver.java: Remove compiler warning. --- java/org/gnu/emacs/EmacsSdk7FontDriver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index a964cadb74c..ba92d4cef49 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -325,7 +325,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver list.add (new Sdk7FontEntity (typefaceList[i])); } - return (FontEntity[]) list.toArray (new FontEntity[0]); + return list.toArray (new FontEntity[0]); } @Override -- cgit v1.2.1 From 8e4c5db193dc7baee5846520fe8b63d8bea99148 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 25 Feb 2023 19:11:07 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Startup, Android File System) (Android Environment, Android Windowing, Android Troubleshooting): Improve documentation; fix typos. * doc/lispref/commands.texi (Misc Events): Likewise. * java/org/gnu/emacs/EmacsService.java (queryBattery): New function. * lisp/battery.el (battery-status-function): Set appropriately for Android. (battery-android): New function. * src/android.c (struct android_emacs_service): New method `query_battery'. (android_check_content_access): Improve exception checking. (android_init_emacs_service): Look up new method. (android_destroy_handle, android_create_window) (android_init_android_rect_class, android_init_emacs_gc_class) (android_set_clip_rectangles) (android_create_pixmap_from_bitmap_data, android_fill_polygon) (android_get_image, android_put_image, android_bell) (android_set_input_focus, android_raise_window) (android_lower_window, android_query_tree, android_get_geometry) (android_translate_coordinates, android_wc_lookup_string) (android_damage_window, android_build_string) (android_build_jstring, android_exception_check_1) (android_exception_check_2): New functions. (android_browse_url): Improve exception handling. Always use android_exception_check and don't leak local refs. (android_query_battery): New function. * src/android.h (struct android_battery_state): New struct. * src/androidfns.c (Fandroid_query_battery, syms_of_androidfns): New function. * src/androidfont.c (androidfont_from_lisp, DO_SYMBOL_FIELD) (DO_CARDINAL_FIELD, androidfont_list, androidfont_match) (androidfont_draw, androidfont_open_font) (androidfont_close_font): * src/androidselect.c (Fandroid_set_clipboard) (Fandroid_get_clipboard): * src/sfnt.c (sfnt_map_glyf_table): * src/sfntfont.c (sfntfont_free_outline_cache) (sfntfont_free_raster_cache, sfntfont_close): Allow font close functions to be called twice. --- java/org/gnu/emacs/EmacsService.java | 57 +++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index c9701ff2990..48c7c743014 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -54,9 +54,9 @@ import android.content.res.AssetManager; import android.database.Cursor; import android.database.MatrixCursor; - import android.net.Uri; +import android.os.BatteryManager; import android.os.Build; import android.os.Looper; import android.os.IBinder; @@ -762,4 +762,59 @@ public class EmacsService extends Service return false; } } + + /* Return the status of the battery. See struct + android_battery_status for the order of the elements + returned. + + Value may be null upon failure. */ + + public long[] + queryBattery () + { + Object tem; + BatteryManager manager; + long capacity, chargeCounter, currentAvg, currentNow; + long status, remaining; + int prop; + + /* Android 4.4 or earlier require applications to listen to + changes to the battery instead of querying for its status. */ + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return null; + + tem = getSystemService (Context.BATTERY_SERVICE); + manager = (BatteryManager) tem; + remaining = -1; + + prop = BatteryManager.BATTERY_PROPERTY_CAPACITY; + capacity = manager.getLongProperty (prop); + prop = BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER; + chargeCounter = manager.getLongProperty (prop); + prop = BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE; + currentAvg = manager.getLongProperty (prop); + prop = BatteryManager.BATTERY_PROPERTY_CURRENT_NOW; + currentNow = manager.getLongProperty (prop); + + /* Return the battery status. N.B. that Android 7.1 and earlier + only return ``charging'' or ``discharging''. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + status = manager.getIntProperty (BatteryManager.BATTERY_PROPERTY_STATUS); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + status = (manager.isCharging () + ? BatteryManager.BATTERY_STATUS_CHARGING + : BatteryManager.BATTERY_STATUS_DISCHARGING); + else + status = (currentNow > 0 + ? BatteryManager.BATTERY_STATUS_CHARGING + : BatteryManager.BATTERY_STATUS_DISCHARGING); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + remaining = manager.computeChargeTimeRemaining (); + + return new long[] { capacity, chargeCounter, currentAvg, + currentNow, remaining, status, }; + } }; -- cgit v1.2.1 From df29bb71fc47b5e24fa971b668e4fa09dd6d244d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 25 Feb 2023 20:11:48 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsNoninteractive.java (main): Port to Android 2.2. * src/android-asset.h (AAsset_openFileDescriptor): Delete stub function. * src/android.c (android_check_compressed_file): Delete function. (android_open): Stop trying to find compressed files or to use the system provided file descriptor. Explain why. --- java/org/gnu/emacs/EmacsNoninteractive.java | 118 ++++++++++++++++++---------- 1 file changed, 76 insertions(+), 42 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index 4da82f2f894..30901edb75f 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -87,56 +87,23 @@ public class EmacsNoninteractive /* Create and attach the activity thread. */ activityThread = method.invoke (null); + context = 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_MR1) + try { - method - = activityThreadClass.getMethod ("getPackageInfo", - String.class, - int.class); - loadedApk = method.invoke (activityThread, "org.gnu.emacs", - 0); + loadedApkClass = Class.forName ("android.app.LoadedApk"); } - else + catch (ClassNotFoundException exception) { - 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"); + /* Android 2.2 has no LoadedApk class, but fortunately it + does not need to be used, since contexts can be + directly created. */ - /* Now, get a context. */ - contextImplClass = Class.forName ("android.app.ContextImpl"); + loadedApkClass = null; + contextImplClass = Class.forName ("android.app.ContextImpl"); - try - { - method = contextImplClass.getDeclaredMethod ("createAppContext", - activityThreadClass, - loadedApkClass); - method.setAccessible (true); - context = (Context) method.invoke (null, activityThread, loadedApk); - } - catch (NoSuchMethodException exception) - { - /* Older Android versions don't have createAppContext, but - instead require creating a ContextImpl, and then - calling createPackageContext. */ method = activityThreadClass.getDeclaredMethod ("getSystemContext"); context = (Context) method.invoke (activityThread); method = contextImplClass.getDeclaredMethod ("createPackageContext", @@ -147,6 +114,73 @@ public class EmacsNoninteractive 0); } + /* If the context has not already been created, then do what + is appropriate for newer versions of Android. */ + + if (context == null) + { + /* 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_MR1) + { + 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"); + + try + { + method + = contextImplClass.getDeclaredMethod ("createAppContext", + activityThreadClass, + loadedApkClass); + method.setAccessible (true); + context = (Context) method.invoke (null, activityThread, + loadedApk); + } + catch (NoSuchMethodException exception) + { + /* Older Android versions don't have createAppContext, but + instead require creating a ContextImpl, and then + calling createPackageContext. */ + method + = activityThreadClass.getDeclaredMethod ("getSystemContext"); + context = (Context) method.invoke (activityThread); + method + = contextImplClass.getDeclaredMethod ("createPackageContext", + String.class, + int.class); + method.setAccessible (true); + context = (Context) method.invoke (context, "org.gnu.emacs", + 0); + } + } + /* Don't actually start the looper or anything. Instead, obtain an AssetManager. */ assets = context.getAssets (); -- cgit v1.2.1 From 39ddf1855bbebc5d31b544e6308a4ae49f4925b3 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 26 Feb 2023 10:33:41 +0800 Subject: Update Android port * doc/lispref/commands.texi (Misc Events): Update documentation. * java/org/gnu/emacs/EmacsService.java (EmacsService) (onStartCommand): Improve notification message. * src/android.c (android_hack_asset_fd): Detect if ashmem is available dynamically. (android_detect_ashmem): New function. * src/textconv.c (record_buffer_change): Use markers to represent BEG and END instead. (syms_of_textconv): Update doc string. --- java/org/gnu/emacs/EmacsService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 48c7c743014..7f4f75b5147 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -101,6 +101,8 @@ public class EmacsService extends Service /* Display metrics used by font backends. */ public DisplayMetrics metrics; + /* Flag that says whether or not to print verbose debugging + information. */ public static final boolean DEBUG_IC = false; @Override @@ -117,8 +119,10 @@ public class EmacsService extends Service { tem = getSystemService (Context.NOTIFICATION_SERVICE); manager = (NotificationManager) tem; - infoBlurb = ("See (emacs)Android Environment for more" - + " details about this notification."); + infoBlurb = ("This notification is displayed to keep Emacs" + + " running while it is in the background. You" + + " may disable if you want;" + + " see (emacs)Android Environment."); channel = new NotificationChannel ("emacs", "Emacs persistent notification", NotificationManager.IMPORTANCE_DEFAULT); -- cgit v1.2.1 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/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 +- 26 files changed, 45 insertions(+), 36 deletions(-) (limited to 'java/org/gnu') 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.2.1 From 194b3f948cba9f6da0e5d3b36ada8ab9ca74d482 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 1 Mar 2023 14:31:57 +0800 Subject: Update Android port * java/AndroidManifest.xml.in: Specify @style/EmacsStyle. * java/org/gnu/emacs/EmacsActivity.java (onCreate): Stop setting the theme here. * java/res/values-v11/style.xml: * java/res/values-v14/style.xml: * java/res/values-v29/style.xml: * java/res/values/style.xml: Extract style resources into res/values. --- java/org/gnu/emacs/EmacsActivity.java | 7 ------- 1 file changed, 7 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 0ee8c239899..1c5d7605caa 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -187,13 +187,6 @@ public class EmacsActivity extends Activity = intent.getBooleanExtra ("org.gnu.emacs.START_DASH_Q", false); - /* 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); - params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); -- cgit v1.2.1 From ad8e12b9f9937f999a277e2608a1098a1c494532 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 1 Mar 2023 15:49:02 +0800 Subject: Update Android port * doc/emacs/android.texi (Android File System): Document new behavior of starting a subprocess from /assets. * java/org/gnu/emacs/EmacsWindow.java (onSomeKindOfMotionEvent): Don't use isFromSource where not present. * src/androidterm.c (android_scroll_run): Avoid undefined behavior writing to bitfields. * src/callproc.c (get_current_directory): When trying to run a subprocess inside /assets, run it from the home directory instead. --- java/org/gnu/emacs/EmacsWindow.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 007a2a86e68..5c481aa3ef4 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -875,7 +875,14 @@ public final class EmacsWindow extends EmacsHandleObject public boolean onSomeKindOfMotionEvent (MotionEvent event) { - if (!event.isFromSource (InputDevice.SOURCE_CLASS_POINTER)) + /* isFromSource is not available until API level 18. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) + { + if (!event.isFromSource (InputDevice.SOURCE_CLASS_POINTER)) + return false; + } + else if (event.getSource () != InputDevice.SOURCE_CLASS_POINTER) return false; switch (event.getAction ()) -- cgit v1.2.1 From 7fb3c0d0397096f643f6239d50cf52eaf96e7b07 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 2 Mar 2023 09:27:37 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Windowing): Reword documentation. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu): * java/org/gnu/emacs/EmacsFontDriver.java (EmacsFontDriver): * java/org/gnu/emacs/EmacsSdk7FontDriver.java (EmacsSdk7FontDriver): * java/org/gnu/emacs/EmacsService.java (queryBattery): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow): Make functions final and classes static where necessary. * src/android.c (struct android_emacs_service): New method `display_toast'. (android_init_emacs_service): Load new method. (android_display_toast): New function. * src/android.h: Export. * src/androidfns.c (Fandroid_detect_mouse): * src/androidselect.c (Fandroid_clipboard_owner_p) (Fandroid_set_clipboard, Fandroid_get_clipboard) (Fandroid_browse_url): Prevent crashes when called from libandroid-emacs.so. * src/androidterm.c (handle_one_android_event): Fix out of date commentary. --- java/org/gnu/emacs/EmacsActivity.java | 2 +- java/org/gnu/emacs/EmacsContextMenu.java | 2 +- java/org/gnu/emacs/EmacsFontDriver.java | 6 +++--- java/org/gnu/emacs/EmacsSdk7FontDriver.java | 6 +++--- java/org/gnu/emacs/EmacsService.java | 24 +++++++++++++++++++++++- java/org/gnu/emacs/EmacsWindow.java | 3 ++- 6 files changed, 33 insertions(+), 10 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 1c5d7605caa..c444110de60 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -207,7 +207,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onDestroy () { EmacsWindowAttachmentManager manager; diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 0de292af21a..a1bca98daa0 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -52,7 +52,7 @@ public final class EmacsContextMenu /* Whether or not a submenu was selected. */ public static boolean wasSubmenuSelected; - private class Item implements MenuItem.OnMenuItemClickListener + private static class Item implements MenuItem.OnMenuItemClickListener { public int itemID; public String itemName, tooltip; diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java index 39bda5a456d..e142a3121d3 100644 --- a/java/org/gnu/emacs/EmacsFontDriver.java +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -65,7 +65,7 @@ public abstract class EmacsFontDriver public static final int MONO = 100; public static final int CHARCELL = 110; - public class FontSpec + public static class FontSpec { /* The fields below mean the same as they do in enum font_property_index in font.h. */ @@ -99,7 +99,7 @@ public abstract class EmacsFontDriver } }; - public class FontMetrics + public static class FontMetrics { public short lbearing; public short rbearing; @@ -119,7 +119,7 @@ public abstract class EmacsFontDriver } } - public class FontEntity extends FontSpec + public static class FontEntity extends FontSpec { /* No extra fields here. */ }; diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index ba92d4cef49..ae91c299de8 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -40,7 +40,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver private static final String EM_STRING = "m"; private static final String TAG = "EmacsSdk7FontDriver"; - protected class Sdk7Typeface + protected static final class Sdk7Typeface { /* The typeface and paint. */ public Typeface typeface; @@ -164,7 +164,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } }; - protected class Sdk7FontEntity extends FontEntity + protected static final class Sdk7FontEntity extends FontEntity { /* The typeface. */ public Sdk7Typeface typeface; @@ -187,7 +187,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } }; - protected class Sdk7FontObject extends FontObject + protected final class Sdk7FontObject extends FontObject { /* The typeface. */ public Sdk7Typeface typeface; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index e61d9487375..67de5d26f53 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -54,6 +54,8 @@ import android.content.res.AssetManager; import android.database.Cursor; import android.database.MatrixCursor; +import android.hardware.input.InputManager; + import android.net.Uri; import android.os.BatteryManager; @@ -72,7 +74,7 @@ import android.provider.DocumentsContract.Document; import android.util.Log; import android.util.DisplayMetrics; -import android.hardware.input.InputManager; +import android.widget.Toast; class Holder { @@ -821,4 +823,24 @@ public final class EmacsService extends Service return new long[] { capacity, chargeCounter, currentAvg, currentNow, remaining, status, }; } + + /* Display the specified STRING in a small dialog box on the main + thread. */ + + public void + displayToast (final String string) + { + runOnUiThread (new Runnable () { + @Override + public void + run () + { + Toast toast; + + toast = Toast.makeText (getApplicationContext (), + string, Toast.LENGTH_SHORT); + toast.show (); + } + }); + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 5c481aa3ef4..ea4cf48090d 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -64,11 +64,12 @@ public final class EmacsWindow extends EmacsHandleObject { private static final String TAG = "EmacsWindow"; - private class Coordinate + private static class Coordinate { /* Integral coordinate. */ int x, y; + public Coordinate (int x, int y) { this.x = x; -- cgit v1.2.1 From 09aa948ab48261b13550b76a5820d3473caf200a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 2 Mar 2023 12:30:36 +0800 Subject: Improve criteria for restoring fullscreen state on Android * java/Makefile.in ($(CLASS_FILES) &): Touch all class files, even those javac chose not to rebuild. * java/org/gnu/emacs/EmacsActivity.java (onWindowFocusChanged): Restore fullscreen state here. (onResume): And not here. --- java/org/gnu/emacs/EmacsActivity.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index c444110de60..bcfee3f7080 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -241,6 +241,15 @@ public class EmacsActivity extends Activity { focusedActivities.add (this); lastFocusedActivity = this; + + /* Update the window insets as the focus change may have + changed the window insets as well, and the system does not + automatically restore visibility flags. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + && Build.VERSION.SDK_INT < Build.VERSION_CODES.R + && isFullscreen) + syncFullscreenWith (window); } else focusedActivities.remove (this); @@ -264,9 +273,6 @@ public class EmacsActivity extends Activity { isPaused = false; - /* Update the window insets. */ - syncFullscreenWith (window); - EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this); super.onResume (); } -- cgit v1.2.1 From bc9239eb51b1a346dac8a7f0f20b857143114bae Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 3 Mar 2023 15:23:21 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity) (onCreate): Add view tree observer. (onGlobalLayout): Sync fullscreen state. (syncFullscreenWith): Improve visibility flag setting. * src/textconv.c (select_window): New function. (textconv_query): (restore_selected_window): (really_commit_text): (really_set_composing_text): (really_set_composing_region): (really_delete_surrounding_text): (really_set_point_and_mark): (get_extracted_text): Call it instead of Fselect_window to avoid selecting the mini window if it is no longer active. --- java/org/gnu/emacs/EmacsActivity.java | 42 ++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index bcfee3f7080..62bef33fab3 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -31,6 +31,7 @@ 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; @@ -38,7 +39,8 @@ import android.widget.FrameLayout.LayoutParams; import android.widget.FrameLayout; public class EmacsActivity extends Activity - implements EmacsWindowAttachmentManager.WindowConsumer + implements EmacsWindowAttachmentManager.WindowConsumer, + ViewTreeObserver.OnGlobalLayoutListener { public static final String TAG = "EmacsActivity"; @@ -180,6 +182,8 @@ public class EmacsActivity extends Activity { FrameLayout.LayoutParams params; Intent intent; + View decorView; + ViewTreeObserver observer; /* See if Emacs should be started with -Q. */ intent = getIntent (); @@ -203,9 +207,29 @@ public class EmacsActivity extends Activity /* Add this activity to the list of available activities. */ EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this); + /* Start observing global layout changes between Jelly Bean and Q. + This is required to restore the fullscreen state whenever the + on screen keyboard is displayed, as there is otherwise no way + to determine when the on screen keyboard becomes visible. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) + { + decorView = getWindow ().getDecorView (); + observer = decorView.getViewTreeObserver (); + observer.addOnGlobalLayoutListener (this); + } + super.onCreate (savedInstanceState); } + @Override + public final void + onGlobalLayout () + { + syncFullscreenWith (window); + } + @Override public final void onDestroy () @@ -348,22 +372,20 @@ public class EmacsActivity extends Activity if (isFullscreen) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) - /* This flag means that Emacs will be full screen, but - the system will cancel the full screen state upon - switching to another program. */ - view.setSystemUiVisibility (View.SYSTEM_UI_FLAG_FULLSCREEN); - else + flags = 0; + flags |= View.SYSTEM_UI_FLAG_FULLSCREEN; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { /* These flags means that Emacs will be full screen as long as the state flag is set. */ - flags = 0; - flags |= View.SYSTEM_UI_FLAG_FULLSCREEN; flags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; flags |= View.SYSTEM_UI_FLAG_IMMERSIVE; flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - view.setSystemUiVisibility (flags); } + + /* Apply the given flags. */ + view.setSystemUiVisibility (flags); } else view.setSystemUiVisibility (View.SYSTEM_UI_FLAG_VISIBLE); -- cgit v1.2.1 From 48b5a770f247d8c027d209ce941767ab5a7d139d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 3 Mar 2023 16:00:27 +0800 Subject: Fix visiting and saving writable content provider files * java/org/gnu/emacs/EmacsService.java (checkContentUri): Improve debug output. * lisp/files.el (basic-save-buffer): Check whether or not file itself exists before checking for the existence of the directory containing it. * src/android.c (android_open): Don't forget to set errno after open_content_uri fails. --- java/org/gnu/emacs/EmacsService.java | 2 ++ 1 file changed, 2 insertions(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 67de5d26f53..d9cb25f3e9c 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -752,6 +752,8 @@ public final class EmacsService extends Service if (writable) mode += "w"; + Log.d (TAG, "checkContentUri: checking against mode " + mode); + try { fd = resolver.openFileDescriptor (Uri.parse (name), mode); -- cgit v1.2.1 From 2634765bc382c27e2d11dc14174ca80d9cf41e15 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 4 Mar 2023 15:55:09 +0800 Subject: Improve context menus on old versions of Android * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): New field `lastClosedMenu'. (onContextMenuClosed): Don't send event if a menu is closed twice in a row. Also, clear wasSubmenuSelected immediately. * java/org/gnu/emacs/EmacsContextMenu.java: Display submenus manually in Android 6.0 and earlier. * java/org/gnu/emacs/EmacsView.java (onCreateContextMenu) (popupMenu): Adjust accordingly. --- java/org/gnu/emacs/EmacsActivity.java | 17 ++++++- java/org/gnu/emacs/EmacsContextMenu.java | 81 ++++++++++++++++++++++---------- java/org/gnu/emacs/EmacsView.java | 9 ++-- 3 files changed, 76 insertions(+), 31 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 62bef33fab3..13002d5d0e5 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -65,6 +65,9 @@ public class EmacsActivity extends Activity /* Whether or not this activity is fullscreen. */ private boolean isFullscreen; + /* The last context menu to be closed. */ + private Menu lastClosedMenu; + static { focusedActivities = new ArrayList (); @@ -308,9 +311,19 @@ public class EmacsActivity extends Activity Log.d (TAG, "onContextMenuClosed: " + menu); /* See the comment inside onMenuItemClick. */ + if (EmacsContextMenu.wasSubmenuSelected - && menu.toString ().contains ("ContextMenuBuilder")) - return; + || menu == lastClosedMenu) + { + EmacsContextMenu.wasSubmenuSelected = false; + lastClosedMenu = menu; + return; + } + + /* lastClosedMenu is set because Android apparently calls this + function twice. */ + + lastClosedMenu = null; /* Send a context menu event given that no menu item has already been selected. */ diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index a1bca98daa0..d1a624e68d9 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -58,6 +58,7 @@ public final class EmacsContextMenu public String itemName, tooltip; public EmacsContextMenu subMenu; public boolean isEnabled, isCheckable, isChecked; + public EmacsView inflatedView; @Override public boolean @@ -67,6 +68,34 @@ public final class EmacsContextMenu if (subMenu != null) { + /* Android 6.0 and earlier don't support nested submenus + properly, so display the submenu popup by hand. */ + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + { + Log.d (TAG, "onMenuItemClick: displaying submenu " + subMenu); + + /* Still set wasSubmenuSelected -- if not set, the + dismissal of this context menu will result in a + context menu event being sent. */ + wasSubmenuSelected = true; + + /* Running a popup menu from inside a click handler + doesn't work, so make sure it is displayed + outside. */ + + inflatedView.post (new Runnable () { + @Override + public void + run () + { + inflatedView.popupMenu (subMenu, 0, 0, true); + } + }); + + return true; + } + /* After opening a submenu within a submenu, Android will send onContextMenuClosed for a ContextMenuBuilder. This will normally confuse Emacs into thinking that the @@ -164,10 +193,11 @@ public final class EmacsContextMenu return item.subMenu; } - /* Add the contents of this menu to MENU. */ + /* Add the contents of this menu to MENU. Assume MENU will be + displayed in INFLATEDVIEW. */ private void - inflateMenuItems (Menu menu) + inflateMenuItems (Menu menu, EmacsView inflatedView) { Intent intent; MenuItem menuItem; @@ -177,26 +207,26 @@ public final class EmacsContextMenu { if (item.subMenu != null) { - try + /* This is a submenu. On versions of Android which + support doing so, create the submenu and add the + contents of the menu to it. + + Note that Android 4.0 and later technically supports + having multiple layers of nested submenus, but if they + are used, onContextMenuClosed becomes unreliable. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - /* This is a submenu. On versions of Android which - support doing so, create the submenu and add the - contents of the menu to it. */ submenu = menu.addSubMenu (item.itemName); - item.subMenu.inflateMenuItems (submenu); - } - catch (UnsupportedOperationException exception) - { - /* This version of Android has a restriction - preventing submenus from being added to submenus. - Inflate everything into the parent menu - instead. */ - item.subMenu.inflateMenuItems (menu); - continue; + item.subMenu.inflateMenuItems (submenu, inflatedView); + + /* This is still needed to set wasSubmenuSelected. */ + menuItem = submenu.getItem (); } + else + menuItem = menu.add (item.itemName); - /* This is still needed to set wasSubmenuSelected. */ - menuItem = submenu.getItem (); + item.inflatedView = inflatedView; menuItem.setOnMenuItemClickListener (item); } else @@ -227,16 +257,14 @@ public final class EmacsContextMenu } } - /* Enter the items in this context menu to MENU. Create each menu - item with an Intent containing a Bundle, where the key - "emacs:menu_item_hi" maps to the high 16 bits of the - corresponding item ID, and the key "emacs:menu_item_low" maps to - the low 16 bits of the item ID. */ + /* Enter the items in this context menu to MENU. + Assume that MENU will be displayed in VIEW; this may lead to + popupMenu being called on VIEW if a submenu is selected. */ public void - expandTo (Menu menu) + expandTo (Menu menu, EmacsView view) { - inflateMenuItems (menu); + inflateMenuItems (menu, view); } /* Return the parent or NULL. */ @@ -260,7 +288,8 @@ public final class EmacsContextMenu /* No submenu has been selected yet. */ wasSubmenuSelected = false; - return window.view.popupMenu (this, xPosition, yPosition); + return window.view.popupMenu (this, xPosition, yPosition, + false); } /* Display this context menu on WINDOW, at xPosition and diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index d2330494bc7..617836d8811 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -464,19 +464,22 @@ public final class EmacsView extends ViewGroup if (contextMenu == null) return; - contextMenu.expandTo (menu); + contextMenu.expandTo (menu, this); } public boolean popupMenu (EmacsContextMenu menu, int xPosition, - int yPosition) + int yPosition, boolean force) { - if (popupActive) + if (popupActive && !force) return false; contextMenu = menu; popupActive = true; + Log.d (TAG, "popupMenu: " + menu + " @" + xPosition + + ", " + yPosition + " " + force); + /* Use showContextMenu (float, float) on N to get actual popup behavior. */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) -- cgit v1.2.1 From 26b3b8433d933c9f8b26b83ca96ac1bb47711907 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 5 Mar 2023 15:55:24 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsOpenActivity.java (onCreate): Don't set the style here. * java/res/values-v11/style.xml: * java/res/values-v14/style.xml: * java/res/values-v29/style.xml: * java/res/values/style.xml: Define styles for the emacsclient wrapper. * src/keyboard.c (read_key_sequence): Don't disable text conversion if use_mouse_menu or if a menu bar prefix key is being displayed. --- java/org/gnu/emacs/EmacsOpenActivity.java | 5 ----- 1 file changed, 5 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index fddd5331d2f..ac643ae8a13 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -380,11 +380,6 @@ public final class EmacsOpenActivity extends Activity return; } - /* Set an appropriate theme. */ - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) - setTheme (android.R.style.Theme_DeviceDefault); - /* Now see if the action specified is supported by Emacs. */ if (action.equals ("android.intent.action.VIEW") -- cgit v1.2.1 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') 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.2.1 From 97ca0a855116797779450bfb758ea6c706348df3 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 6 Mar 2023 11:25:51 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsService.java (sync): Delete function. * java/org/gnu/emacs/EmacsView.java (handleDirtyBitmap): Erase with window background. (onDetachedFromWindow): Only recycle bitmap if non-NULL. * java/org/gnu/emacs/EmacsWindow.java (background): New field. (changeWindowBackground): Set it. * src/android.c (struct android_emacs_service): Remove `sync'. (android_init_emacs_service): Likewise. (android_sync): Delete function. * src/androidfns.c (android_create_tip_frame): Set frame background color correctly. (Fx_show_tip): Make the tip frame visible. * src/androidgui.h: Update prototypes. * src/androidterm.c (handle_one_android_event): Handle tooltip movement correctly. --- java/org/gnu/emacs/EmacsService.java | 19 ------------------- java/org/gnu/emacs/EmacsView.java | 7 +++++-- java/org/gnu/emacs/EmacsWindow.java | 7 +++++++ 3 files changed, 12 insertions(+), 21 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index d05ebce75dc..f99d7a40067 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -481,25 +481,6 @@ public final class EmacsService extends Service return String.valueOf (keysym); } - public void - sync () - { - Runnable runnable; - - runnable = new Runnable () { - public void - run () - { - synchronized (this) - { - notify (); - } - } - }; - - syncRunnable (runnable); - } - /* Start the Emacs service if necessary. On Android 26 and up, diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index aefc79c4fb7..f751eaaa3e4 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -168,7 +168,7 @@ public final class EmacsView extends ViewGroup = Bitmap.createBitmap (measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888); - bitmap.eraseColor (0xffffffff); + bitmap.eraseColor (window.background | 0xff000000); /* And canvases. */ canvas = new Canvas (bitmap); @@ -507,7 +507,10 @@ public final class EmacsView extends ViewGroup synchronized (this) { /* Recycle the bitmap and call GC. */ - bitmap.recycle (); + + if (bitmap != null) + bitmap.recycle (); + bitmap = null; canvas = null; surfaceView.setBitmap (null, null); diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index ffc7476f010..9d907d2b481 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -129,6 +129,10 @@ public final class EmacsWindow extends EmacsHandleObject /* Whether or not this window is fullscreen. */ public boolean fullscreen; + /* The window background pixel. This is used by EmacsView when + creating new bitmaps. */ + public volatile int background; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height, boolean overrideRedirect) @@ -183,6 +187,9 @@ public final class EmacsWindow extends EmacsHandleObject /* scratchGC is used as the argument to a FillRectangles req. */ scratchGC.foreground = pixel; scratchGC.markDirty (false); + + /* Make the background known to the view as well. */ + background = pixel; } public Rect -- cgit v1.2.1 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 ++++++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) (limited to 'java/org/gnu') 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; -- cgit v1.2.1 From 83c29bd40e0c7e3975f575067a3112a0191b5590 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 7 Mar 2023 16:49:45 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsWindow.java (onSomeKindOfMotionEvent): Dismiss splurious LeaveNotify events from button presses. * src/android.c (android_change_window_attributes) (android_change_gc, android_set_clip_rectangles) (android_reparent_window, android_clear_window, android_map_window) (android_unmap_window, android_resize_window, android_move_window) (android_swap_buffers, android_fill_rectangle, android_copy_area) (android_fill_polygon, android_draw_rectangle, android_draw_point) (android_draw_line, android_clear_area, android_bell) (android_set_input_focus, android_raise_window) (android_lower_window, android_set_dont_focus_on_map) (android_set_dont_accept_focus, android_get_keysym_name) (android_toggle_on_screen_keyboard, android_restart_emacs) (android_display_toast, android_update_ic, android_reset_ic) (android_set_fullscreen): Optimize by specifying the class explicitly when calling a method. --- java/org/gnu/emacs/EmacsWindow.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 9d907d2b481..3569d93136b 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -906,9 +906,16 @@ public final class EmacsWindow extends EmacsHandleObject return true; case MotionEvent.ACTION_HOVER_EXIT: - EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (), - (int) event.getY (), - event.getEventTime ()); + + /* If the exit event comes from a button press, its button + state will have extra bits compared to the last known + button state. Since the exit event will interfere with + tool bar button presses, ignore such splurious events. */ + + if ((event.getButtonState () & ~lastButtonState) == 0) + EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime ()); return true; case MotionEvent.ACTION_BUTTON_PRESS: -- cgit v1.2.1 From bb55528c7b58c5f50336ed3f2ff9759559d78680 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 8 Mar 2023 15:04:49 +0800 Subject: Update Android port * doc/emacs/android.texi (Android File System): Document what `temp~unlinked' means in the temporary files directory. * java/org/gnu/emacs/EmacsService.java (updateExtractedText): New function. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Ask the input method nicely to not display the extracted text UI. * src/android.c (struct android_emacs_service): New method `updateExtractedText'. (android_hack_asset_fd_fallback): Improve naming convention. Fix typo. (android_init_emacs_service): Add new method. (android_update_extracted_text): New function. (android_open_asset): Fix typo. * src/androidgui.h: Update prototypes. * src/androidterm.c (struct android_get_extracted_text_context): New field `flags'. (android_get_extracted_text): Set flags on the frame's output data. (android_build_extracted_text): New function. (getExtractedText): Move out class structures. (android_update_selection): Send updates to extracted text if the input method asked for them. (android_reset_conversion): Clear extracted text flags. * src/androidterm.h (struct android_output): New fields for storing extracted text data. --- java/org/gnu/emacs/EmacsService.java | 12 ++++++++++++ java/org/gnu/emacs/EmacsView.java | 1 + 2 files changed, 13 insertions(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index f99d7a40067..848ad4de789 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -29,6 +29,7 @@ import android.graphics.Point; import android.view.InputDevice; import android.view.KeyEvent; +import android.view.inputmethod.ExtractedText; import android.app.Notification; import android.app.NotificationManager; @@ -811,4 +812,15 @@ public final class EmacsService extends Service } }); } + + public void + updateExtractedText (EmacsWindow window, ExtractedText text, + int token) + { + if (DEBUG_IC) + Log.d (TAG, "updateExtractedText: @" + token + ", " + text); + + window.view.imManager.updateExtractedText (window.view, + token, text); + } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 90a2c912a5a..6ace609f386 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -569,6 +569,7 @@ public final class EmacsView extends ViewGroup /* Make sure the input method never displays a full screen input box that obscures Emacs. */ info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; + info.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI; /* Set a reasonable inputType. */ info.inputType = InputType.TYPE_CLASS_TEXT; -- cgit v1.2.1 From dcc3c63c6e6f2536341b22cb428ef4755a171e6b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 9 Mar 2023 11:27:00 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu) (addItem): New argument `tooltip'. --- java/org/gnu/emacs/EmacsContextMenu.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index dec5e148a8e..0553ff9d4a3 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -139,11 +139,14 @@ public final class EmacsContextMenu If this is not a submenu and ISCHECKABLE is set, make the item checkable. Likewise, if ISCHECKED is set, make the item - checked. */ + checked. + + If TOOLTIP is non-NULL, set the menu item tooltip to TOOLTIP. */ public void addItem (int itemID, String itemName, boolean isEnabled, - boolean isCheckable, boolean isChecked) + boolean isCheckable, boolean isChecked, + String tooltip) { Item item; @@ -153,6 +156,7 @@ public final class EmacsContextMenu item.isEnabled = isEnabled; item.isCheckable = isCheckable; item.isChecked = isChecked; + item.tooltip = tooltip; menuItems.add (item); } -- cgit v1.2.1 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 ++- 4 files changed, 29 insertions(+), 11 deletions(-) (limited to 'java/org/gnu') 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, -- cgit v1.2.1 From 488a75f2e2b73038ff341f3484a8cf8584633eff Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Mar 2023 09:40:41 +0800 Subject: Port Android battery status to Android 4.4 and earlier * java/org/gnu/emacs/EmacsService.java (EmacsService) (queryBattery19): New function. (queryBattery): Call it on old systems. Also, return AC line status and temperature. * lisp/battery.el (battery-android): Implement more format directives. * src/android.c (android_query_battery): Handle new status fields. * src/android.h (struct android_battery_state): Add `plugged' and `temperature'. * src/androidfns.c (Fandroid_query_battery): Return new fields. --- java/org/gnu/emacs/EmacsService.java | 65 ++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 6 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 848ad4de789..9c48c56ca26 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -40,6 +40,7 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.ContentResolver; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager; @@ -738,6 +739,36 @@ public final class EmacsService extends Service } } + private long[] + queryBattery19 () + { + IntentFilter filter; + Intent battery; + long capacity, chargeCounter, currentAvg, currentNow; + long status, remaining, plugged, temp; + + filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED); + battery = registerReceiver (null, filter); + + if (battery == null) + return null; + + capacity = battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 0); + chargeCounter + = (battery.getIntExtra (BatteryManager.EXTRA_SCALE, 0) + / battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 100) * 100); + currentAvg = 0; + currentNow = 0; + status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0); + remaining = -1; + plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0); + temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0); + + return new long[] { capacity, chargeCounter, currentAvg, + currentNow, remaining, status, plugged, + temp, }; + } + /* Return the status of the battery. See struct android_battery_status for the order of the elements returned. @@ -750,14 +781,16 @@ public final class EmacsService extends Service Object tem; BatteryManager manager; long capacity, chargeCounter, currentAvg, currentNow; - long status, remaining; + long status, remaining, plugged, temp; int prop; + IntentFilter filter; + Intent battery; - /* Android 4.4 or earlier require applications to listen to - changes to the battery instead of querying for its status. */ + /* Android 4.4 or earlier require applications to use a different + API to query the battery status. */ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) - return null; + return queryBattery19 (); tem = getSystemService (Context.BATTERY_SERVICE); manager = (BatteryManager) tem; @@ -776,7 +809,8 @@ public final class EmacsService extends Service only return ``charging'' or ``discharging''. */ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - status = manager.getIntProperty (BatteryManager.BATTERY_PROPERTY_STATUS); + status + = manager.getIntProperty (BatteryManager.BATTERY_PROPERTY_STATUS); else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) status = (manager.isCharging () ? BatteryManager.BATTERY_STATUS_CHARGING @@ -789,8 +823,27 @@ public final class EmacsService extends Service if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) remaining = manager.computeChargeTimeRemaining (); + plugged = -1; + temp = -1; + + /* Now obtain additional information from the battery manager. */ + + filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED); + battery = registerReceiver (null, filter); + + if (battery != null) + { + plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0); + temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0); + + /* Make status more reliable. */ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0); + } + return new long[] { capacity, chargeCounter, currentAvg, - currentNow, remaining, status, }; + currentNow, remaining, status, plugged, + temp, }; } /* Display the specified STRING in a small dialog box on the main -- cgit v1.2.1 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') 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.2.1 From ae5513ede52536df2cd823699d6168985915ce0f Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Mar 2023 15:16:05 +0800 Subject: Implement mouse cursors on Android 7.0 and later * java/org/gnu/emacs/EmacsWindow.java (defineCursor): New function. * src/android.c (struct android_emacs_cursor): New struct. (android_init_emacs_cursor): New function. (JNICALL): Call it. (android_create_font_cursor, android_define_cursor) (android_free_cursor): New functions. * src/android.h (enum android_handle_type): Add cursor handle type. * src/androidfns.c (Fx_create_frame, android_create_tip_frame) (enum mouse_cursor, struct mouse_cursor_types, mouse_cursor_types) (struct mouse_cursor_data, android_set_mouse_color) (syms_of_androidfns): * src/androidgui.h (enum android_cursor_shape): * src/androidterm.c (make_invisible_cursor) (android_toggle_invisible_pointer, android_free_frame_resources) (android_define_frame_cursor): * src/androidterm.h (struct android_display_info) (struct android_output): Port mouse cursor code over from X. --- java/org/gnu/emacs/EmacsWindow.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 3569d93136b..6be609edcfe 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -1222,4 +1222,20 @@ public final class EmacsWindow extends EmacsHandleObject } }); } + + public void + defineCursor (final EmacsCursor cursor) + { + /* Don't post this message if pointer icons aren't supported. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + view.post (new Runnable () { + @Override + public void + run () + { + view.setPointerIcon (cursor.icon); + } + }); + } }; -- cgit v1.2.1 From 98d43dbef5786e02389e883ba5b4aaae7f261b79 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 10 Mar 2023 15:16:13 +0800 Subject: * java/org/gnu/emacs/EmacsCursor.java: New file. --- java/org/gnu/emacs/EmacsCursor.java | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 java/org/gnu/emacs/EmacsCursor.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsCursor.java b/java/org/gnu/emacs/EmacsCursor.java new file mode 100644 index 00000000000..c14c6f2a11b --- /dev/null +++ b/java/org/gnu/emacs/EmacsCursor.java @@ -0,0 +1,47 @@ +/* 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.PointerIcon; +import android.os.Build; + +/* Cursor wrapper. Note that pointer icons are not supported prior to + Android 24. */ + +public final class EmacsCursor extends EmacsHandleObject +{ + /* The pointer icon associated with this cursor. */ + public final PointerIcon icon; + + public + EmacsCursor (short handle, int glyph) + { + super (handle); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + { + icon = null; + return; + } + + icon = PointerIcon.getSystemIcon (EmacsService.SERVICE, + glyph); + } +}; -- cgit v1.2.1 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. --- java/org/gnu/emacs/EmacsNative.java | 4 ++++ java/org/gnu/emacs/EmacsView.java | 18 ++++++++++++++++++ java/org/gnu/emacs/EmacsWindow.java | 5 ++++- 3 files changed, 26 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') 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); } }); } -- cgit v1.2.1 From 4a328b857812625ec82b31d8efd928f8580d9561 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 11 Mar 2023 11:35:59 +0800 Subject: Fix problems with the menu bar on large screen Android devices * java/org/gnu/emacs/EmacsActivity.java (onContextMenuClosed): Process submenu closing normally if it happens more than 300 ms after a submenu item was selected. * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu) (onMenuItemClick, display1): Give `wasSubmenuSelected' different values depending on how the submenu was selected. --- java/org/gnu/emacs/EmacsActivity.java | 8 ++++++-- java/org/gnu/emacs/EmacsContextMenu.java | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 735a464be8e..b480fc40b2e 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -321,10 +321,14 @@ public class EmacsActivity extends Activity /* See the comment inside onMenuItemClick. */ - if (EmacsContextMenu.wasSubmenuSelected + if (((EmacsContextMenu.wasSubmenuSelected == -2) + || (EmacsContextMenu.wasSubmenuSelected >= 0 + && ((System.currentTimeMillis () + - EmacsContextMenu.wasSubmenuSelected) + <= 300))) || menu == lastClosedMenu) { - EmacsContextMenu.wasSubmenuSelected = false; + EmacsContextMenu.wasSubmenuSelected = -1; lastClosedMenu = menu; return; } diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index abc1869ac6a..d780641ba70 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -46,8 +46,11 @@ public final class EmacsContextMenu /* Whether or not an item was selected. */ public static boolean itemAlreadySelected; - /* Whether or not a submenu was selected. */ - public static boolean wasSubmenuSelected; + /* Whether or not a submenu was selected. + Value is -1 if no; value is -2 if yes, and a context menu + close event will definitely be sent. Any other value is + the timestamp when the submenu was selected. */ + public static long wasSubmenuSelected; /* The serial ID of the last context menu to be displayed. */ public static int lastMenuEventSerial; @@ -78,7 +81,7 @@ public final class EmacsContextMenu /* Still set wasSubmenuSelected -- if not set, the dismissal of this context menu will result in a context menu event being sent. */ - wasSubmenuSelected = true; + wasSubmenuSelected = -2; /* Running a popup menu from inside a click handler doesn't work, so make sure it is displayed @@ -103,8 +106,13 @@ public final class EmacsContextMenu Setting this flag makes EmacsActivity to only handle SubMenuBuilder being closed, which always means the menu - has actually been dismissed. */ - wasSubmenuSelected = true; + has actually been dismissed. + + However, these extraneous events aren't sent on devices + where submenus display without dismissing their parents. + Thus, only ignore the close event if it happens within + 300 milliseconds of the submenu being selected. */ + wasSubmenuSelected = System.currentTimeMillis (); return false; } @@ -291,7 +299,7 @@ public final class EmacsContextMenu itemAlreadySelected = false; /* No submenu has been selected yet. */ - wasSubmenuSelected = false; + wasSubmenuSelected = -1; return window.view.popupMenu (this, xPosition, yPosition, false); -- cgit v1.2.1 From a17380e80d162dbc15110ce84ff2e12e11e0623b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 12 Mar 2023 15:43:14 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity) (onCancel): New function. (displayFailureDialog): Handle dialog cancellation. * src/sfntfont.c (sfnt_parse_languages): Look for SLNG tag if DLNG is not present. --- java/org/gnu/emacs/EmacsOpenActivity.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index 51335ddb2dd..e8fb24d53d8 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -68,7 +68,8 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; public final class EmacsOpenActivity extends Activity - implements DialogInterface.OnClickListener + implements DialogInterface.OnClickListener, + DialogInterface.OnCancelListener { private static final String TAG = "EmacsOpenActivity"; @@ -121,6 +122,13 @@ public final class EmacsOpenActivity extends Activity finish (); } + @Override + public void + onCancel (DialogInterface dialog) + { + finish (); + } + public String readEmacsClientLog () { @@ -178,6 +186,7 @@ public final class EmacsOpenActivity extends Activity dialog.setMessage (text); dialog.setButton (DialogInterface.BUTTON_POSITIVE, "OK", this); + dialog.setOnCancelListener (this); dialog.show (); } -- cgit v1.2.1 From b776feb7f2737fb6b3fca05ae3b786dc67a2a9ae Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 13 Mar 2023 13:25:02 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Startup): Document changes to emacsclient wrapper. * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity) (startEmacsClient): Open EmacsActivity if the service is not running. * java/org/gnu/emacs/EmacsService.java (onCreate): * java/org/gnu/emacs/EmacsThread.java (EmacsThread, run): Pass any file to open to Emacs. * lisp/term/android-win.el (handle-args-function): Implement. --- java/org/gnu/emacs/EmacsOpenActivity.java | 15 +++++++++++++++ java/org/gnu/emacs/EmacsService.java | 7 +++++-- java/org/gnu/emacs/EmacsThread.java | 29 +++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 6 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index e8fb24d53d8..f402e25c7fb 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -72,6 +72,7 @@ public final class EmacsOpenActivity extends Activity DialogInterface.OnCancelListener { private static final String TAG = "EmacsOpenActivity"; + public static String fileToOpen; private class EmacsClientThread extends Thread { @@ -317,6 +318,20 @@ public final class EmacsOpenActivity extends Activity Process process; EmacsClientThread thread; File file; + Intent intent; + + /* If the Emacs service is not running, then start Emacs and make + it open this file. */ + + if (EmacsService.SERVICE == null) + { + fileToOpen = fileName; + intent = new Intent (EmacsOpenActivity.this, + EmacsActivity.class); + finish (); + startActivity (intent); + return; + } libDir = EmacsService.getLibraryDirectory (this); builder = new ProcessBuilder (libDir + "/libemacsclient.so", diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 9c48c56ca26..33436892caa 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -215,7 +215,8 @@ public final class EmacsService extends Service classPath = getApkFile (); Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir - + ", libDir = " + libDir + ", and classPath = " + classPath); + + ", libDir = " + libDir + ", and classPath = " + classPath + + "; fileToOpen = " + EmacsOpenActivity.fileToOpen); /* Start the thread that runs Emacs. */ thread = new EmacsThread (this, new Runnable () { @@ -228,7 +229,9 @@ public final class EmacsService extends Service (float) pixelDensityY, classPath, EmacsService.this); } - }, needDashQ); + }, needDashQ, + /* If any file needs to be opened, open it now. */ + EmacsOpenActivity.fileToOpen); thread.start (); } catch (IOException exception) diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 30484710651..d175fe332b5 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -20,24 +20,32 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import java.lang.Thread; +import java.util.Arrays; import android.os.Build; +import android.util.Log; public class EmacsThread extends Thread { + private static final String TAG = "EmacsThread"; + /* Whether or not Emacs should be started -Q. */ private boolean startDashQ; /* Runnable run to initialize Emacs. */ private Runnable paramsClosure; + /* Whether or not to open a file after starting Emacs. */ + private String fileToOpen; + public EmacsThread (EmacsService service, Runnable paramsClosure, - boolean startDashQ) + boolean startDashQ, String fileToOpen) { super ("Emacs main thread"); this.startDashQ = startDashQ; this.paramsClosure = paramsClosure; + this.fileToOpen = fileToOpen; } @Override @@ -46,14 +54,27 @@ public class EmacsThread extends Thread { String args[]; - if (!startDashQ) - args = new String[] { "libandroid-emacs.so", }; + if (fileToOpen == 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", + fileToOpen, }; + else + args = new String[] { "libandroid-emacs.so", "-Q", + fileToOpen, }; + } paramsClosure.run (); /* Run the native code now. */ + Log.d (TAG, "run: " + Arrays.toString (args)); EmacsNative.initEmacs (args, EmacsApplication.dumpFileName, Build.VERSION.SDK_INT); } -- cgit v1.2.1 From 5964051fcefc52e02c88a41b91797cc7a785d550 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 14 Mar 2023 09:48:02 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Windowing): Document how to display dialogs when Emacs is in the background. * java/org/gnu/emacs/EmacsDialog.java (display1): Use system dialogs if possible. --- java/org/gnu/emacs/EmacsDialog.java | 45 ++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 8 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index aed84de29e5..de5a37bd5c5 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -23,8 +23,14 @@ import java.util.List; import java.util.ArrayList; import android.app.AlertDialog; -import android.content.DialogInterface; + import android.content.Context; +import android.content.DialogInterface; + +import android.os.Build; + +import android.provider.Settings; + import android.util.Log; import android.widget.Button; @@ -33,6 +39,8 @@ import android.widget.FrameLayout; import android.view.View; import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; /* Toolkit dialog implementation. This object is built from JNI and describes a single alert dialog. Then, `inflate' turns it into @@ -225,33 +233,54 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener /* Internal helper for display run on the main thread. */ + @SuppressWarnings("deprecation") private boolean display1 () { - EmacsActivity activity; - int size; + Context context; + int size, type; Button buttonView; EmacsButton button; AlertDialog dialog; + Window window; + + /* First, try to display a dialog using the service context. */ - if (EmacsActivity.focusedActivities.isEmpty ()) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || Settings.canDrawOverlays (EmacsService.SERVICE)) + context = EmacsService.SERVICE; + else if (EmacsActivity.focusedActivities.isEmpty ()) { /* If focusedActivities is empty then this dialog may have been displayed immediately after a popup dialog is dismissed. */ - activity = EmacsActivity.lastFocusedActivity; + context = EmacsActivity.lastFocusedActivity; - if (activity == null) + if (context == null) return false; } else - activity = EmacsActivity.focusedActivities.get (0); + context = EmacsActivity.focusedActivities.get (0); + + Log.d (TAG, "display1: using context " + context); - dialog = dismissDialog = toAlertDialog (activity); + dialog = dismissDialog = toAlertDialog (context); try { + if (context == EmacsService.SERVICE) + { + /* Apply the system alert window type to make sure this + dialog can be displayed. */ + + window = dialog.getWindow (); + type = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + : WindowManager.LayoutParams.TYPE_PHONE); + window.setType (type); + } + dismissDialog.show (); } catch (Exception exception) -- cgit v1.2.1 From d6bddca26c7cf827e098ae783e865fcbcdd48799 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 14 Mar 2023 13:19:01 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsWindow.java (figureChange): Detect mice on up events as well. (onSomeKindOfMotionEvent): Work past framework bug. * src/androidterm.c (android_perform_conversion_query): * src/textconv.c (textconv_query): * src/textconv.h (TEXTCONV_SKIP_ACTIVE_REGION): Remove unused code. --- java/org/gnu/emacs/EmacsWindow.java | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index d786c104153..a8d1beedef7 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -755,6 +755,14 @@ public final class EmacsWindow extends EmacsHandleObject break; case MotionEvent.ACTION_UP: + + /* 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; + /* Primary pointer released with index 0. */ pointerID = event.getPointerId (0); pointerMap.remove (pointerID); @@ -916,6 +924,7 @@ public final class EmacsWindow extends EmacsHandleObject EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (), (int) event.getY (), event.getEventTime ()); + return true; case MotionEvent.ACTION_BUTTON_PRESS: @@ -949,13 +958,35 @@ public final class EmacsWindow extends EmacsHandleObject return true; case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_UP: /* 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_UP: + /* However, if ACTION_UP reports a different button state from + the last known state, look up which button was released and + send a ButtonRelease event; this is to work around a bug in + the framework where real ACTION_BUTTON_RELEASE events are + not delivered. */ + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + return true; + + if (event.getButtonState () == 0 && lastButtonState != 0) + { + EmacsNative.sendButtonRelease (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime (), + lastModifiers, + whatButtonWasIt (event, false)); + lastButtonState = event.getButtonState (); + } + + return true; + case MotionEvent.ACTION_SCROLL: /* Send a scroll event with the specified deltas. */ EmacsNative.sendWheel (this.handle, (int) event.getX (), -- cgit v1.2.1 From c74bab6067cb95516b25e5650d5077441541ea1e Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 15 Mar 2023 09:46:01 +0800 Subject: Update Android port * doc/lispref/commands.texi (Misc Events): Document variable `disable-inhibit-text-conversion'. * java/org/gnu/emacs/EmacsDialog.java (display1): Try an activity that is certain to be focused first. * lisp/touch-screen.el (touch-screen-track-tap) (touch-screen-track-drag): Bind `disable-inhibit-text-conversion'. * src/keyboard.c (read_key_sequence): Only disable text conversion if an actual function or numeric key is found in the key sequence. (syms_of_keyboard): New variable `disable-inhibit-text-conversion'. * src/lread.c (read_filtered_event): Check new variable. * src/textconv.c (textconv_query): Remove unused label. --- java/org/gnu/emacs/EmacsDialog.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index de5a37bd5c5..3a5f22021fc 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -244,23 +244,26 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener AlertDialog dialog; Window window; - /* First, try to display a dialog using the service context. */ - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || Settings.canDrawOverlays (EmacsService.SERVICE)) - context = EmacsService.SERVICE; - else if (EmacsActivity.focusedActivities.isEmpty ()) + if (EmacsActivity.focusedActivities.isEmpty ()) { /* If focusedActivities is empty then this dialog may have been displayed immediately after a popup dialog is - dismissed. */ + dismissed. Or Emacs might legitimately be in the + background. Try the service context first if possible. */ - context = EmacsActivity.lastFocusedActivity; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || Settings.canDrawOverlays (EmacsService.SERVICE)) + context = EmacsService.SERVICE; + else + context = EmacsActivity.lastFocusedActivity; if (context == null) return false; } else + /* Display using the activity context when Emacs is in the + foreground, as this allows the dialog to be dismissed more + consistently. */ context = EmacsActivity.focusedActivities.get (0); Log.d (TAG, "display1: using context " + context); -- cgit v1.2.1 From ce66228ac538949d9dda2fc759982600344c7a5e Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 16 Mar 2023 14:13:21 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsDocumentsProvider.java (queryRoots): Add icon to document root. --- java/org/gnu/emacs/EmacsDocumentsProvider.java | 2 ++ 1 file changed, 2 insertions(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index f70da6040ff..a92a1a5d330 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -66,6 +66,7 @@ public final class EmacsDocumentsProvider extends DocumentsProvider Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES, Root.COLUMN_FLAGS, + Root.COLUMN_ICON, Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, @@ -116,6 +117,7 @@ public final class EmacsDocumentsProvider extends DocumentsProvider row.add (Root.COLUMN_FLAGS, (Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD)); + row.add (Root.COLUMN_ICON, R.drawable.emacs); row.add (Root.FLAG_LOCAL_ONLY); row.add (Root.COLUMN_TITLE, "Emacs"); row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ()); -- cgit v1.2.1 From da660a1ffa3218f8e6ec4dfd5422ca6c1ded38ae Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 17 Mar 2023 10:38:09 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsView.java (cancelPopupMenu): Dismiss context menu correctly. (isOpaque): New function. * java/org/gnu/emacs/EmacsWindowAttachmentManager.java: Make consumer list public. --- java/org/gnu/emacs/EmacsView.java | 21 +++++++++++++++++++++ .../org/gnu/emacs/EmacsWindowAttachmentManager.java | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 878ef2f3fbf..681da98fa16 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -514,6 +514,17 @@ public final class EmacsView extends ViewGroup contextMenu = null; popupActive = false; + + /* It is not possible to know with 100% certainty which activity + is currently displaying the context menu. Loop through each + activity and call `closeContextMenu' instead. */ + + for (EmacsWindowAttachmentManager.WindowConsumer consumer + : EmacsWindowAttachmentManager.MANAGER.consumers) + { + if (consumer instanceof EmacsActivity) + ((EmacsActivity) consumer).closeContextMenu (); + } } @Override @@ -646,6 +657,16 @@ public final class EmacsView extends ViewGroup return isCurrentlyTextEditor; } + @Override + public boolean + isOpaque () + { + /* Returning true here allows the system to not draw the contents + of windows underneath this view, thereby improving + performance. */ + return true; + } + public synchronized void setICMode (int icMode) { diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 30f29250970..c0197ab802c 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -67,7 +67,7 @@ public final class EmacsWindowAttachmentManager public void destroy (); }; - private List consumers; + public List consumers; public List windows; public -- cgit v1.2.1 From 45b5c9b8b72a9dd561c7e2d43ead8ce64e79b041 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 17 Mar 2023 13:10:23 +0800 Subject: Improve radio button appearance in Android menus * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu): New field `lastGroupId'. (Item): New field `isRadio'. (addItem): New arg `isRadio'. (inflateMenuItems): Apply an empty radio button group if required. * src/androidmenu.c (android_init_emacs_context_menu): Adjust accordingly. (android_menu_show): Likewise. --- java/org/gnu/emacs/EmacsContextMenu.java | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index d780641ba70..5bae41bd61d 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -55,6 +55,9 @@ public final class EmacsContextMenu /* The serial ID of the last context menu to be displayed. */ public static int lastMenuEventSerial; + /* The last group ID used for a menu item. */ + public int lastGroupId; + private static class Item implements MenuItem.OnMenuItemClickListener { public int itemID; @@ -62,6 +65,7 @@ public final class EmacsContextMenu public EmacsContextMenu subMenu; public boolean isEnabled, isCheckable, isChecked; public EmacsView inflatedView; + public boolean isRadio; @Override public boolean @@ -153,12 +157,14 @@ public final class EmacsContextMenu checkable. Likewise, if ISCHECKED is set, make the item checked. - If TOOLTIP is non-NULL, set the menu item tooltip to TOOLTIP. */ + If TOOLTIP is non-NULL, set the menu item tooltip to TOOLTIP. + + If ISRADIO, then display the check mark as a radio button. */ public void addItem (int itemID, String itemName, boolean isEnabled, boolean isCheckable, boolean isChecked, - String tooltip) + String tooltip, boolean isRadio) { Item item; @@ -169,6 +175,7 @@ public final class EmacsContextMenu item.isCheckable = isCheckable; item.isChecked = isChecked; item.tooltip = tooltip; + item.isRadio = isRadio; menuItems.add (item); } @@ -244,7 +251,11 @@ public final class EmacsContextMenu } else { - menuItem = menu.add (item.itemName); + if (item.isRadio) + menuItem = menu.add (++lastGroupId, Menu.NONE, Menu.NONE, + item.itemName); + else + menuItem = menu.add (item.itemName); menuItem.setOnMenuItemClickListener (item); /* If the item ID is zero, then disable the item. */ @@ -260,6 +271,12 @@ public final class EmacsContextMenu if (item.isChecked) menuItem.setChecked (true); + /* Define an exclusively checkable group if the item is a + radio button. */ + + if (item.isRadio) + menu.setGroupCheckable (lastGroupId, true, true); + /* If the tooltip text is set and the system is new enough to support menu item tooltips, set it on the item. */ -- cgit v1.2.1 From 634e3fcc20ea9fa5b1af59286f4b846a351f52c8 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 18 Mar 2023 10:54:26 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsView.java (EmacsView) (prepareForLayout): New function. Call this prior to mapping the view. (onGlobalLayout): New function. Register as global layout listener. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow) (notifyContentRectPosition): New function. Use specified xPosition and yPosition when reporting the offsets of children of the root window. * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (registerWindow): Specify activity launch bounds if necessary. * src/androidterm.c (handle_one_android_event): Send MOVE_FRAME_EVENT where necessary. --- java/org/gnu/emacs/EmacsView.java | 30 ++++- java/org/gnu/emacs/EmacsWindow.java | 145 ++++++++++++--------- .../gnu/emacs/EmacsWindowAttachmentManager.java | 16 ++- 3 files changed, 128 insertions(+), 63 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 681da98fa16..88d17a255a2 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -28,6 +28,7 @@ import android.view.View; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -51,6 +52,7 @@ import android.util.Log; It is also a ViewGroup, as it also lays out children. */ public final class EmacsView extends ViewGroup + implements ViewTreeObserver.OnGlobalLayoutListener { public static final String TAG = "EmacsView"; @@ -136,6 +138,9 @@ public final class EmacsView extends ViewGroup context = getContext (); tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); imManager = (InputMethodManager) tem; + + /* Add this view as its own global layout listener. */ + getViewTreeObserver ().addOnGlobalLayoutListener (this); } private void @@ -238,6 +243,13 @@ public final class EmacsView extends ViewGroup return canvas; } + public void + prepareForLayout (int wantedWidth, int wantedHeight) + { + measuredWidth = wantedWidth; + measuredHeight = wantedWidth; + } + @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) @@ -562,8 +574,7 @@ public final class EmacsView extends ViewGroup bitmapDirty = true; /* Now expose the view contents again. */ - EmacsNative.sendExpose (this.window.handle, 0, 0, - measuredWidth, measuredHeight); + EmacsNative.sendExpose (this.window.handle, 0, 0, 0, 0); super.onAttachedToWindow (); } @@ -678,4 +689,19 @@ public final class EmacsView extends ViewGroup { return icMode; } + + @Override + public void + onGlobalLayout () + { + int[] locations; + + /* Get the absolute offset of this view and specify its left and + top position in subsequent ConfigureNotify events. */ + + locations = new int[2]; + getLocationInWindow (locations); + window.notifyContentRectPosition (locations[0], + locations[1]); + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index a8d1beedef7..c14bf16b96e 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -133,6 +133,9 @@ public final class EmacsWindow extends EmacsHandleObject creating new bitmaps. */ public volatile int background; + /* The position of this window relative to the root window. */ + public int xPosition, yPosition; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height, boolean overrideRedirect) @@ -192,18 +195,14 @@ public final class EmacsWindow extends EmacsHandleObject background = pixel; } - public Rect + public synchronized Rect getGeometry () { - synchronized (this) - { - /* Huh, this is it. */ - return rect; - } + return new Rect (rect); } @Override - public void + public synchronized void destroyHandle () throws IllegalStateException { if (parent != null) @@ -258,22 +257,28 @@ public final class EmacsWindow extends EmacsHandleObject return attached; } - public long + public synchronized long 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; - } + rect.left = left; + rect.top = top; + rect.right = right; + rect.bottom = bottom; rectWidth = right - left; rectHeight = bottom - top; + /* If parent is null, use xPosition and yPosition instead of the + geometry rectangle positions. */ + + if (parent == null) + { + left = xPosition; + top = yPosition; + } + return EmacsNative.sendConfigureNotify (this.handle, System.currentTimeMillis (), left, top, rectWidth, @@ -283,8 +288,6 @@ public final class EmacsWindow extends EmacsHandleObject public void requestViewLayout () { - /* This is necessary because otherwise subsequent drawing on the - Emacs thread may be lost. */ view.explicitlyDirtyBitmap (); EmacsService.SERVICE.runOnUiThread (new Runnable () { @@ -302,35 +305,29 @@ public final class EmacsWindow extends EmacsHandleObject }); } - public void + public synchronized void resizeWindow (int width, int height) { - synchronized (this) - { - rect.right = rect.left + width; - rect.bottom = rect.top + height; + rect.right = rect.left + width; + rect.bottom = rect.top + height; - requestViewLayout (); - } + requestViewLayout (); } - public void + public synchronized void moveWindow (int x, int y) { int width, height; - synchronized (this) - { - width = rect.width (); - height = rect.height (); + width = rect.width (); + height = rect.height (); - rect.left = x; - rect.top = y; - rect.right = x + width; - rect.bottom = y + height; + rect.left = x; + rect.top = y; + rect.right = x + width; + rect.bottom = y + height; - requestViewLayout (); - } + requestViewLayout (); } private WindowManager.LayoutParams @@ -366,13 +363,17 @@ public final class EmacsWindow extends EmacsHandleObject return EmacsService.SERVICE; } - public void + public synchronized void mapWindow () { + final int width, height; + if (isMapped) return; isMapped = true; + width = rect.width (); + height = rect.height (); if (parent == null) { @@ -424,6 +425,7 @@ public final class EmacsWindow extends EmacsHandleObject /* Attach the view. */ try { + view.prepareForLayout (width, height); windowManager.addView (view, params); /* Record the window manager being used in the @@ -448,10 +450,14 @@ public final class EmacsWindow extends EmacsHandleObject public void run () { + /* Prior to mapping the view, set its measuredWidth and + measuredHeight to some reasonable value, in order to + avoid excessive bitmap dirtying. */ + + view.prepareForLayout (width, height); view.setVisibility (View.VISIBLE); if (!getDontFocusOnMap ()) - /* Eventually this should check no-focus-on-map. */ view.requestFocus (); } }); @@ -512,12 +518,9 @@ public final class EmacsWindow extends EmacsHandleObject public void clearWindow () { - synchronized (this) - { - EmacsService.SERVICE.fillRectangle (this, scratchGC, - 0, 0, rect.width (), - rect.height ()); - } + EmacsService.SERVICE.fillRectangle (this, scratchGC, + 0, 0, rect.width (), + rect.height ()); } public void @@ -1001,7 +1004,7 @@ public final class EmacsWindow extends EmacsHandleObject return false; } - public void + public synchronized void reparentTo (final EmacsWindow otherWindow, int x, int y) { int width, height; @@ -1017,15 +1020,12 @@ public final class EmacsWindow extends EmacsHandleObject 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; - } + 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. */ @@ -1081,7 +1081,7 @@ public final class EmacsWindow extends EmacsHandleObject }); } - public void + public synchronized void raise () { /* This does nothing here. */ @@ -1103,7 +1103,7 @@ public final class EmacsWindow extends EmacsHandleObject }); } - public void + public synchronized void lower () { /* This does nothing here. */ @@ -1125,17 +1125,15 @@ public final class EmacsWindow extends EmacsHandleObject }); } - public int[] + public synchronized int[] getWindowGeometry () { int[] array; - Rect rect; array = new int[4]; - rect = getGeometry (); - array[0] = rect.left; - array[1] = rect.top; + array[0] = parent != null ? rect.left : xPosition; + array[1] = parent != null ? rect.top : yPosition; array[2] = rect.width (); array[3] = rect.height (); @@ -1272,4 +1270,31 @@ public final class EmacsWindow extends EmacsHandleObject } }); } + + public synchronized void + notifyContentRectPosition (int xPosition, int yPosition) + { + Rect geometry; + + /* Ignore these notifications if not a child of the root + window. */ + if (parent != null) + return; + + /* xPosition and yPosition are the position of this window + relative to the screen. Set them and request a ConfigureNotify + event. */ + + if (this.xPosition != xPosition + || this.yPosition != yPosition) + { + this.xPosition = xPosition; + this.yPosition = yPosition; + + EmacsNative.sendConfigureNotify (this.handle, + System.currentTimeMillis (), + xPosition, yPosition, + rect.width (), rect.height ()); + } + } }; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index c0197ab802c..4fda48616f0 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -22,7 +22,9 @@ package org.gnu.emacs; import java.util.ArrayList; import java.util.List; +import android.app.ActivityOptions; import android.content.Intent; +import android.os.Build; import android.util.Log; /* Code to paper over the differences in lifecycles between @@ -102,6 +104,7 @@ public final class EmacsWindowAttachmentManager registerWindow (EmacsWindow window) { Intent intent; + ActivityOptions options; Log.d (TAG, "registerWindow (maybe): " + window); @@ -128,7 +131,18 @@ public final class EmacsWindowAttachmentManager intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - EmacsService.SERVICE.startActivity (intent); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + EmacsService.SERVICE.startActivity (intent); + else + { + /* Specify the desired window size. */ + options = ActivityOptions.makeBasic (); + options.setLaunchBounds (window.getGeometry ()); + EmacsService.SERVICE.startActivity (intent, + options.toBundle ()); + } + Log.d (TAG, "registerWindow: startActivity"); } -- cgit v1.2.1 From f3dd887d1848759baf0d91264882509159bb04fc Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 18 Mar 2023 20:05:38 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsView.java (onAttachedToWindow): Send measured width and height in exposures again. --- java/org/gnu/emacs/EmacsView.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 88d17a255a2..10c1af9e19a 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -574,7 +574,8 @@ public final class EmacsView extends ViewGroup bitmapDirty = true; /* Now expose the view contents again. */ - EmacsNative.sendExpose (this.window.handle, 0, 0, 0, 0); + EmacsNative.sendExpose (this.window.handle, 0, 0, + measuredWidth, measuredHeight); super.onAttachedToWindow (); } -- cgit v1.2.1 From 3b07a4b3158d024c6eb19ce0e7c67b799ae0d1fc Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 6 Apr 2023 09:56:23 +0800 Subject: Implement `yank-media' on Android * doc/emacs/android.texi (Android Windowing): Update selection restrictions. * java/org/gnu/emacs/EmacsClipboard.java (EmacsClipboard): New functions `getClipboardTargets' and `getClipboardData'. * java/org/gnu/emacs/EmacsSdk11Clipboard.java (EmacsSdk11Clipboard, getClipboardTargets, getClipboardData): Implement. * java/org/gnu/emacs/EmacsSdk8Clipboard.java: Stub out new functions. * lisp/term/android-win.el (android-get-clipboard-1): Implement MIME type targets. * src/android.c (android_exception_check) (android_exception_check_1, android_exception_check_2): Fix punctuation in warning message. (android_exception_check_nonnull_1): New function. * src/android.h: Update prototypes. * src/androidselect.c (struct android_emacs_clipboard): New methods. (android_init_emacs_clipboard): Initialize new methods. (Fandroid_get_clipboard_targets, android_xfree_inside) (Fandroid_get_clipboard_data): New functions. (syms_of_androidselect): Define new subrs. --- java/org/gnu/emacs/EmacsClipboard.java | 3 + java/org/gnu/emacs/EmacsSdk11Clipboard.java | 130 ++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsSdk8Clipboard.java | 29 +++++++ 3 files changed, 162 insertions(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsClipboard.java b/java/org/gnu/emacs/EmacsClipboard.java index cd6bcebfe0e..5cd48af6e3a 100644 --- a/java/org/gnu/emacs/EmacsClipboard.java +++ b/java/org/gnu/emacs/EmacsClipboard.java @@ -31,6 +31,9 @@ public abstract class EmacsClipboard public abstract boolean clipboardExists (); public abstract byte[] getClipboard (); + public abstract byte[][] getClipboardTargets (); + public abstract long[] getClipboardData (byte[] target); + /* Create the correct kind of clipboard for this system. */ public static EmacsClipboard diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java index a05184513cd..4959ec36eed 100644 --- a/java/org/gnu/emacs/EmacsSdk11Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java @@ -21,12 +21,20 @@ package org.gnu.emacs; import android.content.ClipboardManager; import android.content.Context; +import android.content.ContentResolver; import android.content.ClipData; +import android.content.ClipDescription; + +import android.content.res.AssetFileDescriptor; + +import android.net.Uri; import android.util.Log; import android.os.Build; +import java.io.FileNotFoundException; +import java.io.IOException; import java.io.UnsupportedEncodingException; /* This class implements EmacsClipboard for Android 3.0 and later @@ -40,6 +48,7 @@ public final class EmacsSdk11Clipboard extends EmacsClipboard private boolean ownsClipboard; private int clipboardChangedCount; private int monitoredClipboardChangedCount; + private ContentResolver resolver; public EmacsSdk11Clipboard () @@ -51,6 +60,11 @@ public final class EmacsSdk11Clipboard extends EmacsClipboard if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) manager.addPrimaryClipChangedListener (this); + + /* Now obtain the content resolver used to open file + descriptors. */ + + resolver = EmacsService.SERVICE.getContentResolver (); } @Override @@ -157,4 +171,120 @@ public final class EmacsSdk11Clipboard extends EmacsClipboard return null; } + + /* Return an array of targets currently provided by the + clipboard, or NULL if there are none. */ + + @Override + public byte[][] + getClipboardTargets () + { + ClipData clip; + ClipDescription description; + byte[][] typeArray; + int i; + + /* N.B. that Android calls the clipboard the ``primary clip''; it + is not related to the X primary selection. */ + clip = manager.getPrimaryClip (); + description = clip.getDescription (); + i = description.getMimeTypeCount (); + typeArray = new byte[i][i]; + + try + { + for (i = 0; i < description.getMimeTypeCount (); ++i) + typeArray[i] = description.getMimeType (i).getBytes ("UTF-8"); + } + catch (UnsupportedEncodingException exception) + { + return null; + } + + return typeArray; + } + + /* Return the clipboard data for the given target, or NULL if it + does not exist. + + Value is normally an array of three longs: the file descriptor, + the start offset of the data, and its length; length may be + AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends + from that offset to the end of the file. + + Do not use this function to open text targets; use `getClipboard' + for that instead, as it will handle selection data consisting + solely of a URI. */ + + @Override + public long[] + getClipboardData (byte[] target) + { + ClipData data; + String mimeType; + int fd; + AssetFileDescriptor assetFd; + Uri uri; + long[] value; + + /* Decode the target given by Emacs. */ + try + { + mimeType = new String (target, "UTF-8"); + } + catch (UnsupportedEncodingException exception) + { + return null; + } + + Log.d (TAG, "getClipboardData: "+ mimeType); + + /* Now obtain the clipboard data and the data corresponding to + that MIME type. */ + + data = manager.getPrimaryClip (); + + if (data.getItemCount () < 1) + return null; + + try + { + uri = data.getItemAt (0).getUri (); + + if (uri == null) + return null; + + Log.d (TAG, "getClipboardData: "+ uri); + + /* Now open the file descriptor. */ + assetFd = resolver.openTypedAssetFileDescriptor (uri, mimeType, + null); + + /* Duplicate the file descriptor. */ + fd = assetFd.getParcelFileDescriptor ().getFd (); + fd = EmacsNative.dup (fd); + + /* Return the relevant information. */ + value = new long[] { fd, assetFd.getStartOffset (), + assetFd.getLength (), }; + + /* Close the original offset. */ + assetFd.close (); + + Log.d (TAG, "getClipboardData: "+ value); + } + catch (FileNotFoundException e) + { + return null; + } + catch (IOException e) + { + return null; + } + + /* Don't return value if the file descriptor couldn't be + created. */ + + return fd != -1 ? value : null; + } }; diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java index 5a40128b0ac..9622641810f 100644 --- a/java/org/gnu/emacs/EmacsSdk8Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java @@ -115,4 +115,33 @@ public final class EmacsSdk8Clipboard extends EmacsClipboard return null; } + + /* Return an array of targets currently provided by the + clipboard, or NULL if there are none. */ + + @Override + public byte[][] + getClipboardTargets () + { + return null; + } + + /* Return the clipboard data for the given target, or NULL if it + does not exist. + + Value is normally an array of three longs: the file descriptor, + the start offset of the data, and its length; length may be + AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends + from that offset to the end of the file. + + Do not use this function to open text targets; use `getClipboard' + for that instead, as it will handle selection data consisting + solely of a URI. */ + + @Override + public long[] + getClipboardData (byte[] target) + { + return null; + } }; -- cgit v1.2.1 From 3198b7dc565e0e41d40b6c23509c285e96044a83 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 6 May 2023 11:32:56 +0800 Subject: Update Android port * cross/verbose.mk.android: Get rid of badly aligned ANDROID_CC messages. * java/org/gnu/emacs/EmacsInputConnection.java (syncAfterCommit) (extractAbsoluteOffsets): Add workarounds for several kinds of machines. (commitText, getExtractedText): Likewise. * src/textconv.c (really_commit_text): Improve definition of POSITION. (get_extracted_text): Default to providing at least 4 characters. --- java/org/gnu/emacs/EmacsInputConnection.java | 87 ++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 5 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 7b40fcff99e..d13b48288ce 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -26,6 +26,8 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.TextSnapshot; import android.view.KeyEvent; +import android.os.Build; + import android.util.Log; /* Android input methods, take number six. See textconv.c for more @@ -37,6 +39,34 @@ public final class EmacsInputConnection extends BaseInputConnection private EmacsView view; private short windowHandle; + /* Whether or not to synchronize and call `updateIC' with the + selection position after committing text. + + This helps with on screen keyboard programs found in some vendor + versions of Android, which rely on immediate updates to the point + position after text is commited in order to place the cursor + within that text. */ + + private static boolean syncAfterCommit; + + /* Whether or not to return empty text with the offset set to zero + if a request arrives that has no flags set and has requested no + characters at all. + + This is necessary with on screen keyboard programs found in some + vendor versions of Android which don't rely on the documented + meaning of `ExtractedText.startOffset', and instead take the + selection offset inside at face value. */ + + private static boolean extractAbsoluteOffsets; + + static + { + if (Build.MANUFACTURER.equalsIgnoreCase ("Huawei") + || Build.MANUFACTURER.equalsIgnoreCase ("Honor")) + extractAbsoluteOffsets = syncAfterCommit = true; + }; + public EmacsInputConnection (EmacsView view) { @@ -85,11 +115,32 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean commitText (CharSequence text, int newCursorPosition) { + int[] selection; + if (EmacsService.DEBUG_IC) Log.d (TAG, "commitText: " + text + " " + newCursorPosition); EmacsNative.commitText (windowHandle, text.toString (), newCursorPosition); + + if (syncAfterCommit) + { + /* Synchronize with the Emacs thread, obtain the new + selection, and report it immediately. */ + + selection = EmacsNative.getSelection (windowHandle); + + if (EmacsService.DEBUG_IC && selection != null) + Log.d (TAG, "commitText: new selection is " + selection[0] + + ", by " + selection[1]); + + if (selection != null) + /* N.B. that the composing region is removed after text is + committed. */ + view.imManager.updateSelection (view, selection[0], + selection[1], -1, -1); + } + return true; } @@ -203,16 +254,42 @@ public final class EmacsInputConnection extends BaseInputConnection getExtractedText (ExtractedTextRequest request, int flags) { ExtractedText text; + int[] selection; if (EmacsService.DEBUG_IC) - Log.d (TAG, "getExtractedText: " + request + " " + flags); - - text = EmacsNative.getExtractedText (windowHandle, request, - flags); + Log.d (TAG, "getExtractedText: " + request.hintMaxChars + ", " + + request.hintMaxLines + " " + flags); + + /* If a request arrives with hintMaxChars, hintMaxLines and flags + set to 0, and the system is known to be buggy, return an empty + extracted text object with the absolute selection positions. */ + + if (extractAbsoluteOffsets + && request.hintMaxChars == 0 + && request.hintMaxLines == 0 + && flags == 0) + { + /* Obtain the selection. */ + selection = EmacsNative.getSelection (windowHandle); + if (selection == null) + return null; + + /* Create the workaround extracted text. */ + text = new ExtractedText (); + text.partialStartOffset = -1; + text.partialEndOffset = -1; + text.text = ""; + text.selectionStart = selection[0]; + text.selectionEnd = selection[1]; + } + else + text = EmacsNative.getExtractedText (windowHandle, request, + flags); if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + text.text + " @" - + text.startOffset + ":" + text.selectionStart); + + text.startOffset + ":" + text.selectionStart + + ", " + text.selectionEnd); return text; } -- cgit v1.2.1 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 ++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) (limited to 'java/org/gnu') 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. -- cgit v1.2.1 From 841b0e220111869fcaf26468d88c7dd18e3caac1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 14 May 2023 11:12:54 +0800 Subject: Implement document moving on Android * java/org/gnu/emacs/EmacsDocumentsProvider.java (notifyChangeByName): New function. (queryDocument1): Set FLAG_SUPPORTS_MOVE where necessary. (moveDocument): Implement new function. --- java/org/gnu/emacs/EmacsDocumentsProvider.java | 114 ++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index a92a1a5d330..b4ac4624829 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -38,7 +38,9 @@ import android.webkit.MimeTypeMap; import android.net.Uri; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; /* ``Documents provider''. This allows Emacs's home directory to be @@ -155,6 +157,22 @@ public final class EmacsDocumentsProvider extends DocumentsProvider context.getContentResolver ().notifyChange (updatedUri, null); } + /* Inform the system that FILE's contents (or FILE itself) has + changed. FILE is a string describing containing the file name of + a directory as opposed to a File. */ + + private void + notifyChangeByName (String file) + { + Uri updatedUri; + Context context; + + context = getContext (); + updatedUri + = buildChildDocumentsUri ("org.gnu.emacs", file); + context.getContentResolver ().notifyChange (updatedUri, null); + } + /* Return the MIME type of a file FILE. */ private String @@ -212,6 +230,9 @@ public final class EmacsDocumentsProvider extends DocumentsProvider if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) flags |= Document.FLAG_SUPPORTS_RENAME; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + flags |= Document.FLAG_SUPPORTS_MOVE; } } else if (file.canWrite ()) @@ -224,7 +245,10 @@ public final class EmacsDocumentsProvider extends DocumentsProvider flags |= Document.FLAG_SUPPORTS_RENAME; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - flags |= Document.FLAG_SUPPORTS_REMOVE; + { + flags |= Document.FLAG_SUPPORTS_REMOVE; + flags |= Document.FLAG_SUPPORTS_MOVE; + } } displayName = file.getName (); @@ -460,4 +484,92 @@ public final class EmacsDocumentsProvider extends DocumentsProvider { return documentId.startsWith (parentDocumentId); } + + @Override + public String + moveDocument (String sourceDocumentId, + String sourceParentDocumentId, + String targetParentDocumentId) + throws FileNotFoundException + { + File file, newName; + FileInputStream inputStream; + FileOutputStream outputStream; + byte buffer[]; + int length; + + file = new File (sourceDocumentId); + + /* Now, create the file name of the parent document. */ + newName = new File (targetParentDocumentId, + file.getName ()); + + /* Try to perform a simple rename, before falling back to + copying. */ + + if (file.renameTo (newName)) + { + notifyChangeByName (file.getParent ()); + notifyChangeByName (targetParentDocumentId); + return newName.getAbsolutePath (); + } + + /* If that doesn't work, create the new file and copy over the old + file's contents. */ + + inputStream = null; + outputStream = null; + + try + { + if (!newName.createNewFile () + || !newName.setWritable (true) + || !newName.setReadable (true)) + throw new FileNotFoundException ("failed to create new file"); + + /* Open the file in preparation for a copy. */ + + inputStream = new FileInputStream (file); + outputStream = new FileOutputStream (newName); + + /* Allocate the buffer used to hold data. */ + + buffer = new byte[4096]; + + while ((length = inputStream.read (buffer)) > 0) + outputStream.write (buffer, 0, length); + } + catch (IOException e) + { + throw new FileNotFoundException ("IOException: " + e); + } + finally + { + try + { + if (inputStream != null) + inputStream.close (); + } + catch (IOException e) + { + + } + + try + { + if (outputStream != null) + outputStream.close (); + } + catch (IOException e) + { + + } + } + + file.delete (); + notifyChangeByName (file.getParent ()); + notifyChangeByName (targetParentDocumentId); + + return newName.getAbsolutePath (); + } } -- cgit v1.2.1 From 181453285c67783ebf8eb269dc19fdb0e563af62 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 20 May 2023 10:26:28 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsView.java (swapBuffers): Restore missing damage rect code. (onDetachedFromWindow): Remove redundant synchronization. --- java/org/gnu/emacs/EmacsView.java | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 10c1af9e19a..124ea5301bb 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -356,16 +356,23 @@ public final class EmacsView extends ViewGroup damageRect = null; + /* Now see if there is a damage region. */ + synchronized (damageRegion) { if (damageRegion.isEmpty ()) return; + /* And extract and clear the damage region. */ + + damageRect = damageRegion.getBounds (); + damageRegion.setEmpty (); + bitmap = getBitmap (); /* Transfer the bitmap to the surface view, then invalidate it. */ - surfaceView.setBitmap (bitmap, damageRect); + surfaceView.setBitmap (bitmap, damageRect); } } @@ -545,20 +552,17 @@ public final class EmacsView extends ViewGroup { isAttachedToWindow = false; - synchronized (this) - { - /* Recycle the bitmap and call GC. */ + /* Recycle the bitmap and call GC. */ - if (bitmap != null) - bitmap.recycle (); + if (bitmap != null) + bitmap.recycle (); - bitmap = null; - canvas = null; - surfaceView.setBitmap (null, null); + bitmap = null; + canvas = null; + surfaceView.setBitmap (null, null); - /* Collect the bitmap storage; it could be large. */ - Runtime.getRuntime ().gc (); - } + /* Collect the bitmap storage; it could be large. */ + Runtime.getRuntime ().gc (); super.onDetachedFromWindow (); } -- cgit v1.2.1 From 0eb1f4e57125117006f109a5549082008fc9fbb1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 26 May 2023 15:20:39 +0800 Subject: Allow starting Emacs --debug-init on Android * doc/emacs/android.texi (Android Troubleshooting): Document `debug-init' option. * java/AndroidManifest.xml.in (EmacsLauncherPreferencesActivity): New activity. Export on systems older than Android 7.0. * java/org/gnu/emacs/EmacsActivity.java (onCreate): Adjust for string startup argument. * java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java: New file. * java/org/gnu/emacs/EmacsPreferencesActivity.java (EmacsPreferencesActivity): Don't make final. (startEmacsQ): Give start-up argument as an argument, not as a boolean. (startEmacsDebugInit): New function. (onCreate): Register new listener; make final. * java/org/gnu/emacs/EmacsService.java (onCreate): Pass extraStartupArgument. * java/org/gnu/emacs/EmacsThread.java (EmacsThread): Rename startDashQ to extraStartupArgument. (run): Adjust accordingly. * java/res/values-v24/bool.xml: * java/res/values/bool.xml: * java/res/values/strings.xml: New files. * java/res/xml/preferences.xml: Add new option. Move string resources around. --- java/org/gnu/emacs/EmacsActivity.java | 8 ++--- .../emacs/EmacsLauncherPreferencesActivity.java | 31 ++++++++++++++++ java/org/gnu/emacs/EmacsPreferencesActivity.java | 41 ++++++++++++++++++---- java/org/gnu/emacs/EmacsService.java | 7 ++-- java/org/gnu/emacs/EmacsThread.java | 19 +++++----- 5 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index b480fc40b2e..7ba268ba42d 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -193,11 +193,11 @@ public class EmacsActivity extends Activity ViewTreeObserver observer; int matchParent; - /* See if Emacs should be started with -Q. */ + /* See if Emacs should be started with any extra arguments, such + as `--quick'. */ intent = getIntent (); - EmacsService.needDashQ - = intent.getBooleanExtra ("org.gnu.emacs.START_DASH_Q", - false); + EmacsService.extraStartupArgument + = intent.getStringExtra ("org.gnu.emacs.STARTUP_ARGUMENT"); matchParent = FrameLayout.LayoutParams.MATCH_PARENT; params diff --git a/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java b/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java new file mode 100644 index 00000000000..1e1e5d97631 --- /dev/null +++ b/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java @@ -0,0 +1,31 @@ +/* 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; + +/* This class only exists because EmacsPreferencesActivity is already + defined as an activity, the system wants a new class in order to + define a new activity, and only activities can be enabled or + disabled per the API level of the host. */ + +public final class EmacsLauncherPreferencesActivity + extends EmacsPreferencesActivity +{ + +} diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index 70934fa4bd4..7e67cc3679b 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -42,10 +42,11 @@ import android.preference.*; Unfortunately, there is no alternative that looks the same way. */ @SuppressWarnings ("deprecation") -public final class EmacsPreferencesActivity extends PreferenceActivity +public 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. */ + /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now, + and tell the system to start EmacsActivity with some parameters + later. */ private void startEmacsQ () @@ -55,7 +56,24 @@ public final class EmacsPreferencesActivity extends PreferenceActivity 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); + intent.putExtra ("org.gnu.emacs.STARTUP_ARGUMENT", "--quick"); + startActivity (intent); + System.exit (0); + } + + /* Restart Emacs with `--debug-init'. Call EmacsThread.exit to kill + Emacs now, and tell the system to EmacsActivity with some + parameters later. */ + + private void + startEmacsDebugInit () + { + 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.STARTUP_ARGUMENT", "--debug-init"); startActivity (intent); System.exit (0); } @@ -89,7 +107,7 @@ public final class EmacsPreferencesActivity extends PreferenceActivity } @Override - public void + public final void onCreate (Bundle savedInstanceState) { Preference tem; @@ -111,7 +129,6 @@ public final class EmacsPreferencesActivity extends PreferenceActivity items. */ tem = findPreference ("start_quick"); - listener = new Preference.OnPreferenceClickListener () { @Override public boolean @@ -123,9 +140,19 @@ public final class EmacsPreferencesActivity extends PreferenceActivity }; tem.setOnPreferenceClickListener (listener); + tem = findPreference ("start_debug_init"); + listener = new Preference.OnPreferenceClickListener () { + @Override + public boolean + onPreferenceClick (Preference preference) + { + startEmacsDebugInit (); + return true; + } + }; + tem.setOnPreferenceClickListener (listener); tem = findPreference ("erase_dump"); - listener = new Preference.OnPreferenceClickListener () { @Override public boolean diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 30ef71540a9..bb17d27bcf8 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -79,7 +79,10 @@ public final class EmacsService extends Service { public static final String TAG = "EmacsService"; public static volatile EmacsService SERVICE; - public static boolean needDashQ; + + /* If non-NULL, an extra argument to pass to + `android_emacs_init'. */ + public static String extraStartupArgument; private EmacsThread thread; private Handler handler; @@ -231,7 +234,7 @@ public final class EmacsService extends Service (float) pixelDensityY, classPath, EmacsService.this); } - }, needDashQ, + }, extraStartupArgument, /* If any file needs to be opened, open it now. */ EmacsOpenActivity.fileToOpen); thread.start (); diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index d175fe332b5..468c6530af0 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -29,8 +29,9 @@ public class EmacsThread extends Thread { private static final String TAG = "EmacsThread"; - /* Whether or not Emacs should be started -Q. */ - private boolean startDashQ; + /* Whether or not Emacs should be started with an additional + argument, and that additional argument if non-NULL. */ + private String extraStartupArgument; /* Runnable run to initialize Emacs. */ private Runnable paramsClosure; @@ -40,10 +41,10 @@ public class EmacsThread extends Thread public EmacsThread (EmacsService service, Runnable paramsClosure, - boolean startDashQ, String fileToOpen) + String extraStartupArgument, String fileToOpen) { super ("Emacs main thread"); - this.startDashQ = startDashQ; + this.extraStartupArgument = extraStartupArgument; this.paramsClosure = paramsClosure; this.fileToOpen = fileToOpen; } @@ -56,18 +57,20 @@ public class EmacsThread extends Thread if (fileToOpen == null) { - if (!startDashQ) + if (extraStartupArgument == null) args = new String[] { "libandroid-emacs.so", }; else - args = new String[] { "libandroid-emacs.so", "-Q", }; + args = new String[] { "libandroid-emacs.so", + extraStartupArgument, }; } else { - if (!startDashQ) + if (extraStartupArgument != null) args = new String[] { "libandroid-emacs.so", fileToOpen, }; else - args = new String[] { "libandroid-emacs.so", "-Q", + args = new String[] { "libandroid-emacs.so", + extraStartupArgument, fileToOpen, }; } -- cgit v1.2.1 From d33bf0a0afddb76598aa020f68402d0e19cfb65c Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 27 May 2023 15:19:02 +0800 Subject: Remove synchronization around `damageRegion' * java/org/gnu/emacs/EmacsView.java (EmacsView, swapBuffers): Remove unnecessary documentation. `damageRegion' is only changed from the Emacs thread. --- java/org/gnu/emacs/EmacsView.java | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 124ea5301bb..eb1d88ae242 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -338,10 +338,7 @@ public final class EmacsView extends ViewGroup public void damageRect (Rect damageRect) { - synchronized (damageRegion) - { - damageRegion.union (damageRect); - } + damageRegion.union (damageRect); } /* This method is called from both the UI thread and the Emacs @@ -358,22 +355,19 @@ public final class EmacsView extends ViewGroup /* Now see if there is a damage region. */ - synchronized (damageRegion) - { - if (damageRegion.isEmpty ()) - return; + if (damageRegion.isEmpty ()) + return; - /* And extract and clear the damage region. */ + /* And extract and clear the damage region. */ - damageRect = damageRegion.getBounds (); - damageRegion.setEmpty (); + damageRect = damageRegion.getBounds (); + damageRegion.setEmpty (); - bitmap = getBitmap (); + bitmap = getBitmap (); - /* Transfer the bitmap to the surface view, then invalidate - it. */ - surfaceView.setBitmap (bitmap, damageRect); - } + /* Transfer the bitmap to the surface view, then invalidate + it. */ + surfaceView.setBitmap (bitmap, damageRect); } @Override -- cgit v1.2.1 From 327d2d013130ec745880b44573dcda3a6620faba Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 27 May 2023 16:30:12 +0800 Subject: Add extra thread-related checking * java/org/gnu/emacs/EmacsService.java (EmacsService) (checkEmacsThread): New function. (fillPolygon, drawRectangle, drawLine, drawPoint, copyArea) (clearArea): * java/org/gnu/emacs/EmacsThread.java (EmacsThread): * java/org/gnu/emacs/EmacsView.java (EmacsView, swapBuffers): Call where appropriate. --- java/org/gnu/emacs/EmacsService.java | 30 ++++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsThread.java | 2 +- java/org/gnu/emacs/EmacsView.java | 5 +++++ 3 files changed, 36 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index bb17d27bcf8..d2e52ed5e62 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -100,6 +100,10 @@ public final class EmacsService extends Service information. */ public static final boolean DEBUG_IC = false; + /* Flag that says whether or not to perform extra checks on threads + performing drawing calls. */ + private static final boolean DEBUG_THREADS = false; + /* Return the directory leading to the directory in which native library files are stored on behalf of CONTEXT. */ @@ -309,10 +313,29 @@ public final class EmacsService extends Service syncRunnable (runnable); } + + + public static void + checkEmacsThread () + { + if (DEBUG_THREADS) + { + if (Thread.currentThread () instanceof EmacsThread) + return; + + throw new RuntimeException ("Emacs thread function" + + " called from other thread!"); + } + } + + /* These drawing functions must only be called from the Emacs + thread. */ + public void fillRectangle (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { + checkEmacsThread (); EmacsFillRectangle.perform (drawable, gc, x, y, width, height); } @@ -321,6 +344,7 @@ public final class EmacsService extends Service fillPolygon (EmacsDrawable drawable, EmacsGC gc, Point points[]) { + checkEmacsThread (); EmacsFillPolygon.perform (drawable, gc, points); } @@ -328,6 +352,7 @@ public final class EmacsService extends Service drawRectangle (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { + checkEmacsThread (); EmacsDrawRectangle.perform (drawable, gc, x, y, width, height); } @@ -336,6 +361,7 @@ public final class EmacsService extends Service drawLine (EmacsDrawable drawable, EmacsGC gc, int x, int y, int x2, int y2) { + checkEmacsThread (); EmacsDrawLine.perform (drawable, gc, x, y, x2, y2); } @@ -344,6 +370,7 @@ public final class EmacsService extends Service drawPoint (EmacsDrawable drawable, EmacsGC gc, int x, int y) { + checkEmacsThread (); EmacsDrawPoint.perform (drawable, gc, x, y); } @@ -353,6 +380,7 @@ public final class EmacsService extends Service int srcX, int srcY, int width, int height, int destX, int destY) { + checkEmacsThread (); EmacsCopyArea.perform (srcDrawable, gc, dstDrawable, srcX, srcY, width, height, destX, destY); @@ -361,6 +389,7 @@ public final class EmacsService extends Service public void clearWindow (EmacsWindow window) { + checkEmacsThread (); window.clearWindow (); } @@ -368,6 +397,7 @@ public final class EmacsService extends Service clearArea (EmacsWindow window, int x, int y, int width, int height) { + checkEmacsThread (); window.clearArea (x, y, width, height); } diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 468c6530af0..1343b70bf5a 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -25,7 +25,7 @@ import java.util.Arrays; import android.os.Build; import android.util.Log; -public class EmacsThread extends Thread +public final class EmacsThread extends Thread { private static final String TAG = "EmacsThread"; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index eb1d88ae242..09bc9d719d3 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -338,6 +338,7 @@ public final class EmacsView extends ViewGroup public void damageRect (Rect damageRect) { + EmacsService.checkEmacsThread (); damageRegion.union (damageRect); } @@ -351,6 +352,10 @@ public final class EmacsView extends ViewGroup Rect damageRect; Bitmap bitmap; + /* Make sure this function is called only from the Emacs + thread. */ + EmacsService.checkEmacsThread (); + damageRect = null; /* Now see if there is a damage region. */ -- cgit v1.2.1 From 00671b18438fec8f2e7f774cb3fd4cd6f694fd18 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 29 May 2023 15:44:14 +0800 Subject: Implement android_copy_area in C * java/org/gnu/emacs/EmacsCopyArea.java: Remove file. * java/org/gnu/emacs/EmacsService.java (EmacsService, copyArea): Delete function. * src/android.c (struct android_emacs_service) (android_init_emacs_service): Remove `copy_area'. (android_create_gc, android_change_gc, android_get_gc_values): Record new GC values. (android_neon_mask_line): New function. (android_blit_copy, android_blit_xor): New functions. (android_copy_area): Implement in C. (android_lock_bitmap): Accept drawables instead of windows. * src/android.h: Adjust prototype for `android_lock_bitmap'. * src/androidgui.h (struct android_gc): Record last known GC values. --- java/org/gnu/emacs/EmacsCopyArea.java | 206 ---------------------------------- java/org/gnu/emacs/EmacsService.java | 12 -- 2 files changed, 218 deletions(-) delete mode 100644 java/org/gnu/emacs/EmacsCopyArea.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java deleted file mode 100644 index f69b0cde866..00000000000 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ /dev/null @@ -1,206 +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.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 EmacsCopyArea -{ - private static Xfermode overAlu; - - static - { - overAlu = new PorterDuffXfermode (Mode.SRC_OVER); - }; - - private static void - insetRectBy (Rect rect, int left, int top, int right, - int bottom) - { - rect.left += left; - rect.top += top; - rect.right -= right; - rect.bottom -= bottom; - } - - 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 i; - Bitmap bitmap; - Paint maskPaint, paint; - Canvas maskCanvas, canvas; - Bitmap srcBitmap, maskBitmap, clipBitmap; - Rect rect, maskRect, srcRect, dstRect, maskDestRect; - boolean needFill; - - /* TODO implement stippling. */ - if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) - return; - - paint = gc.gcPaint; - - canvas = destination.lockCanvas (gc); - - if (canvas == null) - return; - - /* 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; - - /* If width and height are empty or negative, then skip the entire - CopyArea operation lest createBitmap throw an exception. */ - - if (width <= 0 || height <= 0) - return; - - rect = new Rect (dest_x, dest_y, dest_x + width, - dest_y + height); - - if (gc.clip_mask == null) - { - if (source == destination) - { - /* Create a copy of the bitmap, since Android can't handle - overlapping copies. */ - bitmap = Bitmap.createBitmap (srcBitmap, - src_x, src_y, width, - height); - canvas.drawBitmap (bitmap, null, rect, paint); - bitmap.recycle (); - } - else - { - /* But here the bitmaps are known to not overlap, so avoid - that extra consing overhead. */ - - srcRect = new Rect (src_x, src_y, src_x + width, - src_y + height); - canvas.drawBitmap (srcBitmap, srcRect, rect, paint); - } - } - else - { - /* 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 (dest_x, dest_y, - dest_x + width, - dest_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; - - /* 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); - 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.setXfermode (EmacsGC.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); - gc.resetXfermode (); - - /* Recycle this unused bitmap. */ - maskBitmap.recycle (); - } - - destination.damageRect (rect); - } -} diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index d2e52ed5e62..546d22627c5 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -374,18 +374,6 @@ public final class EmacsService extends Service EmacsDrawPoint.perform (drawable, gc, x, y); } - public void - copyArea (EmacsDrawable srcDrawable, EmacsDrawable dstDrawable, - EmacsGC gc, - int srcX, int srcY, int width, int height, int destX, - int destY) - { - checkEmacsThread (); - EmacsCopyArea.perform (srcDrawable, gc, dstDrawable, - srcX, srcY, width, height, destX, - destY); - } - public void clearWindow (EmacsWindow window) { -- cgit v1.2.1 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 ++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) (limited to 'java/org/gnu') 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 -- cgit v1.2.1 From 57903519eb61632c4a85fbaf420109892955079a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 31 May 2023 10:13:04 +0800 Subject: Update Android port * java/debug.sh (is_root): Go back to using unix sockets; allow adb to forward them correctly. * java/org/gnu/emacs/EmacsInputConnection.java (getExtractedText): Don't print text if NULL. * java/org/gnu/emacs/EmacsService.java (EmacsService): New field `imSyncInProgress'. (updateIC): If an IM sync might be in progress, avoid deadlocks. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Set `imSyncInProgress' across synchronization point. * src/android.c (android_check_query): Use __atomic_store_n. (android_answer_query): New function. (android_begin_query): Set `android_servicing_query' to 2. Check once, and don't spin waiting for query to complete. (android_end_query): Use __atomic_store_n. (android_run_in_emacs_thread): Compare-and-exchange flag. If originally 1, fail. * src/textconv.c (really_set_composing_text): Clear conversion region if text is empty. --- java/org/gnu/emacs/EmacsInputConnection.java | 8 ++++++++ java/org/gnu/emacs/EmacsService.java | 28 ++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsView.java | 8 +++++++- 3 files changed, 43 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 21bbaca5d07..420da58c0f8 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -286,6 +286,14 @@ public final class EmacsInputConnection extends BaseInputConnection text = EmacsNative.getExtractedText (windowHandle, request, flags); + if (text == null) + { + if (EmacsService.DEBUG_IC) + Log.d (TAG, "getExtractedText: text is NULL"); + + return null; + } + if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + text.text + " @" + text.startOffset + ":" + text.selectionStart diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 546d22627c5..2f35933a7d1 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -104,6 +104,9 @@ 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; + /* Return the directory leading to the directory in which native library files are stored on behalf of CONTEXT. */ @@ -636,16 +639,41 @@ public final class EmacsService extends Service 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; + } + window.view.imManager.updateSelection (window.view, newSelectionStart, newSelectionEnd, composingRegionStart, composingRegionEnd); + + if (wasSynchronous) + EmacsNative.endSynchronous (); } public void diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 09bc9d719d3..bb450bb8e6b 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -628,8 +628,14 @@ public final class EmacsView extends ViewGroup } /* Obtain the current position of point and set it as the - selection. */ + 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. */ + + EmacsService.imSyncInProgress = true; selection = EmacsNative.getSelection (window.handle); + EmacsService.imSyncInProgress = false; if (selection != null) Log.d (TAG, "onCreateInputConnection: current selection is: " -- cgit v1.2.1 From 9a958c59a2ce546e6ec99c58ca181dafeac8dd6b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 1 Jun 2023 10:05:42 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): Add compatibility adjustments for Samsung devices. --- java/org/gnu/emacs/EmacsInputConnection.java | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 420da58c0f8..54c98d950aa 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -65,6 +65,13 @@ public final class EmacsInputConnection extends BaseInputConnection if (Build.MANUFACTURER.equalsIgnoreCase ("Huawei") || Build.MANUFACTURER.equalsIgnoreCase ("Honor")) extractAbsoluteOffsets = syncAfterCommit = true; + + /* The Samsung keyboard takes `selectionStart' at face value if + some text is returned, and also searches for words solely + within that text. However, when no text is returned, it falls + back to getTextAfterCursor and getTextBeforeCursor. */ + if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung")) + extractAbsoluteOffsets = true; }; public -- cgit v1.2.1 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 ++ 2 files changed, 48 insertions(+) (limited to 'java/org/gnu') 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); -- cgit v1.2.1 From 0014a10b242bac62b0ba913a5ee4da4cfbe07f41 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 1 Jun 2023 16:31:50 +0800 Subject: Correctly export file:// URIs on Android * java/org/gnu/emacs/EmacsService.java (browseUrl): If uri's scheme is `file', rewrite it into a content URI. --- java/org/gnu/emacs/EmacsService.java | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 2f35933a7d1..dde60e1c5af 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -62,6 +62,8 @@ import android.os.Vibrator; import android.os.VibratorManager; import android.os.VibrationEffect; +import android.provider.DocumentsContract; + import android.util.Log; import android.util.DisplayMetrics; @@ -546,11 +548,33 @@ public final class EmacsService extends Service browseUrl (String url) { Intent intent; + Uri uri; try { - intent = new Intent (Intent.ACTION_VIEW, Uri.parse (url)); - intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + /* Parse the URI. */ + uri = Uri.parse (url); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + { + /* On Android 4.4 and later, check if URI is actually a + file name. If so, rewrite it into a content provider + URI, so that it can be accessed by other programs. */ + + if (uri.getScheme ().equals ("file") + && uri.getPath () != null) + uri + = DocumentsContract.buildDocumentUri ("org.gnu.emacs", + uri.getPath ()); + } + + Log.d (TAG, ("browseUri: browsing " + url + + " --> " + uri.getPath () + + " --> " + uri)); + + intent = new Intent (Intent.ACTION_VIEW, uri); + intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity (intent); } catch (Exception e) -- cgit v1.2.1 From 189a91bfb699babd936dae48b96d71a332cac8d2 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 2 Jun 2023 13:31:40 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): Apply workarounds on Vivo devices as well. * src/android.c (sendKeyPress, sendKeyRelease): Clear counter. * src/androidgui.h (struct android_key_event): New field `counter'. * src/androidterm.c (handle_one_android_event): Generate barriers as appropriate. (JNICALL): Set `counter'. * src/frame.h (enum text_conversion_operation): * src/textconv.c (detect_conversion_events) (really_set_composing_text, handle_pending_conversion_events_1) (handle_pending_conversion_events, textconv_barrier): * src/textconv.h: Implement text conversion barriers and fix various typos. --- java/org/gnu/emacs/EmacsInputConnection.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index eb6fd5f2763..9ced7cb7aaf 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -66,11 +66,12 @@ public final class EmacsInputConnection extends BaseInputConnection || Build.MANUFACTURER.equalsIgnoreCase ("Honor")) extractAbsoluteOffsets = syncAfterCommit = true; - /* The Samsung keyboard takes `selectionStart' at face value if - some text is returned, and also searches for words solely - within that text. However, when no text is returned, it falls - back to getTextAfterCursor and getTextBeforeCursor. */ - if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung")) + /* The Samsung and Vivo keyboards take `selectionStart' at face + value if some text is returned, and also searches for words + solely within that text. However, when no text is returned, it + falls back to getTextAfterCursor and getTextBeforeCursor. */ + if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung") + || Build.MANUFACTURER.equalsIgnoreCase ("Vivo")) extractAbsoluteOffsets = true; }; -- cgit v1.2.1 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/org/gnu/emacs/EmacsNative.java | 6 ++ java/org/gnu/emacs/EmacsService.java | 103 ++++++++++++++++++++++++++--------- java/org/gnu/emacs/EmacsView.java | 13 +++-- 3 files changed, 91 insertions(+), 31 deletions(-) (limited to 'java/org/gnu') 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; -- cgit v1.2.1 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 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'java/org/gnu') 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; -- cgit v1.2.1 From 5b4dea0fc781fe40548e7b58fe5bd201a05f3913 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 6 Jun 2023 14:35:19 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsContextMenu.java (display): Use `EmacsHolder' instead of `Holder'. * java/org/gnu/emacs/EmacsDialog.java (toAlertDialog): Use `EmacsDialogButtonLayout' to ensure that buttons are wrapped properly. (display): Adjust for new holder class. * java/org/gnu/emacs/EmacsDialogButtonLayout.java (EmacsDialogButtonLayout, onMeasure, onLayout): New functions. * java/org/gnu/emacs/EmacsDrawLine.java: * java/org/gnu/emacs/EmacsFillPolygon.java: Remove redundant imports. * java/org/gnu/emacs/EmacsHolder.java (EmacsHolder): * java/org/gnu/emacs/EmacsService.java (class Holder) (getEmacsView, EmacsService): Rename `Holder' to `EmacsHolder' and make it public. --- java/org/gnu/emacs/EmacsContextMenu.java | 4 +- java/org/gnu/emacs/EmacsDialog.java | 17 ++- java/org/gnu/emacs/EmacsDialogButtonLayout.java | 152 ++++++++++++++++++++++++ java/org/gnu/emacs/EmacsDrawLine.java | 2 - java/org/gnu/emacs/EmacsFillPolygon.java | 2 - java/org/gnu/emacs/EmacsHolder.java | 30 +++++ java/org/gnu/emacs/EmacsService.java | 13 +- 7 files changed, 195 insertions(+), 25 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsDialogButtonLayout.java create mode 100644 java/org/gnu/emacs/EmacsHolder.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 5bae41bd61d..60f2db67fb0 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -331,9 +331,9 @@ public final class EmacsContextMenu final int yPosition, final int serial) { Runnable runnable; - final Holder rc; + final EmacsHolder rc; - rc = new Holder (); + rc = new EmacsHolder (); runnable = new Runnable () { @Override diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 3a5f22021fc..afdce3c50ec 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -150,7 +150,7 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener AlertDialog dialog; int size; EmacsButton button; - LinearLayout layout; + EmacsDialogButtonLayout layout; Button buttonView; ViewGroup.LayoutParams layoutParams; @@ -192,19 +192,16 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener } else { - /* There are more than 4 buttons. Add them all to a - LinearLayout. */ - layout = new LinearLayout (context); - layoutParams - = new LinearLayout.LayoutParams (ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT); + /* There are more than 3 buttons. Add them all to a special + container widget that handles wrapping. */ + + layout = new EmacsDialogButtonLayout (context); for (EmacsButton emacsButton : buttons) { buttonView = new Button (context); buttonView.setText (emacsButton.name); buttonView.setOnClickListener (emacsButton); - buttonView.setLayoutParams (layoutParams); buttonView.setEnabled (emacsButton.enabled); layout.addView (buttonView); } @@ -336,9 +333,9 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener display () { Runnable runnable; - final Holder rc; + final EmacsHolder rc; - rc = new Holder (); + rc = new EmacsHolder (); runnable = new Runnable () { @Override public void diff --git a/java/org/gnu/emacs/EmacsDialogButtonLayout.java b/java/org/gnu/emacs/EmacsDialogButtonLayout.java new file mode 100644 index 00000000000..5d97eea32aa --- /dev/null +++ b/java/org/gnu/emacs/EmacsDialogButtonLayout.java @@ -0,0 +1,152 @@ +/* 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.Context; + +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; + + + +/* This ``view group'' implements a container widget for multiple + buttons of the type found in pop-up dialogs. It is used when + displaying a dialog box that contains more than three buttons, as + the default dialog box widget is not capable of holding more than + that many. */ + + + +public class EmacsDialogButtonLayout extends ViewGroup +{ + public + EmacsDialogButtonLayout (Context context) + { + super (context); + } + + @Override + protected void + onMeasure (int widthMeasureSpec, int heightMeasureSpec) + { + int width, count, i, x, y, height, spec, tempSpec; + View view; + + /* Obtain the width of this widget and create the measure + specification used to measure children. */ + + width = MeasureSpec.getSize (widthMeasureSpec); + spec = MeasureSpec.makeMeasureSpec (0, MeasureSpec.UNSPECIFIED); + tempSpec + = MeasureSpec.makeMeasureSpec (width, MeasureSpec.AT_MOST); + x = y = height = 0; + + /* Run through each widget. */ + + count = getChildCount (); + + for (i = 0; i < count; ++i) + { + view = getChildAt (i); + + /* Measure this view. */ + view.measure (spec, spec); + + if (width - x < view.getMeasuredWidth ()) + { + /* Move onto the next line, unless this line is empty. */ + + if (x != 0) + { + y += height; + height = x = 0; + } + + if (view.getMeasuredWidth () > width) + /* Measure the view again, this time forcing it to be at + most width wide, if it is not already. */ + view.measure (tempSpec, spec); + } + + height = Math.max (height, view.getMeasuredHeight ()); + x += view.getMeasuredWidth (); + } + + /* Now set the measured size of this widget. */ + setMeasuredDimension (width, y + height); + } + + @Override + protected void + onLayout (boolean changed, int left, int top, int right, + int bottom) + { + int width, count, i, x, y, height, spec, tempSpec; + View view; + + /* Obtain the width of this widget and create the measure + specification used to measure children. */ + + width = getMeasuredWidth (); + spec = MeasureSpec.makeMeasureSpec (0, MeasureSpec.UNSPECIFIED); + tempSpec + = MeasureSpec.makeMeasureSpec (width, MeasureSpec.AT_MOST); + x = y = height = 0; + + /* Run through each widget. */ + + count = getChildCount (); + + for (i = 0; i < count; ++i) + { + view = getChildAt (i); + + /* Measure this view. */ + view.measure (spec, spec); + + if (width - x < view.getMeasuredWidth ()) + { + /* Move onto the next line, unless this line is empty. */ + + if (x != 0) + { + y += height; + height = x = 0; + } + + if (view.getMeasuredWidth () > width) + /* Measure the view again, this time forcing it to be at + most width wide, if it is not already. */ + view.measure (tempSpec, spec); + } + + /* Now assign this view its position. */ + view.layout (x, y, x + view.getMeasuredWidth (), + y + view.getMeasuredHeight ()); + + /* And move on to the next widget. */ + height = Math.max (height, view.getMeasuredHeight ()); + x += view.getMeasuredWidth (); + } + } +}; diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 92e03c48e26..3f5067c0bdd 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -19,8 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.lang.Math; - import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java index ea8324c543c..4ae3882cab4 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -19,8 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.lang.Math; - import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; diff --git a/java/org/gnu/emacs/EmacsHolder.java b/java/org/gnu/emacs/EmacsHolder.java new file mode 100644 index 00000000000..3ca803d1640 --- /dev/null +++ b/java/org/gnu/emacs/EmacsHolder.java @@ -0,0 +1,30 @@ +/* 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; + + + +/* This class serves as a simple reference to an object of type T. + Nothing could be found inside the standard library. */ + +public class EmacsHolder +{ + T thing; +}; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 6d70536faf0..2074a5b7c2b 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -71,11 +71,6 @@ import android.util.DisplayMetrics; import android.widget.Toast; -class Holder -{ - T thing; -}; - /* EmacsService is the service that starts the thread running Emacs and handles requests by that Emacs instance. */ @@ -282,9 +277,9 @@ public final class EmacsService extends Service final boolean isFocusedByDefault) { Runnable runnable; - final Holder view; + final EmacsHolder view; - view = new Holder (); + view = new EmacsHolder (); runnable = new Runnable () { public void @@ -604,10 +599,10 @@ public final class EmacsService extends Service public ClipboardManager getClipboardManager () { - final Holder manager; + final EmacsHolder manager; Runnable runnable; - manager = new Holder (); + manager = new EmacsHolder (); runnable = new Runnable () { public void -- cgit v1.2.1 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 ++++ 4 files changed, 123 insertions(+) (limited to 'java/org/gnu') 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); -- cgit v1.2.1 From 1661762784520eb6834aa9831dcb646396efde73 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 8 Jun 2023 20:50:02 +0800 Subject: Correctly display popup dialogs from Emacsclient * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu): Make subclasses final. * java/org/gnu/emacs/EmacsDialog.java (display1): Check if an instance of EmacsOpenActivity is open; if it is, try using it to display the pop up dialog. * java/org/gnu/emacs/EmacsDialogButtonLayout.java (EmacsDialogButtonLayout): Make final. * java/org/gnu/emacs/EmacsHolder.java (EmacsHolder): Likewise. * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity): New field `currentActivity'. (onCreate, onDestroy, onWindowFocusChanged, onPause): Set that field as appropriate. --- java/org/gnu/emacs/EmacsContextMenu.java | 2 +- java/org/gnu/emacs/EmacsDialog.java | 17 ++++-- java/org/gnu/emacs/EmacsDialogButtonLayout.java | 2 +- java/org/gnu/emacs/EmacsHolder.java | 2 +- java/org/gnu/emacs/EmacsOpenActivity.java | 70 ++++++++++++++++++++++++- 5 files changed, 85 insertions(+), 8 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 60f2db67fb0..d69d0263b93 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -58,7 +58,7 @@ public final class EmacsContextMenu /* The last group ID used for a menu item. */ public int lastGroupId; - private static class Item implements MenuItem.OnMenuItemClickListener + private static final class Item implements MenuItem.OnMenuItemClickListener { public int itemID; public String itemName, tooltip; diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index afdce3c50ec..3f8fe0109c0 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -68,8 +68,8 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener /* The menu serial associated with this dialog box. */ private int menuEventSerial; - private class EmacsButton implements View.OnClickListener, - DialogInterface.OnClickListener + private final class EmacsButton implements View.OnClickListener, + DialogInterface.OnClickListener { /* Name of this button. */ public String name; @@ -244,13 +244,22 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener if (EmacsActivity.focusedActivities.isEmpty ()) { /* If focusedActivities is empty then this dialog may have - been displayed immediately after a popup dialog is + been displayed immediately after another popup dialog was dismissed. Or Emacs might legitimately be in the - background. Try the service context first if possible. */ + background, possibly displaying this popup in response to + an Emacsclient request. Try the service context if it will + work, then any focused EmacsOpenActivity, and finally the + last EmacsActivity to be focused. */ + + Log.d (TAG, "display1: no focused activities..."); + Log.d (TAG, ("display1: EmacsOpenActivity.currentActivity: " + + EmacsOpenActivity.currentActivity)); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays (EmacsService.SERVICE)) context = EmacsService.SERVICE; + else if (EmacsOpenActivity.currentActivity != null) + context = EmacsOpenActivity.currentActivity; else context = EmacsActivity.lastFocusedActivity; diff --git a/java/org/gnu/emacs/EmacsDialogButtonLayout.java b/java/org/gnu/emacs/EmacsDialogButtonLayout.java index 5d97eea32aa..fd8d63d81d3 100644 --- a/java/org/gnu/emacs/EmacsDialogButtonLayout.java +++ b/java/org/gnu/emacs/EmacsDialogButtonLayout.java @@ -37,7 +37,7 @@ import android.view.ViewGroup; -public class EmacsDialogButtonLayout extends ViewGroup +public final class EmacsDialogButtonLayout extends ViewGroup { public EmacsDialogButtonLayout (Context context) diff --git a/java/org/gnu/emacs/EmacsHolder.java b/java/org/gnu/emacs/EmacsHolder.java index 3ca803d1640..6cd48ba57ce 100644 --- a/java/org/gnu/emacs/EmacsHolder.java +++ b/java/org/gnu/emacs/EmacsHolder.java @@ -24,7 +24,7 @@ package org.gnu.emacs; /* This class serves as a simple reference to an object of type T. Nothing could be found inside the standard library. */ -public class EmacsHolder +public final class EmacsHolder { T thing; }; diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index f402e25c7fb..6af2b2d2e94 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -72,8 +72,17 @@ public final class EmacsOpenActivity extends Activity DialogInterface.OnCancelListener { private static final String TAG = "EmacsOpenActivity"; + + /* The name of any file that should be opened as EmacsThread starts + Emacs. This is never cleared, even if EmacsOpenActivity is + started a second time, as EmacsThread only starts once. */ public static String fileToOpen; + /* Any currently focused EmacsOpenActivity. Used to show pop ups + while the activity is active and Emacs doesn't have permission to + display over other programs. */ + public static EmacsOpenActivity currentActivity; + private class EmacsClientThread extends Thread { private ProcessBuilder builder; @@ -362,6 +371,15 @@ public final class EmacsOpenActivity extends Activity thread.start (); } + /* Run emacsclient to open the file specified in the Intent that + caused this activity to start. + + Determine the name of the file corresponding to the URI specified + in that intent; then, run emacsclient and wait for it to finish. + + Finally, display any error message, transfer the focus to an + Emacs frame, and finish the activity. */ + @Override public void onCreate (Bundle savedInstanceState) @@ -463,10 +481,60 @@ public final class EmacsOpenActivity extends Activity } } - /* And start emacsclient. */ + /* And start emacsclient. Set `currentActivity' to this now. + Presumably, it will shortly become capable of displaying + dialogs. */ + currentActivity = this; startEmacsClient (fileName); } else finish (); } + + + + @Override + public void + onDestroy () + { + Log.d (TAG, "onDestroy: " + this); + + /* Clear `currentActivity' if it refers to the activity being + destroyed. */ + + if (currentActivity == this) + this.currentActivity = null; + + super.onDestroy (); + } + + @Override + public void + onWindowFocusChanged (boolean isFocused) + { + Log.d (TAG, "onWindowFocusChanged: " + this + ", is now focused: " + + isFocused); + + if (isFocused) + currentActivity = this; + else if (currentActivity == this) + currentActivity = null; + + super.onWindowFocusChanged (isFocused); + } + + @Override + public void + onPause () + { + Log.d (TAG, "onPause: " + this); + + /* XXX: clear currentActivity here as well; I don't know whether + or not onWindowFocusChanged is always called prior to this. */ + + if (currentActivity == this) + currentActivity = null; + + super.onPause (); + } } -- cgit v1.2.1 From 7f073df53374bc1b96ac07e949af11b8af06b94b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 9 Jun 2023 09:27:04 +0800 Subject: Fix crash starting Emacs to open file * java/org/gnu/emacs/EmacsThread.java (run): Correct check against extraStartupArgument when an initial file is specified. --- java/org/gnu/emacs/EmacsThread.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 1343b70bf5a..c003ea95c50 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -65,7 +65,7 @@ public final class EmacsThread extends Thread } else { - if (extraStartupArgument != null) + if (extraStartupArgument == null) args = new String[] { "libandroid-emacs.so", fileToOpen, }; else -- cgit v1.2.1 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 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) (limited to 'java/org/gnu') 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) { -- cgit v1.2.1 From 5abc977bbb2d38f3c607f1575e02aa7a6c483db0 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 11 Jun 2023 08:52:10 +0800 Subject: Avoid extraneous calls to the UI thread * java/org/gnu/emacs/EmacsView.java (EmacsView) (showOnScreenKeyboard, hideOnScreenKeyboard) (onCheckIsTextEditor): Make synchronized. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow) (toggleOnScreenKeyboard): Don't post to the main thread. --- java/org/gnu/emacs/EmacsView.java | 6 +++--- java/org/gnu/emacs/EmacsWindow.java | 21 +++++++++------------ 2 files changed, 12 insertions(+), 15 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index d432162132d..7fd672233f2 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -590,7 +590,7 @@ public final class EmacsView extends ViewGroup super.onAttachedToWindow (); } - public void + public synchronized void showOnScreenKeyboard () { /* Specifying no flags at all tells the system the user asked for @@ -599,7 +599,7 @@ public final class EmacsView extends ViewGroup isCurrentlyTextEditor = true; } - public void + public synchronized void hideOnScreenKeyboard () { imManager.hideSoftInputFromWindow (this.getWindowToken (), @@ -686,7 +686,7 @@ public final class EmacsView extends ViewGroup } @Override - public boolean + public synchronized boolean onCheckIsTextEditor () { /* If value is true, then the system will display the on screen diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index c14bf16b96e..f5e40e9a2d9 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -1201,19 +1201,16 @@ public final class EmacsWindow extends EmacsHandleObject } public void - toggleOnScreenKeyboard (final boolean on) + toggleOnScreenKeyboard (boolean on) { - EmacsService.SERVICE.runOnUiThread (new Runnable () { - @Override - public void - run () - { - if (on) - view.showOnScreenKeyboard (); - else - view.hideOnScreenKeyboard (); - } - }); + /* InputMethodManager functions are thread safe. Call + `showOnScreenKeyboard' etc from the Emacs thread in order to + keep the calls in sync with updates to the input context. */ + + if (on) + view.showOnScreenKeyboard (); + else + view.hideOnScreenKeyboard (); } public String -- cgit v1.2.1 From 24f25fc2f8823b1999fa66e4b21601ee4000f321 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 11 Jun 2023 14:35:13 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): Document member variables. (onDraw): Use separate Paint object on the UI thread. * src/textconv.c (really_commit_text, really_set_composing_text) (really_delete_surrounding_text): Run modification hooks when deleting text. --- java/org/gnu/emacs/EmacsSurfaceView.java | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 0deb930c2c2..738b1a99eef 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -38,19 +38,40 @@ import java.lang.ref.WeakReference; public final class EmacsSurfaceView extends View { private static final String TAG = "EmacsSurfaceView"; + + /* The EmacsView representing the window that this surface is + displaying. */ private EmacsView view; + + /* The complete buffer contents at the time of the last draw. */ private Bitmap frontBuffer; + + /* Canvas representing the front buffer. */ private Canvas bitmapCanvas; + + /* Reference to the last bitmap copied to the front buffer. */ private WeakReference bitmap; - private Paint bitmapPaint; + + /* Paint objects used on the main and UI threads, respectively. */ + private static final Paint bitmapPaint, uiThreadPaint; + + static + { + /* Create two different Paint objects; one is used on the main + thread for buffer swaps, while the other is used from the UI + thread in `onDraw'. This is necessary because Paint objects + are not thread-safe, even if their uses are interlocked. */ + + bitmapPaint = new Paint (); + uiThreadPaint = new Paint (); + }; public - EmacsSurfaceView (final EmacsView view) + EmacsSurfaceView (EmacsView view) { super (view.getContext ()); this.view = view; - this.bitmapPaint = new Paint (); this.bitmap = new WeakReference (null); } @@ -161,6 +182,6 @@ public final class EmacsSurfaceView extends View now. */ if (frontBuffer != null) - canvas.drawBitmap (frontBuffer, 0f, 0f, bitmapPaint); + canvas.drawBitmap (frontBuffer, 0f, 0f, uiThreadPaint); } }; -- cgit v1.2.1 From 3b08bb1318cd0bf6bc1811b520f9c6934b1aa3bd Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 12 Jun 2023 14:19:01 +0800 Subject: Fix deadlocks * java/org/gnu/emacs/EmacsView.java (EmacsView) (showOnScreenKeyboard, hideOnScreenKeyboard): Don't synchronize. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow) (toggleOnScreenKeyboard): Revert to calling IMM functions from the main thread. * src/android.c (struct android_event_container) (android_pselect_nfds, android_pselect_readfds) (android_pselect_writefds, android_pselect_exceptfds) (android_pselect_timeout): Don't make volatile. (android_wait_event): Run queries if necessary. --- java/org/gnu/emacs/EmacsView.java | 5 +++-- java/org/gnu/emacs/EmacsWindow.java | 25 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 7fd672233f2..278c6025902 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -590,16 +590,17 @@ public final class EmacsView extends ViewGroup super.onAttachedToWindow (); } - public synchronized void + 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); isCurrentlyTextEditor = true; } - public synchronized void + public void hideOnScreenKeyboard () { imManager.hideSoftInputFromWindow (this.getWindowToken (), diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index f5e40e9a2d9..68a18ec2aa7 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -1201,16 +1201,25 @@ public final class EmacsWindow extends EmacsHandleObject } public void - toggleOnScreenKeyboard (boolean on) + toggleOnScreenKeyboard (final boolean on) { - /* InputMethodManager functions are thread safe. Call - `showOnScreenKeyboard' etc from the Emacs thread in order to - keep the calls in sync with updates to the input context. */ + /* Even though InputMethodManager functions are thread safe, + `showOnScreenKeyboard' etc must be called from the UI thread in + order to avoid deadlocks if the calls happen in tandem with a + call to a synchronizing function within + `onCreateInputConnection'. */ - if (on) - view.showOnScreenKeyboard (); - else - view.hideOnScreenKeyboard (); + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + if (on) + view.showOnScreenKeyboard (); + else + view.hideOnScreenKeyboard (); + } + }); } public String -- cgit v1.2.1 From 1cd514c297fb570b35b257ee9880b729e35a1068 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 12 Jun 2023 20:09:09 +0800 Subject: Improve appearance of custom dialog buttons on Android * java/org/gnu/emacs/EmacsDialog.java (toAlertDialog): Resolve dialog button style and use it instead. --- java/org/gnu/emacs/EmacsDialog.java | 72 ++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 12 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 3f8fe0109c0..5f48a9a5f9f 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -27,6 +27,10 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; + import android.os.Build; import android.provider.Settings; @@ -148,13 +152,17 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener toAlertDialog (Context context) { AlertDialog dialog; - int size; + int size, styleId; + int[] attrs; EmacsButton button; EmacsDialogButtonLayout layout; Button buttonView; ViewGroup.LayoutParams layoutParams; + Theme theme; + TypedArray attributes; size = buttons.size (); + styleId = -1; if (size <= 3) { @@ -193,19 +201,10 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener else { /* There are more than 3 buttons. Add them all to a special - container widget that handles wrapping. */ + container widget that handles wrapping. First, create the + layout. */ layout = new EmacsDialogButtonLayout (context); - - for (EmacsButton emacsButton : buttons) - { - buttonView = new Button (context); - buttonView.setText (emacsButton.name); - buttonView.setOnClickListener (emacsButton); - buttonView.setEnabled (emacsButton.enabled); - layout.addView (buttonView); - } - layoutParams = new FrameLayout.LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); @@ -223,6 +222,55 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener if (title != null) dialog.setTitle (title); + + /* Now that the dialog has been created, set the style of each + custom button to match the usual dialog buttons found on + Android 5 and later. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + /* Obtain the Theme associated with the dialog. */ + theme = dialog.getContext ().getTheme (); + + /* Resolve the dialog button style. */ + attrs + = new int [] { android.R.attr.buttonBarNeutralButtonStyle, }; + + try + { + attributes = theme.obtainStyledAttributes (attrs); + + /* Look for the style ID. Default to -1 if it could + not be found. */ + styleId = attributes.getResourceId (0, -1); + + /* Now clean up the TypedAttributes object. */ + attributes.recycle (); + } + catch (NotFoundException e) + { + /* Nothing to do here. */ + } + } + + /* Create each button and add it to the layout. Set the style + if necessary. */ + + for (EmacsButton emacsButton : buttons) + { + if (styleId == -1) + /* No specific style... */ + buttonView = new Button (context); + else + /* Use the given styleId. */ + buttonView = new Button (context, null, 0, styleId); + + /* Set the text and on click handler. */ + buttonView.setText (emacsButton.name); + buttonView.setOnClickListener (emacsButton); + buttonView.setEnabled (emacsButton.enabled); + layout.addView (buttonView); + } } return dialog; -- cgit v1.2.1 From 90ae3cc387530229e5aca32c00d35495ab680e21 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 14 Jun 2023 15:37:47 +0800 Subject: Improve IM synchronization on Android * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): Reimplement as an InputConnection, not BaseInputConnection. * src/androidterm.c (performEditorAction): Sync prior to sending keyboard events. --- java/org/gnu/emacs/EmacsInputConnection.java | 189 +++++++++++++++++++++++---- 1 file changed, 167 insertions(+), 22 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 73c93c67ac7..1bcc9a62a81 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -19,26 +19,35 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import android.view.inputmethod.BaseInputConnection; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; + +import android.view.KeyEvent; + import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextAttribute; import android.view.inputmethod.TextSnapshot; -import android.view.KeyEvent; - -import android.os.Build; - import android.util.Log; /* 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 +public final class EmacsInputConnection implements InputConnection { private static final String TAG = "EmacsInputConnection"; + + /* View associated with this input connection. */ private EmacsView view; + + /* The handle ID associated with that view's window. */ private short windowHandle; /* Whether or not to synchronize and call `updateIC' with the @@ -77,15 +86,18 @@ public final class EmacsInputConnection extends BaseInputConnection extractAbsoluteOffsets = true; }; + public EmacsInputConnection (EmacsView view) { - super (view, true); - this.view = view; this.windowHandle = view.window.handle; } + + /* The functions below are called by input methods whenever they + need to perform an edit. */ + @Override public boolean beginBatchEdit () @@ -116,7 +128,6 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } - @Override public boolean commitCompletion (CompletionInfo info) { @@ -133,6 +144,19 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + commitCorrection (CorrectionInfo info) + { + /* The input method calls this function not to commit text, but to + indicate that a subsequent edit will consist of a correction. + Emacs has no use for this information. + + Of course this completely contradicts the provided + documentation, but this is how Android actually behaves. */ + return false; + } + @Override public boolean commitText (CharSequence text, int newCursorPosition) @@ -170,6 +194,14 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + commitText (CharSequence text, int newCursorPosition, + TextAttribute textAttribute) + { + return commitText (text, newCursorPosition); + } + @Override public boolean deleteSurroundingText (int leftLength, int rightLength) @@ -187,6 +219,16 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + deleteSurroundingTextInCodePoints (int leftLength, int rightLength) + { + /* Emacs returns characters which cannot be represented in a Java + `char' as NULL characters, so code points always reflect + characters themselves. */ + return deleteSurroundingText (leftLength, rightLength); + } + @Override public boolean finishComposingText () @@ -277,6 +319,14 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + setComposingText (CharSequence text, int newCursorPosition, + TextAttribute textAttribute) + { + return setComposingText (text, newCursorPosition); + } + @Override public boolean setComposingRegion (int start, int end) @@ -292,6 +342,13 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + setComposingRegion (int start, int end, TextAttribute textAttribute) + { + return setComposingRegion (start, end); + } + @Override public boolean performEditorAction (int editorAction) @@ -430,6 +487,8 @@ public final class EmacsInputConnection extends BaseInputConnection } @Override + /* ACTION_MULTIPLE is apparently obsolete. */ + @SuppressWarnings ("deprecation") public boolean sendKeyEvent (KeyEvent key) { @@ -440,20 +499,33 @@ public final class EmacsInputConnection extends BaseInputConnection if (EmacsService.DEBUG_IC) Log.d (TAG, "sendKeyEvent: " + key); - return super.sendKeyEvent (key); - } + /* Use the standard API if possible. */ - @Override - public boolean - deleteSurroundingTextInCodePoints (int beforeLength, int afterLength) - { - /* Return if the input connection is out of date. */ - if (view.icSerial < view.icGeneration) - return false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + view.imManager.dispatchKeyEventFromInputMethod (view, key); + else + { + /* Fall back to dispatching the event manually if not. */ + + switch (key.getAction ()) + { + case KeyEvent.ACTION_DOWN: + view.onKeyDown (key.getKeyCode (), key); + break; + + case KeyEvent.ACTION_UP: + view.onKeyUp (key.getKeyCode (), key); + break; + + case KeyEvent.ACTION_MULTIPLE: + view.onKeyMultiple (key.getKeyCode (), + key.getRepeatCount (), + key); + break; + } + } - /* This can be implemented the same way as - deleteSurroundingText. */ - return this.deleteSurroundingText (beforeLength, afterLength); + return true; } @Override @@ -471,6 +543,16 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public boolean + requestCursorUpdates (int cursorUpdateMode, int filter) + { + if (filter != 0) + return false; + + return requestCursorUpdates (cursorUpdateMode); + } + @Override public SurroundingText getSurroundingText (int beforeLength, int afterLength, @@ -505,11 +587,74 @@ public final class EmacsInputConnection extends BaseInputConnection /* Override functions which are not implemented. */ + @Override + public Handler + getHandler () + { + return null; + } + + @Override + public void + closeConnection () + { + + } + + @Override + public boolean + commitContent (InputContentInfo inputContentInfo, int flags, + Bundle opts) + { + return false; + } + + @Override + public boolean + setImeConsumesInput (boolean imeConsumesInput) + { + return false; + } + @Override public TextSnapshot takeSnapshot () { - Log.d (TAG, "takeSnapshot"); return null; } + + @Override + public boolean + clearMetaKeyStates (int states) + { + return false; + } + + @Override + public boolean + reportFullscreenMode (boolean enabled) + { + return false; + } + + @Override + public boolean + performSpellCheck () + { + return false; + } + + @Override + public boolean + performPrivateCommand (String action, Bundle data) + { + return false; + } + + @Override + public int + getCursorCapsMode (int reqModes) + { + return 0; + } } -- cgit v1.2.1 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 ++ 4 files changed, 81 insertions(+), 15 deletions(-) (limited to 'java/org/gnu') 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; -- cgit v1.2.1 From 377a3ebbb55a9b944551394e00d24c445e3ff4a1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 16 Jun 2023 12:59:44 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): * java/org/gnu/emacs/EmacsApplication.java (findDumpFile): * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu) (addSubmenu, display): * java/org/gnu/emacs/EmacsDocumentsProvider.java (getNotificationUri, queryChildDocuments, deleteDocument): * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): * java/org/gnu/emacs/EmacsFillRectangle.java (perform): * java/org/gnu/emacs/EmacsOpenActivity.java (readEmacsClientLog) (checkReadableOrCopy): * java/org/gnu/emacs/EmacsSdk7FontDriver.java (EmacsSdk7FontDriver): * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): * java/org/gnu/emacs/EmacsView.java (EmacsView): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, onKeyUp): * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager): Remove various unused arguments and variables, dead stores, and make minor cleanups and performance improvements. * src/androidmenu.c (FIND_METHOD_STATIC, android_menu_show): Adjust accordingly. --- java/org/gnu/emacs/EmacsActivity.java | 2 +- java/org/gnu/emacs/EmacsApplication.java | 3 ++ java/org/gnu/emacs/EmacsContextMenu.java | 9 +++-- java/org/gnu/emacs/EmacsDocumentsProvider.java | 17 ++++++---- java/org/gnu/emacs/EmacsDrawRectangle.java | 2 -- java/org/gnu/emacs/EmacsFillRectangle.java | 2 +- java/org/gnu/emacs/EmacsOpenActivity.java | 38 +++++++++++++++++----- java/org/gnu/emacs/EmacsSdk7FontDriver.java | 4 +++ java/org/gnu/emacs/EmacsSurfaceView.java | 5 --- java/org/gnu/emacs/EmacsView.java | 4 --- java/org/gnu/emacs/EmacsWindow.java | 24 ++++++-------- .../gnu/emacs/EmacsWindowAttachmentManager.java | 7 +++- 12 files changed, 69 insertions(+), 48 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 7ba268ba42d..fa9bff39bb0 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -55,7 +55,7 @@ public class EmacsActivity extends Activity private FrameLayout layout; /* List of activities with focus. */ - public static List focusedActivities; + public static final List focusedActivities; /* The last activity to have been focused. */ public static EmacsActivity lastFocusedActivity; diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index 10099721744..d8b77acdf9e 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -60,6 +60,9 @@ public final class EmacsApplication extends Application } }); + if (allFiles == null) + return; + /* Now try to find the right dump file. */ for (i = 0; i < allFiles.length; ++i) { diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index d69d0263b93..eb83016c849 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -131,20 +131,18 @@ public final class EmacsContextMenu }; public List menuItems; - public String title; private EmacsContextMenu parent; /* Create a context menu with no items inside and the title TITLE, which may be NULL. */ public static EmacsContextMenu - createContextMenu (String title) + createContextMenu () { EmacsContextMenu menu; menu = new EmacsContextMenu (); menu.menuItems = new ArrayList (); - menu.title = title; return menu; } @@ -197,7 +195,7 @@ public final class EmacsContextMenu item name. */ public EmacsContextMenu - addSubmenu (String itemName, String title, String tooltip) + addSubmenu (String itemName, String tooltip) { EmacsContextMenu submenu; Item item; @@ -206,7 +204,7 @@ public final class EmacsContextMenu item.itemID = 0; item.itemName = itemName; item.tooltip = tooltip; - item.subMenu = createContextMenu (title); + item.subMenu = createContextMenu (); item.subMenu.parent = this; menuItems.add (item); @@ -334,6 +332,7 @@ public final class EmacsContextMenu final EmacsHolder rc; rc = new EmacsHolder (); + rc.thing = false; runnable = new Runnable () { @Override diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index b4ac4624829..96dc2bc6e14 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -131,9 +131,7 @@ public final class EmacsDocumentsProvider extends DocumentsProvider getNotificationUri (File file) { Uri updatedUri; - Context context; - context = getContext (); updatedUri = buildChildDocumentsUri ("org.gnu.emacs", file.getAbsolutePath ()); @@ -294,6 +292,7 @@ public final class EmacsDocumentsProvider extends DocumentsProvider { MatrixCursor result; File directory; + File[] files; Context context; if (projection == null) @@ -305,9 +304,15 @@ public final class EmacsDocumentsProvider extends DocumentsProvider requested. */ directory = new File (parentDocumentId); - /* Now add each child. */ - for (File child : directory.listFiles ()) - queryDocument1 (result, child); + /* Look up each child. */ + files = directory.listFiles (); + + if (files != null) + { + /* Now add each child. */ + for (File child : files) + queryDocument1 (result, child); + } context = getContext (); @@ -406,12 +411,10 @@ public final class EmacsDocumentsProvider extends DocumentsProvider { File file, parent; File[] children; - Context context; /* Java makes recursively deleting a file hard. File name encoding issues also prevent easily calling into C... */ - context = getContext (); file = new File (documentId); parent = file.getParentFile (); diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index 3bd5779c54e..b54b031b56b 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -36,7 +36,6 @@ public final class EmacsDrawRectangle Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; - Rect rect; Rect maskRect, dstRect; Canvas canvas; Bitmap clipBitmap; @@ -52,7 +51,6 @@ public final class EmacsDrawRectangle paint = gc.gcPaint; paint.setStyle (Paint.Style.STROKE); - rect = new Rect (x, y, x + width, y + height); if (gc.clip_mask == null) /* Use canvas.drawRect with a RectF. That seems to reliably diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java index 4a0478b446f..461fd3c639c 100644 --- a/java/org/gnu/emacs/EmacsFillRectangle.java +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -61,6 +61,7 @@ public final class EmacsFillRectangle /* 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, @@ -69,7 +70,6 @@ public final class EmacsFillRectangle + 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 diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index 6af2b2d2e94..9411f85d434 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -146,7 +146,7 @@ public final class EmacsOpenActivity extends Activity FileReader reader; char[] buffer; int rc; - String what; + StringBuilder builder; /* Because the ProcessBuilder functions necessary to redirect process output are not implemented on Android 7 and earlier, @@ -160,7 +160,8 @@ public final class EmacsOpenActivity extends Activity cache = getCacheDir (); file = new File (cache, "emacsclient.log"); - what = ""; + builder = new StringBuilder (); + reader = null; try { @@ -168,13 +169,25 @@ public final class EmacsOpenActivity extends Activity buffer = new char[2048]; while ((rc = reader.read (buffer, 0, 2048)) != -1) - what += String.valueOf (buffer, 0, 2048); + builder.append (buffer, 0, rc); reader.close (); - return what; + return builder.toString (); } catch (IOException exception) { + /* Close the reader if it's already been opened. */ + + try + { + if (reader != null) + reader.close (); + } + catch (IOException e) + { + /* Not sure what to do here. */ + } + return ("Couldn't read emacsclient.log: " + exception.toString ()); } @@ -248,11 +261,16 @@ public final class EmacsOpenActivity extends Activity /* inFile is now the file being written to. */ inFile = new File (getCacheDir (), inFile.getName ()); buffer = new byte[4098]; - outStream = new FileOutputStream (inFile); - stream = new FileInputStream (fd.getFileDescriptor ()); + + /* Initialize both streams to NULL. */ + outStream = null; + stream = null; try { + outStream = new FileOutputStream (inFile); + stream = new FileInputStream (fd.getFileDescriptor ()); + while ((read = stream.read (buffer)) >= 0) outStream.write (buffer, 0, read); } @@ -263,8 +281,12 @@ public final class EmacsOpenActivity extends Activity Keep in mind that execution is transferred to ``finally'' even if an exception happens inside the while loop above. */ - stream.close (); - outStream.close (); + + if (stream != null) + stream.close (); + + if (outStream != null) + outStream.close (); } return inFile.getCanonicalPath (); diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index 6df102f18a2..9122b46458a 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -252,6 +252,10 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver systemFontsDirectory = new File ("/system/fonts"); fontFamilyList = systemFontsDirectory.list (); + + /* If that returned null, replace it with an empty array. */ + fontFamilyList = new String[0]; + typefaceList = new Sdk7Typeface[fontFamilyList.length + 3]; /* It would be nice to avoid opening each and every font upon diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 738b1a99eef..5b3e05eb9f4 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -39,10 +39,6 @@ public final class EmacsSurfaceView extends View { private static final String TAG = "EmacsSurfaceView"; - /* The EmacsView representing the window that this surface is - displaying. */ - private EmacsView view; - /* The complete buffer contents at the time of the last draw. */ private Bitmap frontBuffer; @@ -71,7 +67,6 @@ public final class EmacsSurfaceView extends View { super (view.getContext ()); - this.view = view; this.bitmap = new WeakReference (null); } diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index aba1184b0c2..0cabefdf385 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -68,9 +68,6 @@ public final class EmacsView extends ViewGroup /* The damage region. */ public Region damageRegion; - /* The paint. */ - public Paint paint; - /* The associated surface view. */ private EmacsSurfaceView surfaceView; @@ -128,7 +125,6 @@ public final class EmacsView extends ViewGroup this.window = window; this.damageRegion = new Region (); - this.paint = new Paint (); setFocusable (true); setFocusableInTouchMode (true); diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 68a18ec2aa7..739a1f43b7d 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -103,11 +103,10 @@ public final class EmacsWindow extends EmacsHandleObject public int lastButtonState, lastModifiers; /* Whether or not the window is mapped. */ - private boolean isMapped; + private volatile boolean isMapped; - /* Whether or not to ask for focus upon being mapped, and whether or - not the window should be focusable. */ - private boolean dontFocusOnMap, dontAcceptFocus; + /* Whether or not to ask for focus upon being mapped. */ + private boolean dontFocusOnMap; /* Whether or not the window is override-redirect. An override-redirect window always has its own system window. */ @@ -464,7 +463,7 @@ public final class EmacsWindow extends EmacsHandleObject } } - public void + public synchronized void unmapWindow () { if (!isMapped) @@ -618,7 +617,7 @@ public final class EmacsWindow extends EmacsHandleObject onKeyUp (int keyCode, KeyEvent event) { int state, state_1; - long time, serial; + long time; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -645,12 +644,11 @@ public final class EmacsWindow extends EmacsHandleObject state_1 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); - serial - = EmacsNative.sendKeyRelease (this.handle, - event.getEventTime (), - state, keyCode, - getEventUnicodeChar (event, - state_1)); + EmacsNative.sendKeyRelease (this.handle, + event.getEventTime (), + state, keyCode, + getEventUnicodeChar (event, + state_1)); lastModifiers = state; if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) @@ -1155,8 +1153,6 @@ public final class EmacsWindow extends EmacsHandleObject public synchronized void setDontAcceptFocus (final boolean dontAcceptFocus) { - this.dontAcceptFocus = dontAcceptFocus; - /* Update the view's focus state. */ EmacsService.SERVICE.runOnUiThread (new Runnable () { @Override diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 4fda48616f0..bc96de7fe1a 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -53,9 +53,11 @@ import android.util.Log; public final class EmacsWindowAttachmentManager { - public static EmacsWindowAttachmentManager MANAGER; private final static String TAG = "EmacsWindowAttachmentManager"; + /* The single window attachment manager ``object''. */ + public static final EmacsWindowAttachmentManager MANAGER; + static { MANAGER = new EmacsWindowAttachmentManager (); @@ -69,7 +71,10 @@ public final class EmacsWindowAttachmentManager public void destroy (); }; + /* List of currently attached window consumers. */ public List consumers; + + /* List of currently attached windows. */ public List windows; public -- cgit v1.2.1 From 797c30b7abc165d5ebe65474c7398ccad0e3023c Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 17 Jun 2023 12:07:40 +0800 Subject: Initialize signal mask earlier * java/org/gnu/emacs/EmacsService.java (onCreate, run): Don't initialize signal mask here. * java/org/gnu/emacs/EmacsApplication.java (onCreate): Do it here instead. * src/android.c (JNICALL): Restore previous signal masks. --- java/org/gnu/emacs/EmacsApplication.java | 8 ++++++++ java/org/gnu/emacs/EmacsService.java | 25 ------------------------- 2 files changed, 8 insertions(+), 25 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index d8b77acdf9e..8afa5bcedb4 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -78,7 +78,15 @@ public final class EmacsApplication extends Application public void onCreate () { + /* Block signals which don't interest the current thread and its + descendants created by the system. The original signal mask + will be restored for the Emacs thread in `initEmacs'. */ + EmacsNative.setupSystemThread (); + + /* Locate a suitable dump file. */ findDumpFile (this); + + /* Start the rest of the application. */ super.onCreate (); } }; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 2fe4e8c4146..820befb52d2 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -25,7 +25,6 @@ import java.io.UnsupportedEncodingException; import java.util.List; -import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import android.graphics.Matrix; @@ -212,7 +211,6 @@ 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 ()); @@ -222,7 +220,6 @@ public final class EmacsService extends Service pixelDensityX = metrics.xdpi; pixelDensityY = metrics.ydpi; resolver = getContentResolver (); - signalSemaphore = new Semaphore (0); try { @@ -251,33 +248,11 @@ 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) { -- cgit v1.2.1 From 405d14402f21df3404ce9c5aa3c7f942e6deb3e3 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 19 Jun 2023 15:26:07 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsView.java (EmacsView, dimensionsLock): New field. (): Create new lock object. (handleDirtyBitmap, onLayout, onAttachedToWindow): Make sure measuredWidth and measuredHeight are extracted and set atomically. Send Expose upon layout changes if the view has grown. --- java/org/gnu/emacs/EmacsView.java | 68 ++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 0cabefdf385..bb4dace655a 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -88,6 +88,9 @@ public final class EmacsView extends ViewGroup /* The last measured width and height. */ private int measuredWidth, measuredHeight; + /* Object acting as a lock for those values. */ + private Object dimensionsLock; + /* The serial of the last clip rectangle change. */ private long lastClipSerial; @@ -144,12 +147,23 @@ public final class EmacsView extends ViewGroup /* Add this view as its own global layout listener. */ getViewTreeObserver ().addOnGlobalLayoutListener (this); + + /* Create an object used as a lock. */ + this.dimensionsLock = new Object (); } private void handleDirtyBitmap () { Bitmap oldBitmap; + int measuredWidth, measuredHeight; + + synchronized (dimensionsLock) + { + /* Load measuredWidth and measuredHeight. */ + measuredWidth = this.measuredWidth; + measuredHeight = this.measuredHeight; + } if (measuredWidth == 0 || measuredHeight == 0) return; @@ -171,7 +185,7 @@ public final class EmacsView extends ViewGroup /* Save the old bitmap. */ oldBitmap = bitmap; - /* Recreate the front and back buffer bitmaps. */ + /* Recreate the back buffer bitmap. */ bitmap = Bitmap.createBitmap (measuredWidth, measuredHeight, @@ -249,8 +263,11 @@ public final class EmacsView extends ViewGroup public void prepareForLayout (int wantedWidth, int wantedHeight) { - measuredWidth = wantedWidth; - measuredHeight = wantedWidth; + synchronized (dimensionsLock) + { + measuredWidth = wantedWidth; + measuredHeight = wantedWidth; + } } @Override @@ -294,19 +311,39 @@ public final class EmacsView extends ViewGroup onLayout (boolean changed, int left, int top, int right, int bottom) { - int count, i; + int count, i, oldMeasuredWidth, oldMeasuredHeight; View child; Rect windowRect; + boolean needExpose; count = getChildCount (); + needExpose = false; - measuredWidth = right - left; - measuredHeight = bottom - top; + synchronized (dimensionsLock) + { + /* Load measuredWidth and measuredHeight. */ + oldMeasuredWidth = measuredWidth; + oldMeasuredHeight = measuredHeight; - /* Dirty the back buffer. */ + /* Set measuredWidth and measuredHeight. */ + measuredWidth = right - left; + measuredHeight = bottom - top; + } - if (changed) - explicitlyDirtyBitmap (); + /* Dirty the back buffer if the layout change resulted in the view + being resized. */ + + if (changed && (right - left != oldMeasuredWidth + || bottom - top != oldMeasuredHeight)) + { + explicitlyDirtyBitmap (); + + /* Expose the window upon a change in the view's size. */ + + if (right - left > oldMeasuredWidth + || bottom - top > oldMeasuredHeight) + needExpose = true; + } for (i = 0; i < count; ++i) { @@ -336,6 +373,10 @@ public final class EmacsView extends ViewGroup mustReportLayout = false; window.viewLayout (left, top, right, bottom); } + + if (needExpose) + EmacsNative.sendExpose (this.window.handle, 0, 0, + right - left, bottom - top); } public void @@ -579,9 +620,12 @@ public final class EmacsView extends ViewGroup was called. */ bitmapDirty = true; - /* Now expose the view contents again. */ - EmacsNative.sendExpose (this.window.handle, 0, 0, - measuredWidth, measuredHeight); + synchronized (dimensionsLock) + { + /* Now expose the view contents again. */ + EmacsNative.sendExpose (this.window.handle, 0, 0, + measuredWidth, measuredHeight); + } super.onAttachedToWindow (); } -- cgit v1.2.1 From a5ee9a69ae73843bf3e620dbe474d5cdaaff5f3a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 23 Jun 2023 11:54:56 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): * java/org/gnu/emacs/EmacsSdk7FontDriver.java (Sdk7FontEntity): (hasChar): Clean up dead stores. --- java/org/gnu/emacs/EmacsDrawRectangle.java | 1 - java/org/gnu/emacs/EmacsSdk7FontDriver.java | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index b54b031b56b..e1261b4a2d2 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -72,7 +72,6 @@ public final class EmacsDrawRectangle + 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 diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index 9122b46458a..97969585d16 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -169,8 +169,6 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver public Sdk7FontEntity (Sdk7Typeface typeface) { - float width; - foundry = "Google"; family = typeface.familyName; adstyle = null; @@ -363,7 +361,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver public int hasChar (FontSpec font, char charCode) { - float missingGlyphWidth, emGlyphWidth, width; + float missingGlyphWidth, width; Rect rect1, rect2; Paint paint; Sdk7FontObject fontObject; @@ -382,7 +380,6 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver return 1; missingGlyphWidth = paint.measureText (TOFU_STRING); - emGlyphWidth = paint.measureText (EM_STRING); width = paint.measureText ("" + charCode); if (width == 0f) -- cgit v1.2.1 From 257e49ff0d333cd5b9967613cb14051d54dd054b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 5 Jul 2023 14:57:15 +0800 Subject: Fix crash between Android 4.0 and Android 5.1 * java/org/gnu/emacs/EmacsService.java (detectMouse): Don't use function that is not present on Android 4.0. --- java/org/gnu/emacs/EmacsService.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 820befb52d2..e1fd2dbffda 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -491,6 +491,8 @@ public final class EmacsService extends Service int i; if (Build.VERSION.SDK_INT + /* Android 4.0 and earlier don't support mouse input events at + all. */ < Build.VERSION_CODES.JELLY_BEAN) return false; @@ -504,8 +506,20 @@ public final class EmacsService extends Service if (device == null) continue; - if (device.supportsSource (InputDevice.SOURCE_MOUSE)) - return true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + { + if (device.supportsSource (InputDevice.SOURCE_MOUSE)) + return true; + } + else + { + /* `supportsSource' is only present on API level 21 and + later, but earlier versions provide a bit mask + containing each supported source. */ + + if ((device.getSources () & InputDevice.SOURCE_MOUSE) != 0) + return true; + } } return false; -- cgit v1.2.1 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 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') 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, -- cgit v1.2.1 From e40dca8361dfa8aafbe17bfeb8ad06b6ba5230ad Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 7 Jul 2023 14:22:38 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsSurfaceView.java (copyToFrontBuffer): Use fallback bit blit function on Android 7.0 as well, as crashes have been observed in drawBitmap. --- java/org/gnu/emacs/EmacsSurfaceView.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 5b3e05eb9f4..3f62af4ab99 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -76,7 +76,9 @@ public final class EmacsSurfaceView extends View EmacsService.checkEmacsThread (); if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O - && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1) + && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1 + && Build.VERSION.SDK_INT != Build.VERSION_CODES.N_MR1 + && Build.VERSION.SDK_INT != Build.VERSION_CODES.N) { /* If `drawBitmap' can safely be used while a bitmap is locked by another thread, continue here... */ @@ -89,8 +91,8 @@ public final class EmacsSurfaceView extends View } else { - /* But if it can not, as on Android 8.0 and 8.1, then use a - replacement function. */ + /* But if it can not, as on Android 7.0 through 8.1, then use + a replacement function. */ if (damageRect != null) EmacsNative.blitRect (bitmap, frontBuffer, -- cgit v1.2.1 From c843f3e23b48dcf65e72db0eefa6a5a74c815ff6 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 8 Jul 2023 10:15:38 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsService.java (DEBUG_IC) (DEBUG_THREADS): Improve commentary. * src/androidterm.c (handle_one_android_event): Signal completion of IME events that have lost their frames. (requestCursorUpdates): Don't set an edit counter as this event won't be passed to the text conversion machinery. --- java/org/gnu/emacs/EmacsService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 37048960f25..62fd2740286 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -96,11 +96,11 @@ public final class EmacsService extends Service public DisplayMetrics metrics; /* Flag that says whether or not to print verbose debugging - information. */ + information when responding to an input method. */ public static final boolean DEBUG_IC = false; - /* Flag that says whether or not to perform extra checks on threads - performing drawing calls. */ + /* Flag that says whether or not to stringently check that only the + Emacs thread is performing drawing calls. */ private static final boolean DEBUG_THREADS = false; /* Atomic integer used for synchronization between -- cgit v1.2.1 From a36207574ac436fd8871639388cc6400069bb9bb Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 8 Jul 2023 21:49:21 +0800 Subject: Fix EmacsDrawLine again * java/org/gnu/emacs/EmacsDrawLine.java (perform): Symmetrically adjust coordinates to cover the last pixel. Then, fill the left most pixel of the line. --- java/org/gnu/emacs/EmacsDrawLine.java | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 3f5067c0bdd..d367ccff9c4 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -32,30 +32,45 @@ public final class EmacsDrawLine Rect rect; Canvas canvas; Paint paint; + int x0, x1, y0, y1; /* TODO implement stippling. */ if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; + /* Calculate the leftmost and rightmost points. */ + + x0 = Math.min (x, x2 + 1); + x1 = Math.max (x, x2 + 1); + y0 = Math.min (y, y2 + 1); + y1 = Math.max (y, y2 + 1); + + /* And the clip rectangle. */ + 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)); + rect = new Rect (x0, y0, x1, y1); canvas = drawable.lockCanvas (gc); if (canvas == null) return; - paint.setStyle (Paint.Style.STROKE); + paint.setStyle (Paint.Style.FILL); /* Since drawLine has PostScript style behavior, adjust the - coordinates appropriately. */ + coordinates appropriately. + + The left most pixel of a straight line is always partially + filled. Patch it in manually. */ if (gc.clip_mask == null) - canvas.drawLine ((float) x, (float) y + 0.5f, - (float) x2 + 0.5f, (float) y2 + 0.5f, - paint); + { + canvas.drawLine ((float) x + 0.5f, (float) y + 0.5f, + (float) x2 + 0.5f, (float) y2 + 0.5f, + paint); + + if (x2 > x) + canvas.drawRect (new Rect (x, y, x + 1, y + 1), paint); + } /* DrawLine with clip mask not implemented; it is not used by Emacs. */ -- cgit v1.2.1 From 105c876a609d4610ce684c7513b8548feae3638a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 9 Jul 2023 10:05:08 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsDrawPoint.java (perform): Don't fill an extra pixel. * java/org/gnu/emacs/EmacsService.java (onCreate): Make sure scaledDensity is always at least 160 dpi. --- java/org/gnu/emacs/EmacsDrawPoint.java | 5 ++++- java/org/gnu/emacs/EmacsService.java | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java index de8ddf09cc4..6a1cb744d60 100644 --- a/java/org/gnu/emacs/EmacsDrawPoint.java +++ b/java/org/gnu/emacs/EmacsDrawPoint.java @@ -25,7 +25,10 @@ public final class EmacsDrawPoint perform (EmacsDrawable drawable, EmacsGC immutableGC, int x, int y) { - EmacsDrawRectangle.perform (drawable, immutableGC, + /* Use EmacsFillRectangle instead of EmacsDrawRectangle, as the + latter actually draws a rectangle one pixel wider than + specified. */ + EmacsFillRectangle.perform (drawable, immutableGC, x, y, 1, 1); } } diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 62fd2740286..f484e2c9ca3 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -225,6 +225,17 @@ public final class EmacsService extends Service * pixelDensityX); resolver = getContentResolver (); + /* If the density used to compute the text size is lesser than + 160, there's likely a bug with display density computation. + Reset it to 160 in that case. + + Note that Android uses 160 ``dpi'' as the density where 1 point + corresponds to 1 pixel, not 72 or 96 as used elsewhere. This + difference is codified in PT_PER_INCH defined in font.h. */ + + if (scaledDensity < 160) + scaledDensity = 160; + try { /* Configure Emacs with the asset manager and other necessary @@ -240,7 +251,9 @@ public final class EmacsService extends Service Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir + ", libDir = " + libDir + ", and classPath = " + classPath - + "; fileToOpen = " + EmacsOpenActivity.fileToOpen); + + "; fileToOpen = " + EmacsOpenActivity.fileToOpen + + "; display density: " + pixelDensityX + " by " + + pixelDensityY + " scaled to " + scaledDensity); /* Start the thread that runs Emacs. */ thread = new EmacsThread (this, new Runnable () { -- cgit v1.2.1 From 8afb5644fd21162f8fd6c39f111844b79e002b7c Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 9 Jul 2023 10:06:18 +0800 Subject: ; * java/org/gnu/emacs/EmacsService.java (onCreate): Fix typo. --- java/org/gnu/emacs/EmacsService.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index f484e2c9ca3..a7e83e276f2 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -212,6 +212,7 @@ public final class EmacsService extends Service final double pixelDensityX; final double pixelDensityY; final double scaledDensity; + double tempScaledDensity; SERVICE = this; handler = new Handler (Looper.getMainLooper ()); @@ -220,9 +221,9 @@ public final class EmacsService extends Service metrics = getResources ().getDisplayMetrics (); pixelDensityX = metrics.xdpi; pixelDensityY = metrics.ydpi; - scaledDensity = ((metrics.scaledDensity - / metrics.density) - * pixelDensityX); + tempScaledDensity = ((metrics.scaledDensity + / metrics.density) + * pixelDensityX); resolver = getContentResolver (); /* If the density used to compute the text size is lesser than @@ -233,8 +234,12 @@ public final class EmacsService extends Service corresponds to 1 pixel, not 72 or 96 as used elsewhere. This difference is codified in PT_PER_INCH defined in font.h. */ - if (scaledDensity < 160) - scaledDensity = 160; + if (tempScaledDensity < 160) + tempScaledDensity = 160; + + /* scaledDensity is const as required to refer to it from within + the nested function below. */ + scaledDensity = tempScaledDensity; try { -- cgit v1.2.1 From 97f926b82d5674f90a16327b0ad4d6b226fcacb4 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 9 Jul 2023 12:50:15 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsService.java (onStartCommand): Fix typo in notification message. * java/org/gnu/emacs/EmacsWindow.java (onFocusChanged): Reset the recorded modifier state upon a change to the window focus. --- java/org/gnu/emacs/EmacsService.java | 2 +- java/org/gnu/emacs/EmacsWindow.java | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index a7e83e276f2..0543c3a1bdd 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -147,7 +147,7 @@ public final class EmacsService extends Service manager = (NotificationManager) tem; infoBlurb = ("This notification is displayed to keep Emacs" + " running while it is in the background. You" - + " may disable if you want;" + + " may disable it if you want;" + " see (emacs)Android Environment."); channel = new NotificationChannel ("emacs", "Emacs persistent notification", diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 739a1f43b7d..2d8a8627468 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -99,7 +99,8 @@ public final class EmacsWindow extends EmacsHandleObject private EmacsGC scratchGC; /* The button state and keyboard modifier mask at the time of the - last button press or release event. */ + last button press or release event. The modifier mask is reset + upon each window focus change. */ public int lastButtonState, lastModifiers; /* Whether or not the window is mapped. */ @@ -670,6 +671,12 @@ public final class EmacsWindow extends EmacsHandleObject onFocusChanged (boolean gainFocus) { EmacsActivity.invalidateFocus (); + + /* If focus has been lost, reset the keyboard modifier state, as + subsequent changes will not be recorded. */ + + if (!gainFocus) + lastModifiers = 0; } /* Notice that the activity has been detached or destroyed. -- cgit v1.2.1 From ed5ade097e5b675167f9c7e4916a4beec62d118a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 9 Jul 2023 13:13:53 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsWindow.java (eventModifiers) (motionEventModifiers): New functions. (onKeyDown, onKeyUp, onFocusChanged, onSomeKindOfMotionEvent): Don't record the previous modifier mask; instead, always use the modifier state specified in the event. * src/androidterm.c (handle_one_android_event): Don't dispatch button release events when a popup is active. --- java/org/gnu/emacs/EmacsWindow.java | 99 +++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 42 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 2d8a8627468..15d5fe8a175 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -99,9 +99,8 @@ public final class EmacsWindow extends EmacsHandleObject private EmacsGC scratchGC; /* The button state and keyboard modifier mask at the time of the - last button press or release event. The modifier mask is reset - upon each window focus change. */ - public int lastButtonState, lastModifiers; + last button press or release event. */ + public int lastButtonState; /* Whether or not the window is mapped. */ private volatile boolean isMapped; @@ -562,15 +561,16 @@ public final class EmacsWindow extends EmacsHandleObject eventStrings.put (serial, string); } - /* event.getCharacters is used because older input methods still - require it. */ - @SuppressWarnings ("deprecation") - public void - onKeyDown (int keyCode, KeyEvent event) + + + /* Return the modifier mask associated with the specified keyboard + input EVENT. Replace bits corresponding to Left or Right keys + with their corresponding general modifier bits. */ + + private int + eventModifiers (KeyEvent event) { - int state, state_1; - long serial; - String characters; + int state; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) state = event.getModifiers (); @@ -592,6 +592,46 @@ public final class EmacsWindow extends EmacsHandleObject state |= KeyEvent.META_CTRL_MASK; } + return state; + } + + /* Return the modifier mask associated with the specified motion + EVENT. Replace bits corresponding to Left or Right keys with + their corresponding general modifier bits. */ + + private int + motionEventModifiers (MotionEvent event) + { + int state; + + state = event.getMetaState (); + + /* Normalize the state by setting the generic modifier bit if + either a left or right modifier is pressed. */ + + if ((state & KeyEvent.META_ALT_LEFT_ON) != 0 + || (state & KeyEvent.META_ALT_RIGHT_ON) != 0) + state |= KeyEvent.META_ALT_MASK; + + if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0 + || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0) + state |= KeyEvent.META_CTRL_MASK; + + return state; + } + + /* 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; + + state = eventModifiers (event); + /* Ignore meta-state understood by Emacs for now, or Ctrl+C will not be recognized as an ASCII key press event. */ state_1 @@ -605,7 +645,6 @@ public final class EmacsWindow extends EmacsHandleObject state, keyCode, getEventUnicodeChar (event, state_1)); - lastModifiers = state; characters = event.getCharacters (); @@ -620,25 +659,8 @@ public final class EmacsWindow extends EmacsHandleObject int state, state_1; long time; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) - state = event.getModifiers (); - else - { - /* Replace this with getMetaState and manual - normalization. */ - state = event.getMetaState (); - - /* Normalize the state by setting the generic modifier bit if - either a left or right modifier is pressed. */ - - if ((state & KeyEvent.META_ALT_LEFT_ON) != 0 - || (state & KeyEvent.META_ALT_RIGHT_ON) != 0) - state |= KeyEvent.META_ALT_MASK; - - if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0 - || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0) - state |= KeyEvent.META_CTRL_MASK; - } + /* Compute the event's modifier mask. */ + state = eventModifiers (event); /* Ignore meta-state understood by Emacs for now, or Ctrl+C will not be recognized as an ASCII key press event. */ @@ -650,7 +672,6 @@ public final class EmacsWindow extends EmacsHandleObject state, keyCode, getEventUnicodeChar (event, state_1)); - lastModifiers = state; if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { @@ -671,12 +692,6 @@ public final class EmacsWindow extends EmacsHandleObject onFocusChanged (boolean gainFocus) { EmacsActivity.invalidateFocus (); - - /* If focus has been lost, reset the keyboard modifier state, as - subsequent changes will not be recorded. */ - - if (!gainFocus) - lastModifiers = 0; } /* Notice that the activity has been detached or destroyed. @@ -940,7 +955,7 @@ public final class EmacsWindow extends EmacsHandleObject EmacsNative.sendButtonPress (this.handle, (int) event.getX (), (int) event.getY (), event.getEventTime (), - lastModifiers, + motionEventModifiers (event), whatButtonWasIt (event, true)); if (Build.VERSION.SDK_INT @@ -955,7 +970,7 @@ public final class EmacsWindow extends EmacsHandleObject EmacsNative.sendButtonRelease (this.handle, (int) event.getX (), (int) event.getY (), event.getEventTime (), - lastModifiers, + motionEventModifiers (event), whatButtonWasIt (event, false)); if (Build.VERSION.SDK_INT @@ -988,7 +1003,7 @@ public final class EmacsWindow extends EmacsHandleObject EmacsNative.sendButtonRelease (this.handle, (int) event.getX (), (int) event.getY (), event.getEventTime (), - lastModifiers, + motionEventModifiers (event), whatButtonWasIt (event, false)); lastButtonState = event.getButtonState (); } @@ -1000,7 +1015,7 @@ public final class EmacsWindow extends EmacsHandleObject EmacsNative.sendWheel (this.handle, (int) event.getX (), (int) event.getY (), event.getEventTime (), - lastModifiers, + motionEventModifiers (event), event.getAxisValue (MotionEvent.AXIS_HSCROLL), event.getAxisValue (MotionEvent.AXIS_VSCROLL)); return true; -- cgit v1.2.1 From cf2dde4261a311406203a38d6bf1be72b4f9e8a7 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 10 Jul 2023 13:31:57 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsService.java (browseUrl): New argument SEND. Choose from a list of applications that want to share the URL if true. * lisp/net/browse-url.el (browse-url-android-share): New user option. (browse-url-default-android-browser): Respect said user option. * src/android.c (android_init_emacs_service) (android_browse_url): Expose new option. * src/android.h: Update prototypes. * src/androidselect.c (Fandroid_browse_url): Likewise. --- java/org/gnu/emacs/EmacsService.java | 65 +++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 20 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 0543c3a1bdd..22649167f8a 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -583,12 +583,18 @@ public final class EmacsService extends Service } } - /* Ask the system to open the specified URL. + /* Ask the system to open the specified URL in an application that + understands how to open it. + + If SEND, tell the system to also open applications that can + ``send'' the URL (through mail, for example), instead of only + those that can view the URL. + Value is NULL upon success, or a string describing the error upon failure. */ public String - browseUrl (String url) + browseUrl (String url, boolean send) { Intent intent; Uri uri; @@ -596,28 +602,47 @@ public final class EmacsService extends Service try { /* Parse the URI. */ - uri = Uri.parse (url); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + if (!send) { - /* On Android 4.4 and later, check if URI is actually a - file name. If so, rewrite it into a content provider - URI, so that it can be accessed by other programs. */ - - if (uri.getScheme ().equals ("file") - && uri.getPath () != null) - uri - = DocumentsContract.buildDocumentUri ("org.gnu.emacs", - uri.getPath ()); + uri = Uri.parse (url); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + { + /* On Android 4.4 and later, check if URI is actually + a file name. If so, rewrite it into a content + provider URI, so that it can be accessed by other + programs. */ + + if (uri.getScheme ().equals ("file") + && uri.getPath () != null) + uri + = DocumentsContract.buildDocumentUri ("org.gnu.emacs", + uri.getPath ()); + } + + Log.d (TAG, ("browseUri: browsing " + url + + " --> " + uri.getPath () + + " --> " + uri)); + + intent = new Intent (Intent.ACTION_VIEW, uri); + intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_GRANT_READ_URI_PERMISSION); } + else + { + intent = new Intent (Intent.ACTION_SEND); + intent.setType ("text/plain"); + intent.putExtra (Intent.EXTRA_SUBJECT, "Sharing link"); + intent.putExtra (Intent.EXTRA_TEXT, url); + + /* Display a list of programs able to send this URL. */ + intent = Intent.createChooser (intent, "Send"); - Log.d (TAG, ("browseUri: browsing " + url - + " --> " + uri.getPath () - + " --> " + uri)); + /* Apparently flags need to be set after a choser is + created. */ + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + } - intent = new Intent (Intent.ACTION_VIEW, uri); - intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity (intent); } catch (Exception e) -- cgit v1.2.1 From 24ae08c11342d93ac68b284302fcf5929c19ffb0 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 11 Jul 2023 10:22:03 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsView.java (onGenericMotionEvent): Call onGenericMotionEvent. * java/org/gnu/emacs/EmacsWindow.java (Coordinate): New fields `button' and `id'. (): Add new arguments to the construtor. (whatButtonWasIt): Return 0 if the button state has not changed. (buttonForEvent): New function. (figureChange): Return the Coordinate object associated to EVENT. Determine whether or not EVENT was accompanied by a change to the button state, and ascertain which button that was. (motionEvent): New function. (onGenericMotionEvent, onTouchEvent): Factor out touch and mouse event delivery to motionEvent. --- java/org/gnu/emacs/EmacsView.java | 2 +- java/org/gnu/emacs/EmacsWindow.java | 435 +++++++++++++++++++++--------------- 2 files changed, 258 insertions(+), 179 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index bb4dace655a..12d8ff4da56 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -475,7 +475,7 @@ public final class EmacsView extends ViewGroup public boolean onGenericMotionEvent (MotionEvent motion) { - return window.onSomeKindOfMotionEvent (motion); + return window.onGenericMotionEvent (motion); } @Override diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 15d5fe8a175..6816f3e8e71 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -66,11 +66,20 @@ public final class EmacsWindow extends EmacsHandleObject /* Integral coordinate. */ int x, y; + /* Button associated with the coordinate, or 0 if it is a touch + event. */ + int button; + + /* Pointer ID associated with the coordinate. */ + int id; + public - Coordinate (int x, int y) + Coordinate (int x, int y, int button, int id) { this.x = x; this.y = y; + this.button = button; + this.id = id; } }; @@ -595,31 +604,6 @@ public final class EmacsWindow extends EmacsHandleObject return state; } - /* Return the modifier mask associated with the specified motion - EVENT. Replace bits corresponding to Left or Right keys with - their corresponding general modifier bits. */ - - private int - motionEventModifiers (MotionEvent event) - { - int state; - - state = event.getMetaState (); - - /* Normalize the state by setting the generic modifier bit if - either a left or right modifier is pressed. */ - - if ((state & KeyEvent.META_ALT_LEFT_ON) != 0 - || (state & KeyEvent.META_ALT_RIGHT_ON) != 0) - state |= KeyEvent.META_ALT_MASK; - - if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0 - || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0) - state |= KeyEvent.META_CTRL_MASK; - - return state; - } - /* event.getCharacters is used because older input methods still require it. */ @SuppressWarnings ("deprecation") @@ -710,6 +694,69 @@ public final class EmacsWindow extends EmacsHandleObject EmacsNative.sendWindowAction (this.handle, 0); } + + + /* Mouse and touch event handling. + + Android does not conceptually distinguish between mouse events + (those coming from a device whose movement affects the on-screen + pointer image) and touch screen events. When a touch, click, or + pointer motion takes place, several kinds of event can be sent: + + ACTION_DOWN or ACTION_POINTER_DOWN is sent with a new coordinate + and an associated ``pointer ID'' identifying the event when a + click or touch takes place. Emacs is responsible for recording + both the position of this click for the purpose of determining + future changes to the position of that touch. + + ACTION_UP or ACTION_POINTER_UP is sent with a pointer ID when the + click associated with a previous ACTION_DOWN event is released. + + ACTION_CANCEL (or ACTION_POINTER_UP with FLAG_CANCELED) is sent + if a similar situation transpires: the window system has chosen + to grab of the click, and future movement will no longer be + reported to Emacs. + + ACTION_MOVE is sent if a coordinate tied to a click that has not + been released changes. Emacs processes this event by comparing + each of the coordinates within the event with its recollection of + those contained within prior ACTION_DOWN and ACTION_MOVE events; + the pointer ID of the difference is then reported within a touch + or pointer motion event along with its new position. + + The events described above are all sent for both touch and mouse + click events. Determining whether an ACTION_DOWN event is + associated with a button event is performed by inspecting the + mouse button state associated with that event. If it contains + any mouse buttons that were not contained in the button state at + the time of the last ACTION_DOWN or ACTION_UP event, the + coordinate contained within is assumed to be a mouse click, + leading to it and associated motion or ACTION_UP events being + reported as mouse button or motion events. Otherwise, those + events are reported as touch screen events, with the touch ID set + to the pointer ID. + + In addition to the events illustrated above, Android also sends + several other types of event upon select types of activity from a + mouse device: + + ACTION_HOVER_MOVE is sent with the coordinate of the mouse + pointer if it moves above a frame prior to any click taking + place. Emacs sends a mouse motion event containing the + coordinate. + + ACTION_HOVER_ENTER and ACTION_HOVER_LEAVE are respectively sent + when the mouse pointer enters and leaves a frame. + + On Android 6.0 and later, ACTION_BUTTON_PRESS is sent with the + coordinate of the mouse pointer if a mouse click occurs, + alongside a ACTION_DOWN event. ACTION_BUTTON_RELEASE is sent + with the same information upon a mouse click being released, also + accompanying an ACTION_UP event. + + However, both types of button events are implemented in a buggy + fashion and cannot be used to report button events. */ + /* 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. */ @@ -719,16 +766,20 @@ public final class EmacsWindow extends EmacsHandleObject { 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; - + /* Obtain the new button state. */ eventState = event.getButtonState (); + + /* Compute which button is now set or no longer set. */ + notIn = (down ? eventState & ~lastButtonState : lastButtonState & ~eventState); + if ((notIn & (MotionEvent.BUTTON_PRIMARY + | MotionEvent.BUTTON_SECONDARY + | MotionEvent.BUTTON_TERTIARY)) == 0) + /* No buttons have been pressed, so this is a touch event. */ + return 0; + if ((notIn & MotionEvent.BUTTON_PRIMARY) != 0) return 1; @@ -742,53 +793,55 @@ public final 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. */ + /* Return the mouse button associated with the specified ACTION_DOWN + or ACTION_POINTER_DOWN EVENT. + + Value is 0 if no mouse button was pressed, or the X number of + that mouse button. */ private int + buttonForEvent (MotionEvent event) + { + /* ICS and earlier don't support true mouse button events, so + treat all down events as touch screen events. */ + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + return 0; + + return whatButtonWasIt (event, true); + } + + /* Return the coordinate object associated with the specified + EVENT, or null if it is not known. */ + + private Coordinate figureChange (MotionEvent event) { - int pointerID, i, truncatedX, truncatedY, pointerIndex; + int i, truncatedX, truncatedY, pointerIndex, pointerID, count; Coordinate coordinate; - boolean mouseFlag; - /* pointerID is always initialized but the Java compiler is too - dumb to know that. */ - pointerID = -1; - mouseFlag = false; + /* Initialize this variable now. */ + coordinate = null; 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))); + coordinate = new Coordinate ((int) event.getX (0), + (int) event.getY (0), + buttonForEvent (event), + pointerID); + pointerMap.put (pointerID, coordinate); break; case MotionEvent.ACTION_UP: - - /* 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; - + case MotionEvent.ACTION_CANCEL: /* Primary pointer released with index 0. */ pointerID = event.getPointerId (0); - pointerMap.remove (pointerID); + coordinate = pointerMap.remove (pointerID); break; case MotionEvent.ACTION_POINTER_DOWN: @@ -796,22 +849,26 @@ public final class EmacsWindow extends EmacsHandleObject it in the map. */ pointerIndex = event.getActionIndex (); pointerID = event.getPointerId (pointerIndex); - pointerMap.put (pointerID, - new Coordinate ((int) event.getX (pointerIndex), - (int) event.getY (pointerIndex))); + coordinate = new Coordinate ((int) event.getX (0), + (int) event.getY (0), + buttonForEvent (event), + pointerID); + pointerMap.put (pointerID, coordinate); break; case MotionEvent.ACTION_POINTER_UP: /* Pointer removed. Remove it from the map. */ pointerIndex = event.getActionIndex (); pointerID = event.getPointerId (pointerIndex); - pointerMap.remove (pointerID); + coordinate = pointerMap.remove (pointerID); break; default: /* Loop through each pointer in the event. */ - for (i = 0; i < event.getPointerCount (); ++i) + + count = event.getPointerCount (); + for (i = 0; i < count; ++i) { pointerID = event.getPointerId (i); @@ -835,73 +892,152 @@ public final class EmacsWindow extends EmacsHandleObject 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); + /* Set coordinate to NULL if the loop failed to find any + matching pointer. */ + + if (i == count) + coordinate = null; } /* Return the pointer ID. */ - return pointerID; + return coordinate; } - public boolean - onTouchEvent (MotionEvent event) + /* Return the modifier mask associated with the specified motion + EVENT. Replace bits corresponding to Left or Right keys with + their corresponding general modifier bits. */ + + private int + motionEventModifiers (MotionEvent event) + { + int state; + + state = event.getMetaState (); + + /* Normalize the state by setting the generic modifier bit if + either a left or right modifier is pressed. */ + + if ((state & KeyEvent.META_ALT_LEFT_ON) != 0 + || (state & KeyEvent.META_ALT_RIGHT_ON) != 0) + state |= KeyEvent.META_ALT_MASK; + + if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0 + || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0) + state |= KeyEvent.META_CTRL_MASK; + + return state; + } + + /* Process a single ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_UP, + ACTION_POINTER_UP, ACTION_CANCEL, or ACTION_MOVE event. + + Ascertain which coordinate changed and send an appropriate mouse + or touch screen event. */ + + private void + motionEvent (MotionEvent event) { - int pointerID, index; + Coordinate coordinate; + int modifiers; + long time; - /* Extract the ``touch ID'' (or in Android, the ``pointer - ID''.) */ - pointerID = figureChange (event); + /* Find data associated with this event's pointer. Namely, its + current location, whether or not a change has taken place, and + whether or not it is a button event. */ - if (pointerID < 0) + coordinate = figureChange (event); + + if (coordinate == null) + return; + + time = event.getEventTime (); + + if (coordinate.button != 0) { - /* If this is a mouse event, give it to - onSomeKindOfMotionEvent. */ - if (pointerID == -2) - return onSomeKindOfMotionEvent (event); + /* This event is tied to a mouse click, so report mouse motion + and button events. */ + + modifiers = motionEventModifiers (event); - return false; + switch (event.getAction ()) + { + case MotionEvent.ACTION_POINTER_DOWN: + case MotionEvent.ACTION_DOWN: + EmacsNative.sendButtonPress (this.handle, coordinate.x, + coordinate.y, time, modifiers, + coordinate.button); + break; + + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + EmacsNative.sendButtonRelease (this.handle, coordinate.x, + coordinate.y, time, modifiers, + coordinate.button); + break; + + case MotionEvent.ACTION_MOVE: + EmacsNative.sendMotionNotify (this.handle, coordinate.x, + coordinate.y, time); + break; + } } + else + { + /* This event is a touch event, and the touch ID is the + pointer ID. */ - /* 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, coordinate.x, + coordinate.y, time, + coordinate.id); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + 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. */ + EmacsNative.sendTouchUp (this.handle, coordinate.x, + coordinate.y, time, coordinate.id); + break; + + case MotionEvent.ACTION_MOVE: + /* Pointer motion event. */ + EmacsNative.sendTouchMove (this.handle, coordinate.x, + coordinate.y, time, coordinate.id); + break; + } + } + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + return; + + /* Now update the button state. */ + lastButtonState = event.getButtonState (); + return; + } + public boolean + onTouchEvent (MotionEvent event) + { 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: 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. */ - 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); + motionEvent (event); return true; } @@ -909,18 +1045,8 @@ public final class EmacsWindow extends EmacsHandleObject } public boolean - onSomeKindOfMotionEvent (MotionEvent event) + onGenericMotionEvent (MotionEvent event) { - /* isFromSource is not available until API level 18. */ - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) - { - if (!event.isFromSource (InputDevice.SOURCE_CLASS_POINTER)) - return false; - } - else if (event.getSource () != InputDevice.SOURCE_CLASS_POINTER) - return false; - switch (event.getAction ()) { case MotionEvent.ACTION_HOVER_ENTER: @@ -929,7 +1055,6 @@ public final class EmacsWindow extends EmacsHandleObject event.getEventTime ()); return true; - case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_HOVER_MOVE: EmacsNative.sendMotionNotify (this.handle, (int) event.getX (), (int) event.getY (), @@ -950,64 +1075,16 @@ public final class EmacsWindow extends EmacsHandleObject 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 (), - motionEventModifiers (event), - 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 (), - motionEventModifiers (event), - 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: - /* 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_POINTER_DOWN: case MotionEvent.ACTION_UP: - /* However, if ACTION_UP reports a different button state from - the last known state, look up which button was released and - send a ButtonRelease event; this is to work around a bug in - the framework where real ACTION_BUTTON_RELEASE events are - not delivered. */ - - if (Build.VERSION.SDK_INT - < Build.VERSION_CODES.ICE_CREAM_SANDWICH) - return true; - - if (event.getButtonState () == 0 && lastButtonState != 0) - { - EmacsNative.sendButtonRelease (this.handle, (int) event.getX (), - (int) event.getY (), - event.getEventTime (), - motionEventModifiers (event), - whatButtonWasIt (event, false)); - lastButtonState = event.getButtonState (); - } - + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_MOVE: + /* MotionEvents may either be sent to onGenericMotionEvent or + onTouchEvent depending on if Android thinks it is a mouse + event or not, but we detect them ourselves. */ + motionEvent (event); return true; case MotionEvent.ACTION_SCROLL: @@ -1024,6 +1101,8 @@ public final class EmacsWindow extends EmacsHandleObject return false; } + + public synchronized void reparentTo (final EmacsWindow otherWindow, int x, int y) { -- cgit v1.2.1 From c8c2bec5f8e4964f23345e1150a7ab85003e688b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 12 Jul 2023 09:45:58 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsWindow.java (whatButtonWasIt): Handle back and forward buttons along with styluses. * src/doc.c (close_file_unwind_android_fd): New function. (get_doc_string, Fsnarf_documentation): Don't create a temporary fd if it can be avoided. --- java/org/gnu/emacs/EmacsWindow.java | 53 ++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 12 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 6816f3e8e71..5e45275631b 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -700,29 +700,36 @@ public final class EmacsWindow extends EmacsHandleObject Android does not conceptually distinguish between mouse events (those coming from a device whose movement affects the on-screen - pointer image) and touch screen events. When a touch, click, or - pointer motion takes place, several kinds of event can be sent: + pointer image) and touch screen events. Each click or touch + starts a single pointer gesture sequence, and subsequent motion + of the device will result in updates being reported relative to + that sequence until the mouse button or touch is released. + + When a touch, click, or pointer motion takes place, several kinds + of event can be sent: ACTION_DOWN or ACTION_POINTER_DOWN is sent with a new coordinate - and an associated ``pointer ID'' identifying the event when a - click or touch takes place. Emacs is responsible for recording - both the position of this click for the purpose of determining - future changes to the position of that touch. + and an associated ``pointer ID'' identifying the event and its + gesture sequence when a click or touch takes place. Emacs is + responsible for recording both the position and pointer ID of + this click for the purpose of determining future changes to its + position. ACTION_UP or ACTION_POINTER_UP is sent with a pointer ID when the click associated with a previous ACTION_DOWN event is released. ACTION_CANCEL (or ACTION_POINTER_UP with FLAG_CANCELED) is sent if a similar situation transpires: the window system has chosen - to grab of the click, and future movement will no longer be - reported to Emacs. + to grab the click, and future changes to its position will no + longer be reported to Emacs. ACTION_MOVE is sent if a coordinate tied to a click that has not been released changes. Emacs processes this event by comparing each of the coordinates within the event with its recollection of those contained within prior ACTION_DOWN and ACTION_MOVE events; - the pointer ID of the difference is then reported within a touch - or pointer motion event along with its new position. + the pointer ID of the differing coordinate is then reported + within a touch or pointer motion event along with its new + position. The events described above are all sent for both touch and mouse click events. Determining whether an ACTION_DOWN event is @@ -746,7 +753,12 @@ public final class EmacsWindow extends EmacsHandleObject coordinate. ACTION_HOVER_ENTER and ACTION_HOVER_LEAVE are respectively sent - when the mouse pointer enters and leaves a frame. + when the mouse pointer enters and leaves a frame. Moreover, + ACTION_HOVER_LEAVE events are sent immediately before an + ACTION_DOWN event associated with a mouse click. These + extraneous events are distinct in that their button states always + contain an additional button compared to the button state + recorded at the time of the last ACTION_UP event. On Android 6.0 and later, ACTION_BUTTON_PRESS is sent with the coordinate of the mouse pointer if a mouse click occurs, @@ -789,8 +801,25 @@ public final class EmacsWindow extends EmacsHandleObject if ((notIn & MotionEvent.BUTTON_TERTIARY) != 0) return 2; + /* Buttons 4, 5, 6 and 7 are actually scroll wheels under X. + Thus, report additional buttons starting at 8. */ + + if ((notIn & MotionEvent.BUTTON_BACK) != 0) + return 8; + + if ((notIn & MotionEvent.BUTTON_FORWARD) != 0) + return 9; + + /* Report stylus events as touch screen events. */ + + if ((notIn & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0) + return 0; + + if ((notIn & MotionEvent.BUTTON_STYLUS_SECONDARY) != 0) + return 0; + /* Not a real value. */ - return 4; + return 11; } /* Return the mouse button associated with the specified ACTION_DOWN -- cgit v1.2.1 From 140755f2cfe6a39f643ab0a9ca2d81b0ed470ae7 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 13 Jul 2023 12:05:50 +0800 Subject: Disable hardware acceleration on Android It serves no purpose and causes tearing. Uploading the bitmap to the GPU takes about as long as it does to incrementally update the surface in software. * java/AndroidManifest.xml.in: Disable hardware acceleration. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): Make lastClosedMenu static. * java/org/gnu/emacs/EmacsDialog.java (toAlertDialog): Enable hardware acceleration within alert dialogs. * java/org/gnu/emacs/EmacsSurfaceView.java (onDraw): Describe why hardware acceleration is disabled. * java/org/gnu/emacs/EmacsWindow.java (run): Remove redundant call. --- java/org/gnu/emacs/EmacsActivity.java | 2 +- java/org/gnu/emacs/EmacsDialog.java | 14 +++++++++++++- java/org/gnu/emacs/EmacsSurfaceView.java | 20 +++++++++++++++++++- java/org/gnu/emacs/EmacsWindow.java | 2 +- 4 files changed, 34 insertions(+), 4 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index fa9bff39bb0..d7b51388929 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -70,7 +70,7 @@ public class EmacsActivity extends Activity private boolean isFullscreen; /* The last context menu to be closed. */ - private Menu lastClosedMenu; + private static Menu lastClosedMenu; static { diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 5f48a9a5f9f..42455ed78f8 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -152,7 +152,7 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener toAlertDialog (Context context) { AlertDialog dialog; - int size, styleId; + int size, styleId, flag; int[] attrs; EmacsButton button; EmacsDialogButtonLayout layout; @@ -160,6 +160,7 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener ViewGroup.LayoutParams layoutParams; Theme theme; TypedArray attributes; + Window window; size = buttons.size (); styleId = -1; @@ -273,6 +274,17 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener } } + /* Make sure the dialog is hardware accelerated. Hardware + acceleration is disabled for dialogs by default, because they + aren't enabled in EmacsActivity either. */ + + if (Build.VERSION.SDK_INT >= 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/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 3f62af4ab99..54fe70e1634 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -176,7 +176,25 @@ public final class EmacsSurfaceView extends View onDraw (Canvas canvas) { /* Paint the view's bitmap; the bitmap might be recycled right - now. */ + 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. */ if (frontBuffer != null) canvas.drawBitmap (frontBuffer, 0f, 0f, uiThreadPaint); diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 5e45275631b..0e96a8382d0 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -1377,7 +1377,7 @@ public final class EmacsWindow extends EmacsHandleObject if (tem != null) { - activity = (EmacsActivity) getAttachedConsumer (); + activity = (EmacsActivity) tem; activity.syncFullscreenWith (EmacsWindow.this); } } -- cgit v1.2.1 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/org/gnu/emacs/EmacsDialog.java | 11 ------ java/org/gnu/emacs/EmacsNative.java | 5 +++ java/org/gnu/emacs/EmacsSurfaceView.java | 61 +++++++++++++++++++++----------- 3 files changed, 46 insertions(+), 31 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 42455ed78f8..e4ed2271741 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -274,17 +274,6 @@ public final class EmacsDialog implements DialogInterface.OnDismissListener } } - /* Make sure the dialog is hardware accelerated. Hardware - acceleration is disabled for dialogs by default, because they - aren't enabled in EmacsActivity either. */ - - if (Build.VERSION.SDK_INT >= 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); + } } }; -- cgit v1.2.1 From 4c390f14f4f344ce63c04fface5c8a2a11061412 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 14 Jul 2023 08:51:07 +0800 Subject: Clean up Android debug code * java/org/gnu/emacs/EmacsInputConnection.java (getSurroundingText): Don't print debug information if DEBUG_IC is off. --- java/org/gnu/emacs/EmacsInputConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index f8dce5dfa79..c3764a7b29f 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -587,7 +587,7 @@ public final class EmacsInputConnection implements InputConnection text = EmacsNative.getSurroundingText (windowHandle, beforeLength, afterLength, flags); - if (text != null) + if (EmacsService.DEBUG_IC && text != null) Log.d (TAG, ("getSurroundingText: " + text.getSelectionStart () + "," -- cgit v1.2.1 From 8335c76e88ec9aa44963a3261ce0ab0eb0555a29 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 16 Jul 2023 20:10:18 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsService.java (displayToast): * src/android.c (android_init_emacs_service): Remove unused function. * lisp/touch-screen.el (touch-screen-handle-point-up): Correctly compute posn point. (touch-screen-translate-touch): Update doc string. (function-key-map): Define touch screen translation functions within the internal border. --- java/org/gnu/emacs/EmacsService.java | 20 -------------------- 1 file changed, 20 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 22649167f8a..6240b0e475a 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1086,26 +1086,6 @@ public final class EmacsService extends Service temp, }; } - /* Display the specified STRING in a small dialog box on the main - thread. */ - - public void - displayToast (final String string) - { - runOnUiThread (new Runnable () { - @Override - public void - run () - { - Toast toast; - - toast = Toast.makeText (getApplicationContext (), - string, Toast.LENGTH_SHORT); - toast.show (); - } - }); - } - public void updateExtractedText (EmacsWindow window, ExtractedText text, int token) -- cgit v1.2.1 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. --- java/org/gnu/emacs/EmacsNative.java | 9 ++++++--- java/org/gnu/emacs/EmacsWindow.java | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) (limited to 'java/org/gnu') 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; } } -- cgit v1.2.1 From 7e8904e796807e3b8bfc20ed45135c53d8a86f50 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 20 Jul 2023 11:21:25 +0800 Subject: Use context menu header titles on Android * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu): New field `title'. (addSubmenu): New arg TITLE. Set that field. (expandTo): Set MENU's header title if it's a context menu. * src/androidmenu.c (android_init_emacs_context_menu): Adjust signature of `createContextMenu'. (android_menu_show): Use TITLE instead of pane titles if there's only one pane. --- java/org/gnu/emacs/EmacsContextMenu.java | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index eb83016c849..46eddeeda3d 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.os.Build; +import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -130,18 +131,27 @@ public final class EmacsContextMenu } }; + /* List of menu items contained in this menu. */ public List menuItems; + + /* The parent context menu, or NULL if none. */ private EmacsContextMenu parent; + /* The title of this context menu, or NULL if none. */ + private String title; + + + /* Create a context menu with no items inside and the title TITLE, which may be NULL. */ public static EmacsContextMenu - createContextMenu () + createContextMenu (String title) { EmacsContextMenu menu; menu = new EmacsContextMenu (); + menu.title = title; menu.menuItems = new ArrayList (); return menu; @@ -204,7 +214,7 @@ public final class EmacsContextMenu item.itemID = 0; item.itemName = itemName; item.tooltip = tooltip; - item.subMenu = createContextMenu (); + item.subMenu = createContextMenu (itemName); item.subMenu.parent = this; menuItems.add (item); @@ -287,12 +297,22 @@ public final class EmacsContextMenu /* Enter the items in this context menu to MENU. Assume that MENU will be displayed in VIEW; this may lead to - popupMenu being called on VIEW if a submenu is selected. */ + popupMenu being called on VIEW if a submenu is selected. + + If MENU is a ContextMenu, set its header title to the one + contained in this object. */ public void expandTo (Menu menu, EmacsView view) { inflateMenuItems (menu, view); + + /* See if menu is a ContextMenu and a title is set. */ + if (title == null || !(menu instanceof ContextMenu)) + return; + + /* Set its title to this.title. */ + ((ContextMenu) menu).setHeaderTitle (title); } /* Return the parent or NULL. */ -- cgit v1.2.1 From 65b58251b1b07b50672367c675efdfdc97354c4a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 27 Jul 2023 17:13:39 +0800 Subject: Update Android port * configure.ac (ANDROID_STUBIFY): Add androidvfs.o when building libemacs.so. * doc/emacs/android.texi (Android): Add `Android Document Providers'. (Android Startup): Update the location of the content identifier directory. (Android File System): Describe access to document provider directories. (Android Document Providers): New node. * doc/emacs/emacs.texi (Top): Update the menu for the Android appendix. * java/Makefile.in (filename, install_temp/assets/build_info): Make directory-tree depend on build_info. * java/org/gnu/emacs/EmacsActivity.java (onActivityResult): New function. When a document tree is accepted, persist access to it. * java/org/gnu/emacs/EmacsDirectoryEntry.java (EmacsDirectoryEntry): New struct. * java/org/gnu/emacs/EmacsOpenActivity.java (checkReadableOrCopy): Use EmacsService.buildContentName. * java/org/gnu/emacs/EmacsService.java (getEmacsView, openContentUri) (checkContentUri): Remove excessive debug logging. (buildContentName, getDocumentAuthorities, requestDirectoryAccess) (getDocumentTrees, decodeFileName, documentIdFromName, getTreeUri) (statDocument, accessDocument, openDocumentDirectory, readDirectoryEntry) (openDocument, createDocument): New functions. * lib-src/asset-directory-tool.c: Improve commentary by illustrating the difference between directory and ordinary files. * src/android.c (ANDROID_THROW, enum android_fd_table_entry_flags) (struct android_emacs_service, android_extract_long) (android_scan_directory_tree, android_is_directory) (android_get_asset_name, android_url_encode, android_content_name_p) (android_get_content_name, android_check_content_access, android_fstat) (android_fstatat, android_file_access_p, android_hack_asset_fd_fallback) (android_detect_ashmem, android_hack_asset_fd, android_close_on_exec) (android_open, android_close, android_fclose, android_create_lib_link) (android_faccessat, struct android_dir, android_opendir, android_dirfd) (android_readdir, android_closedir, android_lookup_asset_directory_fd) (android_exception_check_3, android_get_current_api_level) (android_open_asset, android_close_asset, android_asset_read_quit) (android_asset_read, android_asset_lseek, android_asset_fstat): Move content and asset related functions to androidvfs.c. (android_init_emacs_service): Obtain handles for new JNI functions. (initEmacsParams): Initialize the VFS layer. (android_request_directory_access): New function. (android_display_toast): Remove unused function. * src/android.h (android_get_current_api_level): Assume that this function never returns less than __ANDROID_API__. (struct android_emacs_service): Move `struct android_emacs_service' here. * src/androidfns.c (Fandroid_request_directory_access): New interactive function. (syms_of_androidfns): Register new subr. * src/androidvfs.c (struct android_vdir, struct android_vops) (struct android_vnode, struct android_special_vnode) (enum android_vnode_type, struct android_cursor_class) (struct emacs_directory_entry_class) (struct android_parcel_file_descriptor_class) (android_init_cursor_class, android_init_entry_class) (android_init_fd_class, android_vfs_canonicalize_name) (struct android_unix_vnode, struct android_unix_vdir, unix_vfs_ops) (android_unix_name, android_unix_vnode, android_unix_open) (android_unix_close, android_unix_unlink, android_unix_symlink) (android_unix_rmdir, android_unix_rename, android_unix_stat) (android_unix_access, android_unix_mkdir, android_unix_readdir) (android_unix_closedir, android_unix_dirfd, android_unix_opendir) (android_extract_long, android_scan_directory_tree) (android_is_directory, android_init_assets) (android_hack_asset_fd_fallback, android_detect_ashmem) (android_hack_asset_fd, struct android_afs_vnode) (struct android_afs_vdir, struct android_afs_open_fd, afs_vfs_ops) (android_afs_name, android_afs_initial, android_close_on_exec) (android_afs_open, android_afs_close, android_afs_unlink) (android_afs_symlink, android_afs_rmdir, android_afs_rename) (android_afs_stat, android_afs_access, android_afs_mkdir) (android_afs_readdir, android_afs_closedir, android_afs_dirfd) (android_afs_opendir, android_afs_get_directory_name) (struct android_content_vdir, content_vfs_ops) (content_directory_contents, android_content_name) (android_content_open, android_content_close) (android_content_unlink, android_content_symlink) (android_content_rmdir, android_content_rename) (android_content_stat, android_content_access) (android_content_mkdir, android_content_readdir) (android_content_closedir, android_content_dirfd) (android_content_opendir, android_content_get_directory_name) (android_content_initial, android_get_content_name) (android_check_content_access, struct android_authority_vnode) (authority_vfs_ops, android_authority_name, android_authority_open) (android_authority_close, android_authority_unlink) (android_authority_symlink, android_authority_rmdir) (android_authority_rename, android_authority_stat) (android_authority_access, android_authority_mkdir) (android_authority_opendir, android_authority_initial) (struct android_saf_root_vnode, struct android_saf_root_vdir) (saf_root_vfs_ops, android_saf_root_name, android_saf_root_open) (android_saf_root_close, android_saf_root_unlink) (android_saf_root_symlink, android_saf_root_rmdir) (android_saf_root_rename, android_saf_root_stat) (android_saf_root_access, android_saf_root_mkdir) (android_saf_root_readdir, android_saf_root_closedir) (android_saf_root_dirfd, android_saf_root_opendir) (android_saf_root_initial, android_saf_root_get_directory) (android_saf_stat, android_saf_access) (struct android_saf_tree_vnode, struct android_saf_tree_vdir) (saf_tree_vfs_ops, android_document_id_from_name) (android_saf_tree_name, android_saf_tree_open) (android_saf_tree_close, android_saf_tree_unlink) (android_saf_tree_symlink, android_saf_tree_rmdir) (android_saf_tree_rename, android_saf_tree_stat) (android_saf_tree_access, android_saf_tree_mkdir) (android_saf_tree_opendir_1, android_saf_tree_readdir) (android_saf_tree_closedir, android_saf_tree_dirfd) (android_saf_tree_opendir, android_saf_tree_from_name) (android_saf_tree_get_directory, android_saf_file_vnode) (saf_file_vfs_ops, android_saf_file_name, android_saf_file_open) (android_saf_file_unlink, android_saf_file_rmdir) (android_saf_file_opendir, android_close_parcel_fd) (android_saf_new_vnode, android_saf_new_name, android_saf_new_open) (android_saf_new_unlink, android_saf_new_symlink) (android_saf_new_rmdir, android_saf_new_rename) (android_saf_new_stat, android_saf_new_access) (android_saf_new_mkdir, android_saf_new_opendir, root_vfs_ops) (special_vnodes, android_root_name, android_name_file) (android_vfs_init, android_open, android_unlink, android_symlink) (android_rmdir, android_mkdir, android_renameat_noreplace) (android_rename, android_fstat, android_fstatat_1, android_fstatat) (android_faccessat, android_fdopen, android_close, android_fclose) (android_open_asset, android_close_asset, android_asset_read_quit) (android_asset_read, android_asset_lseek, android_asset_fstat) (android_opendir, android_dirfd, android_readdir) (android_closedir): Move file system emulation routines here. Introduce a new ``VFS'' layer for translating between Emacs-specific file names and the various disparate interfaces for accessing files on Android. * src/callproc.c (delete_temp_file): * src/charset.c (load_charset_map_from_file): * src/dired.c: * src/emacs.c (Fkill_emacs): * src/fileio.c (check_mutable_filename, Fcopy_file) (Fmake_directory_internal, Fdelete_directory_internal) (Fdelete_file, Frename_file, Fadd_name_to_file) (Fmake_symbolic_link, file_accessible_directory_p, Fset_file_modes) (Fset_file_times, write_region): * src/filelock.c (get_boot_time, rename_lock_file) (create_lock_file, current_lock_owner, unlock_file): * src/image.c (slurp_file, png_load_body, jpeg_load_body): * src/keyboard.c (Fopen_dribble_file): * src/lisp.h: * src/lread.c (Fload): * src/process.c (handle_child_signal): * src/sysdep.c (init_standard_fds, emacs_fopen, emacs_fdopen) (emacs_unlink, emacs_symlink, emacs_rmdir, emacs_mkdir) (emacs_renameat_noreplace, emacs_rename): * src/term.c (Fresume_tty, init_tty): Use and add new wrappers for fopen, fdopen, unlink, symlink, rmdir, mkdir, renameat_norepalce and rename. --- java/org/gnu/emacs/EmacsActivity.java | 38 + java/org/gnu/emacs/EmacsDirectoryEntry.java | 33 + java/org/gnu/emacs/EmacsOpenActivity.java | 12 +- java/org/gnu/emacs/EmacsService.java | 1041 ++++++++++++++++++++++++++- 4 files changed, 1101 insertions(+), 23 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsDirectoryEntry.java (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index d7b51388929..86fed5396d7 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import android.app.Activity; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -33,6 +34,8 @@ import android.os.Bundle; import android.util.Log; +import android.net.Uri; + import android.view.Menu; import android.view.View; import android.view.ViewTreeObserver; @@ -48,6 +51,9 @@ public class EmacsActivity extends Activity { public static final String TAG = "EmacsActivity"; + /* ID for URIs from a granted document tree. */ + public static final int ACCEPT_DOCUMENT_TREE = 1; + /* The currently attached EmacsWindow, or null if none. */ private EmacsWindow window; @@ -431,4 +437,36 @@ public class EmacsActivity extends Activity /* Update the window insets. */ syncFullscreenWith (window); } + + + + @Override + public final void + onActivityResult (int requestCode, int resultCode, Intent data) + { + ContentResolver resolver; + Uri uri; + int flags; + + switch (requestCode) + { + case ACCEPT_DOCUMENT_TREE: + + /* A document granted through + EmacsService.requestDirectoryAccess. */ + + if (resultCode == RESULT_OK) + { + resolver = getContentResolver (); + uri = data.getData (); + flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + + if (uri != null) + resolver.takePersistableUriPermission (uri, flags); + } + + break; + } + } }; diff --git a/java/org/gnu/emacs/EmacsDirectoryEntry.java b/java/org/gnu/emacs/EmacsDirectoryEntry.java new file mode 100644 index 00000000000..9c10f2e8771 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDirectoryEntry.java @@ -0,0 +1,33 @@ +/* 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; + +/* Structure holding a single ``directory entry'' from a document + provider. */ + +public class EmacsDirectoryEntry +{ + /* The type of this directory entry. 0 means a regular file and 1 + means a directory. */ + public int d_type; + + /* The display name of the file represented. */ + public String d_name; +}; diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index 9411f85d434..3832cd2faab 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -243,18 +243,8 @@ public final class EmacsOpenActivity extends Activity 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 (); - + content = EmacsService.buildContentName (uri); Log.d (TAG, "checkReadableOrCopy: " + content); - return content; } diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 6240b0e475a..6059439551f 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -23,13 +23,19 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import android.database.Cursor; + import android.graphics.Matrix; import android.graphics.Point; +import android.webkit.MimeTypeMap; + import android.view.InputDevice; import android.view.KeyEvent; import android.view.inputmethod.CursorAnchorInfo; @@ -45,9 +51,12 @@ import android.content.Context; import android.content.ContentResolver; import android.content.Intent; import android.content.IntentFilter; +import android.content.UriPermission; + import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager; + import android.content.res.AssetManager; import android.hardware.input.InputManager; @@ -65,6 +74,7 @@ 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; @@ -77,14 +87,21 @@ import android.widget.Toast; public final class EmacsService extends Service { public static final String TAG = "EmacsService"; - public static volatile EmacsService SERVICE; + + /* The started Emacs service object. */ + public static EmacsService SERVICE; /* If non-NULL, an extra argument to pass to `android_emacs_init'. */ public static String extraStartupArgument; + /* The thread running Emacs C code. */ private EmacsThread thread; + + /* Handler used to run tasks on the main thread. */ private Handler handler; + + /* Content resolver used to access URIs. */ private ContentResolver resolver; /* Keep this in synch with androidgui.h. */ @@ -92,6 +109,13 @@ 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; @@ -305,6 +329,7 @@ public final class EmacsService extends Service view = new EmacsHolder (); runnable = new Runnable () { + @Override public void run () { @@ -557,7 +582,7 @@ public final class EmacsService extends Service return String.valueOf (keysym); } - + /* Start the Emacs service if necessary. On Android 26 and up, start Emacs as a foreground service with a notification, to avoid @@ -872,6 +897,10 @@ public final class EmacsService extends Service icEndSynchronous (); } + + + /* Content provider functions. */ + /* 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. @@ -905,9 +934,8 @@ public final class EmacsService extends Service { /* The usual file name encoding question rears its ugly head again. */ - name = new String (bytes, "UTF-8"); - Log.d (TAG, "openContentUri: " + Uri.parse (name)); + name = new String (bytes, "UTF-8"); fd = resolver.openFileDescriptor (Uri.parse (name), mode); /* Use detachFd on newer versions of Android or plain old @@ -947,7 +975,6 @@ public final class EmacsService extends Service /* 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) { @@ -960,25 +987,58 @@ public final class EmacsService extends Service if (writable) mode += "w"; - Log.d (TAG, "checkContentUri: checking against mode " + mode); - 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; + /* Fall through. */ } + + return false; } + /* Build a content file name for URI. + + Return a file name within the /contents/by-authority + pseudo-directory that `android_get_content_name' can then + transform back into an encoded URI. + + A content name consists of any number of unencoded path segments + separated by `/' characters, possibly followed by a question mark + and an encoded query string. */ + + public static String + buildContentName (Uri uri) + { + StringBuilder builder; + + builder = new StringBuilder ("/content/by-authority/"); + builder.append (uri.getAuthority ()); + + /* First, append each path segment. */ + + for (String segment : uri.getPathSegments ()) + { + /* FIXME: what if segment contains a slash character? */ + builder.append ('/'); + builder.append (uri.encode (segment)); + } + + /* Now, append the query string if necessary. */ + + if (uri.getEncodedQuery () != null) + builder.append ('?').append (uri.getEncodedQuery ()); + + return builder.toString (); + } + + + private long[] queryBattery19 () { @@ -1096,4 +1156,961 @@ public final class EmacsService extends Service window.view.imManager.updateExtractedText (window.view, token, text); } + + + + /* 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. */ + + /* Return an array of each document authority providing at least one + tree URI that Emacs holds the rights to persistently access. */ + + public String[] + getDocumentAuthorities () + { + List permissions; + HashSet allProviders; + Uri uri; + + permissions = resolver.getPersistedUriPermissions (); + allProviders = new HashSet (); + + for (UriPermission permission : permissions) + { + uri = permission.getUri (); + + if (DocumentsContract.isTreeUri (uri) + && permission.isReadPermission ()) + allProviders.add (uri.getAuthority ()); + } + + return allProviders.toArray (new String[0]); + } + + /* Start a file chooser activity to request access to a directory + tree. + + Value is 1 if the activity couldn't be started for some reason, + and 0 in any other case. */ + + public int + requestDirectoryAccess () + { + Runnable runnable; + final EmacsHolder rc; + + /* Return 1 if Android is too old to support this feature. */ + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return 1; + + rc = new EmacsHolder (); + rc.thing = Integer.valueOf (1); + + runnable = new Runnable () { + @Override + public void + run () + { + EmacsActivity activity; + Intent intent; + int id; + + synchronized (this) + { + /* Try to obtain an activity that will receive the + response from the file chooser dialog. */ + + if (EmacsActivity.focusedActivities.isEmpty ()) + { + /* If focusedActivities is empty then this dialog + may have been displayed immediately after another + popup dialog was dismissed. Try the + EmacsActivity to be focused. */ + + activity = EmacsActivity.lastFocusedActivity; + + if (activity == null) + { + /* Still no luck. Return failure. */ + notify (); + return; + } + } + else + activity = EmacsActivity.focusedActivities.get (0); + + /* Now create the intent. */ + intent = new Intent (Intent.ACTION_OPEN_DOCUMENT_TREE); + + try + { + id = EmacsActivity.ACCEPT_DOCUMENT_TREE; + activity.startActivityForResult (intent, id, null); + rc.thing = Integer.valueOf (0); + } + catch (Exception e) + { + e.printStackTrace (); + } + + notify (); + } + } + }; + + syncRunnable (runnable); + return rc.thing; + } + + /* Return an array of each tree provided by the document PROVIDER + that Emacs has permission to access. + + Value is an array if the provider really does exist, NULL + otherwise. */ + + public String[] + getDocumentTrees (byte provider[]) + { + String providerName; + List treeList; + List permissions; + Uri uri; + + try + { + providerName = new String (provider, "ASCII"); + } + catch (UnsupportedEncodingException exception) + { + return null; + } + + permissions = resolver.getPersistedUriPermissions (); + treeList = new ArrayList (); + + for (UriPermission permission : permissions) + { + uri = permission.getUri (); + + if (DocumentsContract.isTreeUri (uri) + && uri.getAuthority ().equals (providerName) + && permission.isReadPermission ()) + /* Make sure the tree document ID is encoded. */ + treeList.add (Uri.encode (DocumentsContract.getTreeDocumentId (uri))); + } + + return treeList.toArray (new String[0]); + } + + /* Decode the specified STRING into a String object using the UTF-8 + format. If an exception is thrown, return null. */ + + private String + decodeFileName (byte[] string) + { + try + { + return new String (string, "UTF-8"); + } + catch (Exception e) /* UnsupportedEncodingException, etc. */ + { + ;; + } + + return null; + } + + /* 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. */ + + private int + documentIdFromName (String tree_uri, byte name[], + String[] id_return) + { + Uri uri, treeUri; + String nameString, 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); + + /* Next, decode NAME. */ + nameString = decodeFileName (name); + + /* Now, split NAME into its individual components. */ + components = nameString.split ("/"); + + /* Set id and type to the value at the root of the tree. */ + type = id = null; + + /* For each component... */ + + 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. */ + + 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 + { + nameString = cursor.getString (column); + } + catch (Exception exception) + { + cursor.close (); + return -1; + } + + /* Break out of the loop only once a matching component is + found. */ + + if (nameString.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; + } + + /* 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 an encoded document URI representing a tree with the + specified IDENTIFIER supplied by the authority AUTHORITY. + + Return null instead if Emacs does not have permanent access + to the specified document tree recorded on disk. */ + + public String + getTreeUri (String tree, String authority) + { + Uri uri, grantedUri; + List permissions; + + /* First, build the URI. */ + tree = Uri.decode (tree); + uri = DocumentsContract.buildTreeDocumentUri (authority, tree); + + /* Now, search for it within the list of persisted URI + permissions. */ + permissions = resolver.getPersistedUriPermissions (); + + for (UriPermission permission : permissions) + { + /* If the permission doesn't entitle Emacs to read access, + skip it. */ + + if (!permission.isReadPermission ()) + continue; + + grantedUri = permission.getUri (); + + if (grantedUri.equals (uri)) + return uri.toString (); + } + + /* Emacs doesn't have permission to access this tree URI. */ + return null; + } + + /* 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. */ + + 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]; + + 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; + } + } + 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; + } + + return stat; + } + + /* 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, upon a security exception or if WRITABLE the file + is not writable. + -3, upon any other error. */ + + 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. */ + + projection = new String[] { + Document.COLUMN_FLAGS, + Document.COLUMN_MIME_TYPE, + }; + + 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 -2; + } + catch (UnsupportedOperationException exception) + { + exception.printStackTrace (); + + /* Why is this? */ + return -3; + } + + if (cursor == null || !cursor.moveToFirst ()) + return -1; + + if (!writable) + return 0; + + try + { + 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; + } + 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 -3; + } + + return 0; + } + + /* 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. */ + + 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); + + projection = new String [] { + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, + }; + + 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; + } + + /* Return the cursor. */ + return cursor; + } + + /* Read a single directory entry from the specified CURSOR. Return + NULL if at the end of the directory stream, and a directory entry + with `d_name' set to NULL if an error occurs. */ + + public EmacsDirectoryEntry + readDirectoryEntry (Cursor cursor) + { + EmacsDirectoryEntry entry; + int index; + String name, type; + + entry = new EmacsDirectoryEntry (); + + while (true) + { + if (!cursor.moveToNext ()) + return null; + + /* First, retrieve the display name. */ + index = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); + + if (index < 0) + /* Return an invalid directory entry upon failure. */ + return entry; + + try + { + name = cursor.getString (index); + } + catch (Exception exception) + { + return entry; + } + + /* Skip this entry if its name cannot be represented. */ + + if (name.equals ("..") || name.equals (".") || name.contains ("/")) + continue; + + /* Now, look for its type. */ + + index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + + if (index < 0) + /* Return an invalid directory entry upon failure. */ + return entry; + + try + { + type = cursor.getString (index); + } + catch (Exception exception) + { + return entry; + } + + if (type.equals (Document.MIME_TYPE_DIR)) + entry.d_type = 1; + entry.d_name = name; + return entry; + } + + /* Not reached. */ + } + + /* 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. */ + + public ParcelFileDescriptor + openDocument (String uri, String documentId, boolean write, + boolean truncate) + { + 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); + + try + { + 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, + 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); + } + } + catch (Exception exception) + { + return null; + } + + return fileDescriptor; + } + + /* Create a new document with the given display NAME within the + directory identified by DOCUMENTID inside the document tree + designated by URI. + + If DOCUMENTID is NULL, create the document inside the root of + that tree. + + Return the document ID of the new file upon success, NULL + otherwise. */ + + public String + createDocument (String uri, String documentId, String name) + { + String mimeType, separator, mime, extension; + int index; + MimeTypeMap singleton; + Uri directoryUri, docUri; + + /* Try to get the MIME type for this document. + Default to ``application/octet-stream''. */ + + mimeType = "application/octet-stream"; + + /* Abuse WebView stuff to get the file's MIME type. */ + + index = name.lastIndexOf ('.'); + + if (index > 0) + { + singleton = MimeTypeMap.getSingleton (); + extension = name.substring (index + 1); + mime = singleton.getMimeTypeFromExtension (extension); + + if (mime != null) + mimeType = mime; + } + + /* Now parse URI. */ + directoryUri = Uri.parse (uri); + + if (documentId == null) + documentId = DocumentsContract.getTreeDocumentId (directoryUri); + + /* And build a file URI referring to the directory. */ + + directoryUri + = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, + documentId); + + try + { + docUri = DocumentsContract.createDocument (resolver, + directoryUri, + mimeType, name); + + if (docUri == null) + return null; + + /* Return the ID of the new document. */ + return DocumentsContract.getDocumentId (docUri); + } + catch (Exception exception) + { + exception.printStackTrace (); + } + + return null; + } }; -- cgit v1.2.1 From 24af8af62c06cef59d2c82799f83da95643ef960 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 27 Jul 2023 20:32:16 +0800 Subject: Avoid crashes in some edge cases * java/org/gnu/emacs/EmacsActivity.java (onActivityResult): Avoid crashes in some edge cases. --- java/org/gnu/emacs/EmacsActivity.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 86fed5396d7..4ddf51fbb20 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -462,8 +462,17 @@ public class EmacsActivity extends Activity flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (uri != null) - resolver.takePersistableUriPermission (uri, flags); + try + { + if (uri != null) + resolver.takePersistableUriPermission (uri, flags); + } + catch (Exception exception) + { + /* Permission to access URI might've been revoked in + between selecting the file and this callback being + invoked. Don't crash in such cases. */ + } } break; -- cgit v1.2.1 From de0e0939f01a747b8201e06bda5cd50dfa95187f Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 27 Jul 2023 21:59:58 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Document Providers): Improve wording of paragraph clarifying limits on subprocesses. * java/org/gnu/emacs/EmacsService.java (getDocumentTrees): Use Java standard US-ASCII coding standard instead of the undocumented ``ASCII'' alias. (decodeFileName): Remove unused function. (documentIdFromName): * src/android.c (android_init_emacs_service): Take a String for NAME instead of a byte array. * src/androidvfs.c (android_verify_jni_string): New function. (android_document_id_from_name): Verify that STRING is a valid Modified UTF-8 string. --- java/org/gnu/emacs/EmacsService.java | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 6059439551f..bc62e050345 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1282,7 +1282,7 @@ public final class EmacsService extends Service try { - providerName = new String (provider, "ASCII"); + providerName = new String (provider, "US-ASCII"); } catch (UnsupportedEncodingException exception) { @@ -1306,24 +1306,6 @@ public final class EmacsService extends Service return treeList.toArray (new String[0]); } - /* Decode the specified STRING into a String object using the UTF-8 - format. If an exception is thrown, return null. */ - - private String - decodeFileName (byte[] string) - { - try - { - return new String (string, "UTF-8"); - } - catch (Exception e) /* UnsupportedEncodingException, etc. */ - { - ;; - } - - return null; - } - /* Find the document ID of the file within TREE_URI designated by NAME. @@ -1342,11 +1324,10 @@ public final class EmacsService extends Service If the designated file can't be located, return -1. */ private int - documentIdFromName (String tree_uri, byte name[], - String[] id_return) + documentIdFromName (String tree_uri, String name, String[] id_return) { Uri uri, treeUri; - String nameString, id, type; + String id, type; String[] components, projection; Cursor cursor; int column; @@ -1360,11 +1341,8 @@ public final class EmacsService extends Service /* Parse the URI identifying the tree first. */ uri = Uri.parse (tree_uri); - /* Next, decode NAME. */ - nameString = decodeFileName (name); - /* Now, split NAME into its individual components. */ - components = nameString.split ("/"); + components = name.split ("/"); /* Set id and type to the value at the root of the tree. */ type = id = null; @@ -1462,7 +1440,7 @@ public final class EmacsService extends Service try { - nameString = cursor.getString (column); + name = cursor.getString (column); } catch (Exception exception) { @@ -1473,7 +1451,7 @@ public final class EmacsService extends Service /* Break out of the loop only once a matching component is found. */ - if (nameString.equals (component)) + if (name.equals (component)) break; } -- cgit v1.2.1 From 03cf3bbb5c38aa55abd6f7d4860025f7482fcfc3 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 28 Jul 2023 12:21:47 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsDirectoryEntry.java (EmacsDirectoryEntry): Make class final. * java/org/gnu/emacs/EmacsService.java (accessDocument) (openDocumentDirectory, openDocument, createDocument): Throw access and IO error exceptions instead of catching them. (createDirectory, deleteDocument): New functions. * src/android.c (android_init_emacs_service): Add new functions. * src/android.h (struct android_emacs_service): Likewise. * src/androidvfs.c (android_saf_exception_check): New function. Translate between Java exceptions and errno values. (android_saf_stat, android_saf_access, android_saf_delete_document) (struct android_saf_tree_vnode, android_document_id_from_name) (android_saf_tree_name, android_saf_tree_rmdir) (android_saf_tree_opendir_1, android_saf_tree_opendir) (android_saf_file_open, android_saf_file_unlink) (android_saf_new_open, android_saf_new_mkdir): Implement missing VFS operations and derive errno values from the type of any exceptions thrown. (android_vfs_init): Initialize exception classes. (android_mkdir, android_fstat): Remove trailing whitespace. --- java/org/gnu/emacs/EmacsDirectoryEntry.java | 2 +- java/org/gnu/emacs/EmacsService.java | 210 ++++++++++++++++------------ 2 files changed, 119 insertions(+), 93 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsDirectoryEntry.java b/java/org/gnu/emacs/EmacsDirectoryEntry.java index 9c10f2e8771..75c52e48002 100644 --- a/java/org/gnu/emacs/EmacsDirectoryEntry.java +++ b/java/org/gnu/emacs/EmacsDirectoryEntry.java @@ -22,7 +22,7 @@ package org.gnu.emacs; /* Structure holding a single ``directory entry'' from a document provider. */ -public class EmacsDirectoryEntry +public final class EmacsDirectoryEntry { /* The type of this directory entry. 0 means a regular file and 1 means a directory. */ diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index bc62e050345..aa672994f12 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1723,9 +1723,12 @@ public final class EmacsService extends Service not: -1, if the file does not exist. - -2, upon a security exception or if WRITABLE the file - is not writable. - -3, upon any other error. */ + -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 (String uri, String documentId, boolean writable) @@ -1754,24 +1757,8 @@ public final class EmacsService extends Service Document.COLUMN_MIME_TYPE, }; - 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 -2; - } - catch (UnsupportedOperationException exception) - { - exception.printStackTrace (); - - /* Why is this? */ - return -3; - } + cursor = resolver.query (uriObject, projection, null, + null, null); if (cursor == null || !cursor.moveToFirst ()) return -1; @@ -1816,13 +1803,10 @@ public final class EmacsService extends Service if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) return -3; } - catch (Exception exception) + finally { - /* Whether or not type errors cause exceptions to be signaled - is defined ``by the implementation of Cursor'', whatever - that means. */ - exception.printStackTrace (); - return -3; + /* Close the cursor if an exception occurs. */ + cursor.close (); } return 0; @@ -1832,7 +1816,11 @@ public final class EmacsService extends Service 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. */ + Value is NULL upon failure. + + In addition, arbitrary runtime exceptions (such as + SecurityException or UnsupportedOperationException) may be + thrown. */ public Cursor openDocumentDirectory (String uri, String documentId) @@ -1861,25 +1849,8 @@ public final class EmacsService extends Service Document.COLUMN_MIME_TYPE, }; - 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; - } - + cursor = resolver.query (uriObject, projection, null, null, + null); /* Return the cursor. */ return cursor; } @@ -1966,11 +1937,15 @@ public final class EmacsService extends Service 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. */ + descriptor instead of using the `close' system call. + + FileNotFoundException and/or SecurityException and + UnsupportedOperationException may be thrown upon failure. */ public ParcelFileDescriptor openDocument (String uri, String documentId, boolean write, boolean truncate) + throws FileNotFoundException { Uri treeUri, documentUri; String mode; @@ -1984,40 +1959,33 @@ public final class EmacsService extends Service documentUri = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); - try + if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - 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, - null); - } - else - { - /* Select the mode used to open the file. `openFile' - below means always open a stat-able file. */ + /* 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) - /* Invalid mode! */ - return null; - else - mode = "r"; + if (truncate) + mode = "rwt"; + else + mode = "rw"; - fileDescriptor = resolver.openFile (documentUri, mode, null); - } + fileDescriptor + = resolver.openFileDescriptor (documentUri, mode, + null); } - catch (Exception exception) + else { - return null; + /* 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); } return fileDescriptor; @@ -2030,11 +1998,15 @@ public final class EmacsService extends Service If DOCUMENTID is NULL, create the document inside the root of that tree. + Either FileNotFoundException, SecurityException or + UnsupportedOperationException may be thrown upon failure. + Return the document ID of the new file upon success, NULL otherwise. */ public String createDocument (String uri, String documentId, String name) + throws FileNotFoundException { String mimeType, separator, mime, extension; int index; @@ -2072,23 +2044,77 @@ public final class EmacsService extends Service = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, documentId); - try - { - docUri = DocumentsContract.createDocument (resolver, - directoryUri, - mimeType, name); + docUri = DocumentsContract.createDocument (resolver, + directoryUri, + mimeType, name); - if (docUri == null) - return null; + if (docUri == null) + return null; - /* Return the ID of the new document. */ - return DocumentsContract.getDocumentId (docUri); - } - catch (Exception exception) - { - exception.printStackTrace (); - } + /* Return the ID of the new document. */ + return DocumentsContract.getDocumentId (docUri); + } - return null; + /* Like `createDocument', but create a directory instead of an + ordinary document. */ + + public String + createDirectory (String uri, String documentId, String name) + throws FileNotFoundException + { + int index; + Uri directoryUri, docUri; + + /* Now parse URI. */ + directoryUri = Uri.parse (uri); + + if (documentId == null) + documentId = DocumentsContract.getTreeDocumentId (directoryUri); + + /* And build a file URI referring to the directory. */ + + directoryUri + = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, + documentId); + + /* If name ends with a directory separator character, delete + it. */ + + if (name.endsWith ("/")) + name = name.substring (0, name.length () - 1); + + /* From Android's perspective, directories are just ordinary + documents with the `MIME_TYPE_DIR' type. */ + + docUri = DocumentsContract.createDocument (resolver, + directoryUri, + Document.MIME_TYPE_DIR, + name); + + if (docUri == null) + return null; + + /* Return the ID of the new document. */ + return DocumentsContract.getDocumentId (docUri); + } + + /* Delete the document identified by ID from the document tree + identified by URI. Return 0 upon success and -1 upon + failure. */ + + public int + deleteDocument (String uri, String id) + throws FileNotFoundException + { + Uri uriObject; + + uriObject = Uri.parse (uri); + uriObject = DocumentsContract.buildDocumentUriUsingTree (uriObject, + id); + + if (DocumentsContract.deleteDocument (resolver, uriObject)) + return 0; + + return -1; } }; -- cgit v1.2.1 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. --- java/org/gnu/emacs/EmacsNative.java | 17 + java/org/gnu/emacs/EmacsSafThread.java | 922 +++++++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsService.java | 520 ++----------------- 3 files changed, 986 insertions(+), 473 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsSafThread.java (limited to 'java/org/gnu') 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 -- cgit v1.2.1 From 25ef0c3a89a92e8f5ea12afe43224c84632e89c6 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 28 Jul 2023 16:43:40 +0800 Subject: Fix SAF query * java/org/gnu/emacs/EmacsSafThread.java (documentIdFromName1): Fix query argument placeholder string. --- java/org/gnu/emacs/EmacsSafThread.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index fd06603fab3..cbd4ff0d25b 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -335,7 +335,7 @@ public final class EmacsSafThread extends HandlerThread cursor = resolver.query (treeUri, projection, (Document.COLUMN_DISPLAY_NAME - + " = ?s"), + + " = ?"), new String[] { component, }, null, signal); -- cgit v1.2.1 From 47f97b5ae49af49ef3adcad4d4d9cab3668002ff Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 29 Jul 2023 11:24:07 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsSafThread.java (EmacsSafThread, getCache) (pruneCache1, pruneCache, cacheChild, cacheDirectoryFromCursor) (documentIdFromName1, openDocumentDirectory1): Implement the cache referred to by the commentary. * java/org/gnu/emacs/EmacsService.java (deleteDocument): Invalidate the cache upon document removal. * src/androidvfs.c (android_saf_exception_check) (android_document_id_from_name): Correctly preserve or set errno in error cases. --- java/org/gnu/emacs/EmacsSafThread.java | 584 +++++++++++++++++++++++++++++---- java/org/gnu/emacs/EmacsService.java | 14 +- 2 files changed, 531 insertions(+), 67 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index cbd4ff0d25b..b3d6ab49f6d 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -1,24 +1,30 @@ /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- - Copyright (C) 2023 Free Software Foundation, Inc. +Copyright (C) 2023 Free Software Foundation, Inc. - This file is part of GNU Emacs. +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 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. +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 . */ +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.Collection; +import java.util.HashMap; +import java.util.Iterator; + +import java.io.FileNotFoundException; + import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; @@ -29,6 +35,8 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.ParcelFileDescriptor; +import android.util.Log; + import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; @@ -38,7 +46,6 @@ import android.provider.DocumentsContract.Document; 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 @@ -53,13 +60,10 @@ import android.provider.DocumentsContract.Document; 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 + The cache is split into two levels: the first caches the + relationships between display names and document IDs, while the + second caches individual document IDs and their contents (children, + type, etc.) Long-running operations are also run on this thread for another reason: Android uses special cancellation objects to terminate @@ -76,9 +80,15 @@ import android.provider.DocumentsContract.Document; public final class EmacsSafThread extends HandlerThread { + private static final String TAG = "EmacsSafThread"; + /* The content resolver used by this thread. */ private final ContentResolver resolver; + /* Map between tree URIs and the cache entry representing its + toplevel directory. */ + private final HashMap cacheToplevels; + /* Handler for this thread's main loop. */ private Handler handler; @@ -89,11 +99,20 @@ public final class EmacsSafThread extends HandlerThread public static final int S_IFDIR = 0040000; public static final int S_IFREG = 0100000; + /* Number of seconds in between each attempt to prune the storage + cache. */ + public static final int CACHE_PRUNE_TIME = 10; + + /* Number of seconds after which an entry in the cache is to be + considered invalid. */ + public static final int CACHE_INVALID_TIME = 10; + public EmacsSafThread (ContentResolver resolver) { super ("Document provider access thread"); this.resolver = resolver; + this.cacheToplevels = new HashMap (); } @@ -106,6 +125,376 @@ public final class EmacsSafThread extends HandlerThread /* Set up the handler after the thread starts. */ handler = new Handler (getLooper ()); + + /* And start periodically pruning the cache. */ + postPruneMessage (); + } + + + private final class CacheToplevel + { + /* Map between document names and children. */ + HashMap children; + + /* Map between document IDs and cache items. */ + HashMap idCache; + }; + + private final class DocIdEntry + { + /* The document ID. */ + String documentId; + + /* The time this entry was created. */ + long time; + + public + DocIdEntry () + { + time = System.currentTimeMillis (); + } + + /* Return a cache entry comprised of the state of the file + identified by `documentId'. TREE is the URI of the tree + containing this entry, and TOPLEVEL is the toplevel + representing it. SIGNAL is a cancellation signal. + + Value is NULL if the file cannot be found. */ + + public CacheEntry + getCacheEntry (Uri tree, CacheToplevel toplevel, + CancellationSignal signal) + { + Uri uri; + String[] projection; + String type; + Cursor cursor; + int column; + CacheEntry entry; + + /* Create a document URI representing DOCUMENTID within URI's + authority. */ + + uri = DocumentsContract.buildDocumentUriUsingTree (tree, + documentId); + projection = new String[] { + Document.COLUMN_MIME_TYPE, + }; + + cursor = null; + + try + { + cursor = resolver.query (uri, projection, null, + null, null, signal); + + if (!cursor.moveToFirst ()) + return null; + + column = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + + if (column < 0) + return null; + + type = cursor.getString (column); + + if (type == null) + return null; + + entry = new CacheEntry (); + entry.type = type; + toplevel.idCache.put (documentId, entry); + return entry; + } + catch (Throwable e) + { + if (e instanceof FileNotFoundException) + return null; + + throw e; + } + finally + { + if (cursor != null) + cursor.close (); + } + } + + public boolean + isValid () + { + return ((System.currentTimeMillis () - time) + < CACHE_INVALID_TIME); + } + }; + + private final class CacheEntry + { + /* The type of this document. */ + String type; + + /* Map between document names and children. */ + HashMap children; + + /* The time this entry was created. */ + long time; + + public + CacheEntry () + { + children = new HashMap (); + time = System.currentTimeMillis (); + } + + public boolean + isValid () + { + return ((System.currentTimeMillis () - time) + < CACHE_INVALID_TIME); + } + }; + + /* Create or return a toplevel for the given tree URI. */ + + private CacheToplevel + getCache (Uri uri) + { + CacheToplevel toplevel; + + toplevel = cacheToplevels.get (uri); + + if (toplevel != null) + return toplevel; + + toplevel = new CacheToplevel (); + toplevel.children = new HashMap (); + toplevel.idCache = new HashMap (); + cacheToplevels.put (uri, toplevel); + return toplevel; + } + + /* Remove each cache entry within COLLECTION older than + CACHE_INVALID_TIME. */ + + private void + pruneCache1 (Collection collection) + { + Iterator iter; + DocIdEntry tem; + + iter = collection.iterator (); + while (iter.hasNext ()) + { + /* Get the cache entry. */ + tem = iter.next (); + + /* If it's not valid anymore, remove it. Iterating over a + collection whose contents are being removed is undefined + unless the removal is performed using the iterator's own + `remove' function, so tem.remove cannot be used here. */ + + if (tem.isValid ()) + continue; + + iter.remove (); + } + } + + /* Remove every entry older than CACHE_INVALID_TIME from each + toplevel inside `cachedToplevels'. */ + + private void + pruneCache () + { + Iterator iter; + CacheEntry tem; + + for (CacheToplevel toplevel : cacheToplevels.values ()) + { + /* First, clean up expired cache entries. */ + iter = toplevel.idCache.values ().iterator (); + + while (iter.hasNext ()) + { + /* Get the cache entry. */ + tem = iter.next (); + + /* If it's not valid anymore, remove it. Iterating over a + collection whose contents are being removed is + undefined unless the removal is performed using the + iterator's own `remove' function, so tem.remove cannot + be used here. */ + + if (tem.isValid ()) + { + /* Otherwise, clean up expired items in its document + ID cache. */ + pruneCache1 (tem.children.values ()); + continue; + } + + iter.remove (); + } + } + + postPruneMessage (); + } + + /* Cache file information within TOPLEVEL, under the list of + children CHILDREN. + + NAME, ID, and TYPE should respectively be the display name of the + document within its parent document (the CacheEntry whose + `children' field is CHILDREN), its document ID, and its MIME + type. + + If ID_ENTRY_EXISTS, don't create a new document ID entry within + CHILDREN indexed by NAME. + + Value is the cache entry saved for the document ID. */ + + private CacheEntry + cacheChild (CacheToplevel toplevel, + HashMap children, + String name, String id, String type, + boolean id_entry_exists) + { + DocIdEntry idEntry; + CacheEntry cacheEntry; + + if (!id_entry_exists) + { + idEntry = new DocIdEntry (); + idEntry.documentId = id; + children.put (name, idEntry); + } + + cacheEntry = new CacheEntry (); + cacheEntry.type = type; + toplevel.idCache.put (id, cacheEntry); + return cacheEntry; + } + + /* Cache the type and as many of the children of the directory + designated by DOC_ID as possible into TOPLEVEL. + + CURSOR should be a cursor representing an open directory stream, + with its projection consisting of at least the display name, + document ID and MIME type columns. + + Rewind the position of CURSOR to before its first element after + completion. */ + + private void + cacheDirectoryFromCursor (CacheToplevel toplevel, String documentId, + Cursor cursor) + { + CacheEntry entry, constitutent; + int nameColumn, idColumn, typeColumn; + String id, name, type; + DocIdEntry idEntry; + + /* Find the numbers of the columns wanted. */ + + nameColumn + = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); + idColumn + = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); + typeColumn + = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + + if (nameColumn < 0 || idColumn < 0 || typeColumn < 0) + return; + + entry = new CacheEntry (); + + /* We know this is a directory already. */ + entry.type = Document.MIME_TYPE_DIR; + toplevel.idCache.put (documentId, entry); + + /* Now, try to cache each of its constituents. */ + + while (cursor.moveToNext ()) + { + try + { + name = cursor.getString (nameColumn); + id = cursor.getString (idColumn); + type = cursor.getString (typeColumn); + + if (name == null || id == null || type == null) + continue; + + /* First, add the name and ID to ENTRY's map of + children. */ + idEntry = new DocIdEntry (); + idEntry.documentId = id; + entry.children.put (id, idEntry); + + /* If this constituent is a directory, don't cache any + information about it. It cannot be cached without + knowing its children. */ + + if (type.equals (Document.MIME_TYPE_DIR)) + continue; + + /* Otherwise, create a new cache entry comprised of its + type. */ + constitutent = new CacheEntry (); + constitutent.type = type; + toplevel.idCache.put (documentId, entry); + } + catch (Exception e) + { + e.printStackTrace (); + continue; + } + } + + /* Rewind cursor back to the beginning. */ + cursor.moveToPosition (-1); + } + + /* Post a message to run `pruneCache' every CACHE_PRUNE_TIME + seconds. */ + + private void + postPruneMessage () + { + handler.postDelayed (new Runnable () { + @Override + public void + run () + { + pruneCache (); + } + }, CACHE_PRUNE_TIME * 1000); + } + + /* Invalidate the cache entry denoted by DOCUMENT_ID, within the + document tree URI. + Call this after deleting a document or directory. + + Caveat emptor: this does not remove the component name cache + entries linked to the name(s) of the directory being removed, the + assumption being that the next time `documentIdFromName1' is + called, it will notice that the document is missing and remove + the outdated cache entry. */ + + public void + postInvalidateCache (final Uri uri, final String documentId) + { + handler.post (new Runnable () { + @Override + public void + run () + { + CacheToplevel toplevel; + + toplevel = getCache (uri); + toplevel.idCache.remove (documentId); + } + }); } @@ -289,10 +678,14 @@ public final class EmacsSafThread extends HandlerThread String[] id_return, CancellationSignal signal) { Uri uri, treeUri; - String id, type; + String id, type, newId, newType; String[] components, projection; Cursor cursor; - int column; + int nameColumn, idColumn, typeColumn; + CacheToplevel toplevel; + DocIdEntry idEntry; + HashMap children, next; + CacheEntry cache; projection = new String[] { Document.COLUMN_DISPLAY_NAME, @@ -310,6 +703,12 @@ public final class EmacsSafThread extends HandlerThread type = id = null; cursor = null; + /* Obtain the top level of this cache. */ + toplevel = getCache (uri); + + /* Set the current map of children to this top level. */ + children = toplevel.children; + /* For each component... */ try @@ -321,6 +720,48 @@ public final class EmacsSafThread extends HandlerThread if (component.isEmpty ()) continue; + /* Search for component within the currently cached list + of children. */ + + idEntry = children.get (component); + + if (idEntry != null) + { + /* The document ID is known. Now find the + corresponding document ID cache. */ + + cache = toplevel.idCache.get (idEntry.documentId); + + /* Fetch just the information for this document. */ + + if (cache == null) + cache = idEntry.getCacheEntry (uri, toplevel, signal); + + if (cache == null) + { + /* File status matching idEntry could not be + obtained. Treat this as if the file does not + exist. */ + + children.remove (idEntry); + + if ((type == null + || type.equals (Document.MIME_TYPE_DIR)) + /* ... and type and id currently represent the + penultimate component. */ + && component == components[components.length - 1]) + return -2; + + return -1; + } + + /* Otherwise, use the cached information. */ + id = idEntry.documentId; + type = cache.type; + children = cache.children; + continue; + } + /* Create the tree URI for URI from ID if it exists, or the root otherwise. */ @@ -342,6 +783,21 @@ public final class EmacsSafThread extends HandlerThread if (cursor == null) return -1; + /* Find the column numbers for each of the columns that + are wanted. */ + + nameColumn + = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); + idColumn + = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); + typeColumn + = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + + if (nameColumn < 0 || idColumn < 0 || typeColumn < 0) + return -1; + + next = null; + while (true) { /* Even though the query selects for a specific @@ -350,6 +806,12 @@ public final class EmacsSafThread extends HandlerThread if (!cursor.moveToNext ()) { + /* If a component has been found, break out of the + loop. */ + + if (next != null) + break; + /* If the last component considered is a directory... */ if ((type == null @@ -382,52 +844,38 @@ public final class EmacsSafThread extends HandlerThread /* 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; + name = cursor.getString (nameColumn); + newId = cursor.getString (idColumn); + newType = cursor.getString (typeColumn); - /* Now replace ID with the document ID. */ + /* Any of the three variables above may be NULL if the + column data is of the wrong type depending on how + the Cursor returned is implemented. */ - 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) + if (name == null || newId == null || newType == null) return -1; - type = cursor.getString (column); + /* Cache this name, even if it isn't the document + that's being searched for. */ - /* Type may be NULL depending on how the Cursor - returned is implemented. */ + cache = cacheChild (toplevel, children, name, + newId, newType, + idEntry != null); - if (type == null) - return -1; + /* Record the desired component once it is located, + but continue reading and caching items from the + cursor. */ + + if (name.equals (component)) + { + id = newId; + next = cache.children; + type = newType; + } } + children = next; + /* Now close the cursor. */ cursor.close (); cursor = null; @@ -771,11 +1219,12 @@ public final class EmacsSafThread extends HandlerThread openDocumentDirectory1 (String uri, String documentId, CancellationSignal signal) { - Uri uriObject; + Uri uriObject, tree; Cursor cursor; String projection[]; + CacheToplevel toplevel; - uriObject = Uri.parse (uri); + tree = uriObject = Uri.parse (uri); /* If documentId is not set, use the document ID of the tree URI itself. */ @@ -792,11 +1241,22 @@ public final class EmacsSafThread extends HandlerThread projection = new String [] { Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, }; cursor = resolver.query (uriObject, projection, null, null, null, signal); + + /* Create a new cache entry tied to this document ID. */ + + if (cursor != null) + { + toplevel = getCache (tree); + cacheDirectoryFromCursor (toplevel, documentId, + cursor); + } + /* Return the cursor. */ return cursor; } diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index e410754071b..e2abd6c96ef 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1680,14 +1680,18 @@ public final class EmacsService extends Service deleteDocument (String uri, String id) throws FileNotFoundException { - Uri uriObject; + Uri uriObject, tree; - uriObject = Uri.parse (uri); - uriObject = DocumentsContract.buildDocumentUriUsingTree (uriObject, - id); + tree = Uri.parse (uri); + uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, id); if (DocumentsContract.deleteDocument (resolver, uriObject)) - return 0; + { + if (storageThread != null) + storageThread.postInvalidateCache (tree, id); + + return 0; + } return -1; } -- cgit v1.2.1 From d3e1e188739e54079618405bcc5eb7c6914fecdf Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 29 Jul 2023 11:29:25 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsSafThread.java (DocIdEntry) (getCacheEntry, CacheEntry): Use `uptimeMillis' as the basis for cache expiration. --- java/org/gnu/emacs/EmacsSafThread.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index b3d6ab49f6d..9c3e3deb408 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -151,7 +151,7 @@ public final class EmacsSafThread extends HandlerThread public DocIdEntry () { - time = System.currentTimeMillis (); + time = System.uptimeMillis (); } /* Return a cache entry comprised of the state of the file @@ -208,10 +208,7 @@ public final class EmacsSafThread extends HandlerThread } catch (Throwable e) { - if (e instanceof FileNotFoundException) - return null; - - throw e; + return null; } finally { @@ -223,7 +220,7 @@ public final class EmacsSafThread extends HandlerThread public boolean isValid () { - return ((System.currentTimeMillis () - time) + return ((System.uptimeMillis () - time) < CACHE_INVALID_TIME); } }; @@ -243,13 +240,13 @@ public final class EmacsSafThread extends HandlerThread CacheEntry () { children = new HashMap (); - time = System.currentTimeMillis (); + time = System.uptimeMillis (); } public boolean isValid () { - return ((System.currentTimeMillis () - time) + return ((System.uptimeMillis () - time) < CACHE_INVALID_TIME); } }; -- cgit v1.2.1 From 27fe17f0fc56ac11969dbbe54485cff8a4fdab32 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 29 Jul 2023 12:49:36 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsSafThread.java (DocIdEntry): (getCacheEntry): (CacheEntry): (documentIdFromName1): Fix earlier change. --- java/org/gnu/emacs/EmacsSafThread.java | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index 9c3e3deb408..cf067adc87b 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -33,7 +33,9 @@ import android.os.Build; import android.os.CancellationSignal; import android.os.Handler; import android.os.HandlerThread; +import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; +import android.os.SystemClock; import android.util.Log; @@ -151,7 +153,7 @@ public final class EmacsSafThread extends HandlerThread public DocIdEntry () { - time = System.uptimeMillis (); + time = SystemClock.uptimeMillis (); } /* Return a cache entry comprised of the state of the file @@ -206,6 +208,10 @@ public final class EmacsSafThread extends HandlerThread toplevel.idCache.put (documentId, entry); return entry; } + catch (OperationCanceledException e) + { + throw e; + } catch (Throwable e) { return null; @@ -220,8 +226,8 @@ public final class EmacsSafThread extends HandlerThread public boolean isValid () { - return ((System.uptimeMillis () - time) - < CACHE_INVALID_TIME); + return ((SystemClock.uptimeMillis () - time) + < CACHE_INVALID_TIME * 1000); } }; @@ -240,14 +246,14 @@ public final class EmacsSafThread extends HandlerThread CacheEntry () { children = new HashMap (); - time = System.uptimeMillis (); + time = SystemClock.uptimeMillis (); } public boolean isValid () { - return ((System.uptimeMillis () - time) - < CACHE_INVALID_TIME); + return ((SystemClock.uptimeMillis () - time) + < CACHE_INVALID_TIME * 1000); } }; @@ -740,7 +746,12 @@ public final class EmacsSafThread extends HandlerThread obtained. Treat this as if the file does not exist. */ - children.remove (idEntry); + children.remove (component); + + if (id == null) + id = DocumentsContract.getTreeDocumentId (uri); + + id_return[0] = id; if ((type == null || type.equals (Document.MIME_TYPE_DIR)) -- cgit v1.2.1 From 4bf8b0a2e9db842283e9e3849e8d23573ba3b181 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 29 Jul 2023 15:57:44 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsSafThread.java (postInvalidateCache): New argument cacheName. Remove that file from the cache. (accessDocument1): Consult the storage cache as well. * java/org/gnu/emacs/EmacsService.java (deleteDocument): New argument NAME. * src/android.c (android_init_emacs_service): Add new argument. * src/androidvfs.c (android_saf_delete_document) (android_saf_tree_rmdir, android_saf_file_unlink): Pass name of file being deleted to `deleteDocument'. --- java/org/gnu/emacs/EmacsSafThread.java | 71 +++++++++++++++++++++++++++++++--- java/org/gnu/emacs/EmacsService.java | 9 +++-- 2 files changed, 71 insertions(+), 9 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index cf067adc87b..cb69df01bfb 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -478,14 +478,12 @@ public final class EmacsSafThread extends HandlerThread document tree URI. Call this after deleting a document or directory. - Caveat emptor: this does not remove the component name cache - entries linked to the name(s) of the directory being removed, the - assumption being that the next time `documentIdFromName1' is - called, it will notice that the document is missing and remove - the outdated cache entry. */ + At the same time, remove the final component within the file name + CACHENAME from the cache if it exists. */ public void - postInvalidateCache (final Uri uri, final String documentId) + postInvalidateCache (final Uri uri, final String documentId, + final String cacheName) { handler.post (new Runnable () { @Override @@ -493,9 +491,55 @@ public final class EmacsSafThread extends HandlerThread run () { CacheToplevel toplevel; + HashMap children; + String[] components; + CacheEntry entry; + DocIdEntry idEntry; toplevel = getCache (uri); toplevel.idCache.remove (documentId); + + /* If the parent of CACHENAME is cached, remove it. */ + + children = toplevel.children; + components = cacheName.split ("/"); + + for (String component : components) + { + /* Java `split' removes trailing empty matches but not + leading or intermediary ones. */ + if (component.isEmpty ()) + continue; + + if (component == components[components.length - 1]) + { + /* This is the last component, so remove it from + children. */ + children.remove (component); + return; + } + else + { + /* Search for this component within the last level + of the cache. */ + + idEntry = children.get (component); + + if (idEntry == null) + /* Not cached, so return. */ + return; + + entry = toplevel.idCache.get (idEntry.documentId); + + if (entry == null) + /* Not cached, so return. */ + return; + + /* Locate the next component within this + directory. */ + children = entry.children; + } + } } }); } @@ -1109,12 +1153,27 @@ public final class EmacsSafThread extends HandlerThread int tem, index; String tem1; Cursor cursor; + CacheToplevel toplevel; + CacheEntry entry; uriObject = Uri.parse (uri); if (documentId == null) documentId = DocumentsContract.getTreeDocumentId (uriObject); + /* If WRITABLE is false and the document ID is cached, use its + cached value instead. This speeds up + `directory-files-with-attributes' a little. */ + + if (!writable) + { + toplevel = getCache (uriObject); + entry = toplevel.idCache.get (documentId); + + if (entry != null) + return 0; + } + /* Create a document URI representing DOCUMENTID within URI's authority. */ diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index e2abd6c96ef..5186dec974a 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1674,10 +1674,13 @@ public final class EmacsService extends Service /* Delete the document identified by ID from the document tree identified by URI. Return 0 upon success and -1 upon - failure. */ + failure. + + NAME should be the name of the document being deleted, and is + used to invalidate the cache. */ public int - deleteDocument (String uri, String id) + deleteDocument (String uri, String id, String name) throws FileNotFoundException { Uri uriObject, tree; @@ -1688,7 +1691,7 @@ public final class EmacsService extends Service if (DocumentsContract.deleteDocument (resolver, uriObject)) { if (storageThread != null) - storageThread.postInvalidateCache (tree, id); + storageThread.postInvalidateCache (tree, id, name); return 0; } -- cgit v1.2.1 From 9c5e9bb9be64bedb9aac8b12b4f0c717bcaa4da5 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 29 Jul 2023 20:53:21 +0800 Subject: Correct directory permissions reported for VFS files * java/org/gnu/emacs/EmacsSafThread.java (statDocument1): * src/androidvfs.c (android_afs_stat, android_content_stat) (android_saf_root_stat): Report search permissions for files. --- java/org/gnu/emacs/EmacsSafThread.java | 59 ++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 27 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index cb69df01bfb..b0d014ffe94 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -97,6 +97,7 @@ public final class EmacsSafThread extends HandlerThread /* 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_IXUSR = 0000100; public static final int S_IFCHR = 0020000; public static final int S_IFDIR = 0040000; public static final int S_IFREG = 0100000; @@ -1010,7 +1011,8 @@ public final class EmacsSafThread extends HandlerThread Uri uriObject; String[] projection; long[] stat; - int index; + int flagsIndex, columnIndex, typeIndex; + int sizeIndex, mtimeIndex, flags; long tem; String tem1; Cursor cursor; @@ -1041,7 +1043,16 @@ public final class EmacsSafThread extends HandlerThread if (cursor == null) return null; - if (!cursor.moveToFirst ()) + /* Obtain the indices for columns wanted from this cursor. */ + flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS); + sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE); + typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); + + if (!cursor.moveToFirst () + /* COLUMN_LAST_MODIFIED is allowed to be absent in a + conforming documents provider. */ + || flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0) { cursor.close (); return null; @@ -1052,34 +1063,22 @@ public final class EmacsSafThread extends HandlerThread try { - index = cursor.getColumnIndex (Document.COLUMN_FLAGS); - if (index < 0) - return null; - - tem = cursor.getInt (index); + flags = cursor.getInt (flagsIndex); stat[0] |= S_IRUSR; - if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0) + if ((flags & Document.FLAG_SUPPORTS_WRITE) != 0) stat[0] |= S_IWUSR; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0) + && (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) stat[0] |= S_IFCHR; - index = cursor.getColumnIndex (Document.COLUMN_SIZE); - if (index < 0) - return null; - - if (cursor.isNull (index)) + if (cursor.isNull (sizeIndex)) stat[1] = -1; /* The size is unknown. */ else - stat[1] = cursor.getLong (index); + stat[1] = cursor.getLong (sizeIndex); - index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); - if (index < 0) - return null; - - tem1 = cursor.getString (index); + tem1 = cursor.getString (typeIndex); /* Check if this is a directory file. */ if (tem1.equals (Document.MIME_TYPE_DIR) @@ -1087,9 +1086,17 @@ public final class EmacsSafThread extends HandlerThread 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; + { + /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, + just assume they're writable. */ + stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR; + + /* Directory files cannot be modified if + FLAG_DIR_SUPPORTS_CREATE is not set. */ + + if ((flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) + stat[0] &= ~S_IWUSR; + } /* If this file is neither a character special nor a directory, indicate that it's a regular file. */ @@ -1097,12 +1104,10 @@ public final class EmacsSafThread extends HandlerThread 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)) + if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex)) { /* Content providers are allowed to not provide mtime. */ - tem = cursor.getLong (index); + tem = cursor.getLong (mtimeIndex); stat[2] = tem; } } -- cgit v1.2.1 From 37f68e8696200895832ee1f18b0cd1c0998bb207 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 30 Jul 2023 13:39:27 +0800 Subject: Partially implement rename operations on SAF files * java/org/gnu/emacs/EmacsSafThread.java (postInvalidateCacheDir): * java/org/gnu/emacs/EmacsService.java (renameDocument): New functions. * src/android.c (android_init_emacs_service): * src/android.h (struct android_emacs_service): Link to new JNI function. * src/androidvfs.c (android_saf_rename_document): New function. (android_saf_tree_rename): Implement in terms of that function if possible. --- java/org/gnu/emacs/EmacsSafThread.java | 78 +++++++++++++++++++++++++++++++++- java/org/gnu/emacs/EmacsService.java | 37 ++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index b0d014ffe94..007ea9acfbd 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -134,7 +134,7 @@ public final class EmacsSafThread extends HandlerThread } - private final class CacheToplevel + private static final class CacheToplevel { /* Map between document names and children. */ HashMap children; @@ -232,7 +232,7 @@ public final class EmacsSafThread extends HandlerThread } }; - private final class CacheEntry + private static final class CacheEntry { /* The type of this document. */ String type; @@ -545,6 +545,80 @@ public final class EmacsSafThread extends HandlerThread }); } + /* Invalidate the cache entry denoted by DOCUMENT_ID, within the + document tree URI. + Call this after deleting a document or directory. + + At the same time, remove the child referring to DOCUMENTID from + within CACHENAME's cache entry if it exists. */ + + public void + postInvalidateCacheDir (final Uri uri, final String documentId, + final String cacheName) + { + handler.post (new Runnable () { + @Override + public void + run () + { + CacheToplevel toplevel; + HashMap children; + String[] components; + CacheEntry entry; + DocIdEntry idEntry; + Iterator iter; + + toplevel = getCache (uri); + toplevel.idCache.remove (documentId); + + /* Now remove DOCUMENTID from CACHENAME's cache entry, if + any. */ + + children = toplevel.children; + components = cacheName.split ("/"); + + for (String component : components) + { + /* Java `split' removes trailing empty matches but not + leading or intermediary ones. */ + if (component.isEmpty ()) + continue; + + /* Search for this component within the last level + of the cache. */ + + idEntry = children.get (component); + + if (idEntry == null) + /* Not cached, so return. */ + return; + + entry = toplevel.idCache.get (idEntry.documentId); + + if (entry == null) + /* Not cached, so return. */ + return; + + /* Locate the next component within this + directory. */ + children = entry.children; + } + + iter = children.values ().iterator (); + while (iter.hasNext ()) + { + idEntry = iter.next (); + + if (idEntry.documentId.equals (documentId)) + { + iter.remove (); + break; + } + } + } + }); + } + /* ``Prototypes'' for nested functions that are run within the SAF diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 5186dec974a..07e585ad37c 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1698,4 +1698,41 @@ public final class EmacsService extends Service return -1; } + + /* Rename the document designated by DOCID inside the directory tree + identified by URI, which should be within the directory + designated by DIR, to NAME. If the file can't be renamed because + it doesn't support renaming, return -1, 0 otherwise. */ + + public int + renameDocument (String uri, String docId, String dir, String name) + throws FileNotFoundException + { + Uri tree, uriObject; + + tree = Uri.parse (uri); + uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, docId); + + try + { + if (DocumentsContract.renameDocument (resolver, uriObject, + name) + != null) + { + /* Invalidate the cache. */ + if (storageThread != null) + storageThread.postInvalidateCacheDir (tree, docId, + name); + return 0; + } + } + catch (UnsupportedOperationException e) + { + ;; + } + + /* Handle unsupported operation exceptions specially, so + `android_rename' can return ENXDEV. */ + return -1; + } }; -- cgit v1.2.1 From 5a8130ab967cb296d028539d10c749ee35f62e6a Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 31 Jul 2023 10:50:12 +0800 Subject: Implement cross-directory SAF rename operations * java/org/gnu/emacs/EmacsService.java (renameDocument): Don't catch UnsupportedOperationException; handle ENOSYS in android_saf_rename_document instead. (moveDocument): New function. * lisp/subr.el (y-or-n-p): Always change the text conversion style. * src/android.c (android_init_emacs_service) (android_exception_check_4): New function. * src/android.h: Update Java function table. * src/androidvfs.c (android_saf_rename_document): Handle ENOSYS here by setting errno to EXDEV. (android_saf_move_document): New function. (android_document_id_from_name): Take const `dir_name'. (android_saf_tree_rename): Use delete-move-rename to implement cross-directory renames. --- java/org/gnu/emacs/EmacsService.java | 67 +++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 17 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 07e585ad37c..e714f75fdf2 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1713,26 +1713,59 @@ public final class EmacsService extends Service tree = Uri.parse (uri); uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, docId); - try - { - if (DocumentsContract.renameDocument (resolver, uriObject, - name) - != null) - { - /* Invalidate the cache. */ - if (storageThread != null) - storageThread.postInvalidateCacheDir (tree, docId, - name); - return 0; - } - } - catch (UnsupportedOperationException e) + if (DocumentsContract.renameDocument (resolver, uriObject, + name) + != null) { - ;; + /* Invalidate the cache. */ + if (storageThread != null) + storageThread.postInvalidateCacheDir (tree, docId, + name); + return 0; } - /* Handle unsupported operation exceptions specially, so - `android_rename' can return ENXDEV. */ + /* Handle errors specially, so `android_saf_rename_document' can + return ENXDEV. */ return -1; } + + /* Move the document designated by DOCID from the directory under + DIR_NAME designated by SRCID to the directory designated by + DSTID. If the ID of the document being moved changes as a + consequence of the movement, return the new ID, else NULL. + + URI is the document tree containing all three documents. */ + + public String + moveDocument (String uri, String docId, String dirName, + String dstId, String srcId) + throws FileNotFoundException + { + Uri uri1, docId1, dstId1, srcId1; + Uri name; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + throw new UnsupportedOperationException ("Documents aren't capable" + + " of being moved on Android" + + " versions before 7.0."); + + uri1 = Uri.parse (uri); + docId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, docId); + dstId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, dstId); + srcId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, srcId); + + /* Move the document; this function returns the new ID of the + document should it change. */ + name = DocumentsContract.moveDocument (resolver, docId1, + srcId1, dstId1); + + /* Now invalidate the caches for both DIRNAME and DOCID. */ + + if (storageThread != null) + storageThread.postInvalidateCacheDir (uri1, docId, dirName); + + return (name != null + ? DocumentsContract.getDocumentId (name) + : null); + } }; -- cgit v1.2.1 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 +--- 4 files changed, 13 insertions(+), 14 deletions(-) (limited to 'java/org/gnu') 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); } }; -- cgit v1.2.1 From e41349dd93ffec2b1e383cb4c4dfdb59f6e7edac Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 1 Aug 2023 21:06:06 +0800 Subject: Update Android port * doc/emacs/android.texi (Android File System): Describe how to access real files named /assets or /contents if so required. * java/org/gnu/emacs/EmacsService.java (validAuthority): * src/android.c (android_init_emacs_service): * src/android.h: New function. * src/androidvfs.c (android_saf_valid_authority_p): New function. Wrap the Java function. (android_saf_root_stat, android_saf_root_access): Don't return success if no authority by vp->authority's name exists. (android_saf_tree_from_name): Check validity of string data before giving it to JNI. --- java/org/gnu/emacs/EmacsService.java | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 3c1bb0855f4..8554dadd06e 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1769,4 +1769,29 @@ public final class EmacsService extends Service ? DocumentsContract.getDocumentId (name) : null); } + + /* Return if there is a content provider by the name of AUTHORITY + supplying at least one tree URI Emacs retains persistent rights + to access. */ + + public boolean + validAuthority (String authority) + { + List permissions; + Uri uri; + + permissions = resolver.getPersistedUriPermissions (); + + for (UriPermission permission : permissions) + { + uri = permission.getUri (); + + if (DocumentsContract.isTreeUri (uri) + && permission.isReadPermission () + && uri.getAuthority ().equals (authority)) + return true; + } + + return false; + } }; -- cgit v1.2.1 From 8ff8a7fd5c50aaa7721a562a11836b4ec733ba5e Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 2 Aug 2023 09:09:53 +0800 Subject: Fix reporting of key events containing SYM and META * doc/emacs/android.texi (Android)::(What is Android?): (Android Startup, Android File System, Android Environment) (Android Windowing, Android Fonts, Android Troubleshooting): Improve section titles. (Android Windowing): Describe the relation between keyboard modifiers reported by Android and those in key events. * java/org/gnu/emacs/EmacsWindow.java (onKeyDown, onKeyUp): Clear META_SYM_ON and META_META_MASK when retrieving ASCII characters. * src/androidgui.h: Add ANDROID_META_MASK. * src/androidterm.c (android_android_to_emacs_modifiers) (android_emacs_to_android_modifiers): Transform META to Alt, and vice versa. --- java/org/gnu/emacs/EmacsWindow.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 8118479319e..a1f70644e16 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -616,10 +616,13 @@ public final class EmacsWindow extends EmacsHandleObject state = eventModifiers (event); - /* Ignore meta-state understood by Emacs for now, or Ctrl+C will - not be recognized as an ASCII key press event. */ + /* Ignore meta-state understood by Emacs for now, or key presses + such as Ctrl+C and Meta+C will not be recognized as an ASCII + key press event. */ + state_1 - = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); + = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK + | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK); synchronized (eventStrings) { @@ -646,10 +649,13 @@ public final class EmacsWindow extends EmacsHandleObject /* Compute the event's modifier mask. */ state = eventModifiers (event); - /* Ignore meta-state understood by Emacs for now, or Ctrl+C will - not be recognized as an ASCII key press event. */ + /* Ignore meta-state understood by Emacs for now, or key presses + such as Ctrl+C and Meta+C will not be recognized as an ASCII + key press event. */ + state_1 - = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); + = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK + | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK); EmacsNative.sendKeyRelease (this.handle, event.getEventTime (), -- cgit v1.2.1 From 91a7e9d83f212e478958c2bafd59057ec816cba0 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 3 Aug 2023 10:41:40 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsSafThread.java (CacheToplevel): (EmacsSafThread): (DocIdEntry): (getCache): (pruneCache): (cacheDirectoryFromCursor): (run): (documentIdFromName1): (statDocument1): (openDocumentDirectory1): (openDocument1): Introduce a file status cache and populate it with files within directories as they are opened. * java/org/gnu/emacs/EmacsService.java (createDocument): (createDirectory): (moveDocument): Invalidate the file status cache wherever needed. * src/fileio.c (check_vfs_filename): (Fset_file_modes): Permit `set-file-modes' to silently fail on asset and content files. --- java/org/gnu/emacs/EmacsSafThread.java | 326 ++++++++++++++++++++++----------- java/org/gnu/emacs/EmacsService.java | 38 +++- 2 files changed, 252 insertions(+), 112 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java index 007ea9acfbd..29cd3fa6bc7 100644 --- a/java/org/gnu/emacs/EmacsSafThread.java +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -139,11 +139,39 @@ public final class EmacsSafThread extends HandlerThread /* Map between document names and children. */ HashMap children; + /* Map between document names and file status. */ + HashMap statCache; + /* Map between document IDs and cache items. */ HashMap idCache; }; - private final class DocIdEntry + private static final class StatCacheEntry + { + /* The time at which this cache entry was created. */ + long time; + + /* Flags, size, and modification time of this file. */ + long flags, size, mtime; + + /* Whether or not this file is a directory. */ + boolean isDirectory; + + public + StatCacheEntry () + { + time = SystemClock.uptimeMillis (); + } + + public boolean + isValid () + { + return ((SystemClock.uptimeMillis () - time) + < CACHE_INVALID_TIME * 1000); + } + }; + + private static final class DocIdEntry { /* The document ID. */ String documentId; @@ -162,10 +190,14 @@ public final class EmacsSafThread extends HandlerThread containing this entry, and TOPLEVEL is the toplevel representing it. SIGNAL is a cancellation signal. + RESOLVER is the content provider used to retrieve file + information. + Value is NULL if the file cannot be found. */ public CacheEntry - getCacheEntry (Uri tree, CacheToplevel toplevel, + getCacheEntry (ContentResolver resolver, Uri tree, + CacheToplevel toplevel, CancellationSignal signal) { Uri uri; @@ -272,6 +304,7 @@ public final class EmacsSafThread extends HandlerThread toplevel = new CacheToplevel (); toplevel.children = new HashMap (); + toplevel.statCache = new HashMap (); toplevel.idCache = new HashMap (); cacheToplevels.put (uri, toplevel); return toplevel; @@ -311,7 +344,9 @@ public final class EmacsSafThread extends HandlerThread pruneCache () { Iterator iter; + Iterator statIter; CacheEntry tem; + StatCacheEntry stat; for (CacheToplevel toplevel : cacheToplevels.values ()) { @@ -339,6 +374,25 @@ public final class EmacsSafThread extends HandlerThread iter.remove (); } + + statIter = toplevel.statCache.values ().iterator (); + + while (statIter.hasNext ()) + { + /* Get the cache entry. */ + stat = statIter.next (); + + /* If it's not valid anymore, remove it. Iterating over a + collection whose contents are being removed is + undefined unless the removal is performed using the + iterator's own `remove' function, so tem.remove cannot + be used here. */ + + if (stat.isValid ()) + continue; + + statIter.remove (); + } } postPruneMessage (); @@ -379,8 +433,60 @@ public final class EmacsSafThread extends HandlerThread return cacheEntry; } + /* Cache file status for DOCUMENTID within TOPLEVEL. Value is the + new cache entry. CURSOR is the cursor from where to retrieve the + file status, in the form of the columns COLUMN_FLAGS, + COLUMN_SIZE, COLUMN_MIME_TYPE and COLUMN_LAST_MODIFIED. */ + + private StatCacheEntry + cacheFileStatus (String documentId, CacheToplevel toplevel, + Cursor cursor) + { + StatCacheEntry entry; + int flagsIndex, columnIndex, typeIndex; + int sizeIndex, mtimeIndex; + String type; + + /* Obtain the indices for columns wanted from this cursor. */ + flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS); + sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE); + typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); + + /* COLUMN_LAST_MODIFIED is allowed to be absent in a + conforming documents provider. */ + if (flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0) + return null; + + /* Get the file status from CURSOR. */ + entry = new StatCacheEntry (); + entry.flags = cursor.getInt (flagsIndex); + type = cursor.getString (typeIndex); + + if (type == null) + return null; + + entry.isDirectory = type.equals (Document.MIME_TYPE_DIR); + + if (cursor.isNull (sizeIndex)) + /* The size is unknown. */ + entry.size = -1; + else + entry.size = cursor.getLong (sizeIndex); + + /* mtimeIndex is potentially unset, since document providers + aren't obligated to provide modification times. */ + + if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex)) + entry.mtime = cursor.getLong (mtimeIndex); + + /* Finally, add this entry to the cache and return. */ + toplevel.statCache.put (documentId, entry); + return entry; + } + /* Cache the type and as many of the children of the directory - designated by DOC_ID as possible into TOPLEVEL. + designated by DOCUMENTID as possible into TOPLEVEL. CURSOR should be a cursor representing an open directory stream, with its projection consisting of at least the display name, @@ -435,6 +541,12 @@ public final class EmacsSafThread extends HandlerThread idEntry.documentId = id; entry.children.put (id, idEntry); + /* Cache the file status for ID within TOPELVEL too; if a + directory listing is being requested, it's very likely + that a series of calls for file status will follow. */ + + cacheFileStatus (id, toplevel, cursor); + /* If this constituent is a directory, don't cache any information about it. It cannot be cached without knowing its children. */ @@ -499,6 +611,7 @@ public final class EmacsSafThread extends HandlerThread toplevel = getCache (uri); toplevel.idCache.remove (documentId); + toplevel.statCache.remove (documentId); /* If the parent of CACHENAME is cached, remove it. */ @@ -570,6 +683,7 @@ public final class EmacsSafThread extends HandlerThread toplevel = getCache (uri); toplevel.idCache.remove (documentId); + toplevel.statCache.remove (documentId); /* Now remove DOCUMENTID from CACHENAME's cache entry, if any. */ @@ -619,6 +733,27 @@ public final class EmacsSafThread extends HandlerThread }); } + /* Invalidate the file status cache entry for DOCUMENTID within URI. + Call this when the contents of a file (i.e. the constituents of a + directory file) may have changed, but the document's display name + has not. */ + + public void + postInvalidateStat (final Uri uri, final String documentId) + { + handler.post (new Runnable () { + @Override + public void + run () + { + CacheToplevel toplevel; + + toplevel = getCache (uri); + toplevel.statCache.remove (documentId); + } + }); + } + /* ``Prototypes'' for nested functions that are run within the SAF @@ -857,7 +992,8 @@ public final class EmacsSafThread extends HandlerThread /* Fetch just the information for this document. */ if (cache == null) - cache = idEntry.getCacheEntry (uri, toplevel, signal); + cache = idEntry.getCacheEntry (resolver, uri, toplevel, + signal); if (cache == null) { @@ -1082,114 +1218,105 @@ public final class EmacsSafThread extends HandlerThread statDocument1 (String uri, String documentId, CancellationSignal signal) { - Uri uriObject; + Uri uriObject, tree; String[] projection; long[] stat; - int flagsIndex, columnIndex, typeIndex; - int sizeIndex, mtimeIndex, flags; - long tem; - String tem1; Cursor cursor; + CacheToplevel toplevel; + StatCacheEntry cache; - uriObject = Uri.parse (uri); + tree = Uri.parse (uri); if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (uriObject); + documentId = DocumentsContract.getTreeDocumentId (tree); /* Create a document URI representing DOCUMENTID within URI's authority. */ uriObject - = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); + = DocumentsContract.buildDocumentUriUsingTree (tree, documentId); - /* Now stat this document. */ + /* See if the file status cache currently contains this + document. */ - projection = new String[] { - Document.COLUMN_FLAGS, - Document.COLUMN_LAST_MODIFIED, - Document.COLUMN_MIME_TYPE, - Document.COLUMN_SIZE, - }; + toplevel = getCache (tree); + cache = toplevel.statCache.get (documentId); - cursor = resolver.query (uriObject, projection, null, - null, null, signal); - - if (cursor == null) - return null; - - /* Obtain the indices for columns wanted from this cursor. */ - flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS); - sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE); - typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); - mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); - - if (!cursor.moveToFirst () - /* COLUMN_LAST_MODIFIED is allowed to be absent in a - conforming documents provider. */ - || flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0) + if (cache == null || !cache.isValid ()) { - cursor.close (); - return null; - } + /* Stat this document and enter its information into the + cache. */ - /* Create the array of file status. */ - stat = new long[3]; + projection = new String[] { + Document.COLUMN_FLAGS, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_SIZE, + }; - try - { - flags = cursor.getInt (flagsIndex); + cursor = resolver.query (uriObject, projection, null, + null, null, signal); - stat[0] |= S_IRUSR; - if ((flags & Document.FLAG_SUPPORTS_WRITE) != 0) - stat[0] |= S_IWUSR; + if (cursor == null) + return null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) - stat[0] |= S_IFCHR; + try + { + if (!cursor.moveToFirst ()) + return null; - if (cursor.isNull (sizeIndex)) - stat[1] = -1; /* The size is unknown. */ - else - stat[1] = cursor.getLong (sizeIndex); + cache = cacheFileStatus (documentId, toplevel, cursor); + } + finally + { + cursor.close (); + } - tem1 = cursor.getString (typeIndex); + /* If cache is still null, return null. */ - /* 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 | S_IXUSR; + if (cache == null) + return null; + } - /* Directory files cannot be modified if - FLAG_DIR_SUPPORTS_CREATE is not set. */ + /* Create the array of file status and populate it with the + information within cache. */ + stat = new long[3]; - if ((flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) - stat[0] &= ~S_IWUSR; - } + stat[0] |= S_IRUSR; + if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0) + stat[0] |= S_IWUSR; - /* If this file is neither a character special nor a - directory, indicate that it's a regular file. */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) + stat[0] |= S_IFCHR; - if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) - stat[0] |= S_IFREG; + stat[1] = cache.size; - if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex)) - { - /* Content providers are allowed to not provide mtime. */ - tem = cursor.getLong (mtimeIndex); - stat[2] = tem; - } - } - finally + /* Check if this is a directory file. */ + if (cache.isDirectory + /* 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) { - cursor.close (); + /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, + just assume they're writable. */ + stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR; + + /* Directory files cannot be modified if + FLAG_DIR_SUPPORTS_CREATE is not set. */ + + if ((cache.flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) + stat[0] &= ~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; + + stat[2] = cache.mtime; return stat; } @@ -1389,6 +1516,9 @@ public final class EmacsSafThread extends HandlerThread Document.COLUMN_DISPLAY_NAME, Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, + Document.COLUMN_FLAGS, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_SIZE, }; cursor = resolver.query (uriObject, projection, null, null, @@ -1441,6 +1571,7 @@ public final class EmacsSafThread extends HandlerThread Uri treeUri, documentUri; String mode; ParcelFileDescriptor fileDescriptor; + CacheToplevel toplevel; treeUri = Uri.parse (uri); @@ -1450,35 +1581,26 @@ public final class EmacsSafThread extends HandlerThread 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. */ + /* Select the mode used to open the file. */ + if (write) + { 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. */ + mode = "r"; - if (truncate) - /* Invalid mode! */ - return null; - else - mode = "r"; + fileDescriptor + = resolver.openFileDescriptor (documentUri, mode, + signal); - fileDescriptor = resolver.openFile (documentUri, mode, - signal); - } + /* Every time a document is opened, remove it from the file status + cache. */ + toplevel = getCache (treeUri); + toplevel.statCache.remove (documentId); return fileDescriptor; } diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 8554dadd06e..a3dea368272 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1586,7 +1586,7 @@ public final class EmacsService extends Service String mimeType, separator, mime, extension; int index; MimeTypeMap singleton; - Uri directoryUri, docUri; + Uri treeUri, directoryUri, docUri; /* Try to get the MIME type for this document. Default to ``application/octet-stream''. */ @@ -1608,15 +1608,15 @@ public final class EmacsService extends Service } /* Now parse URI. */ - directoryUri = Uri.parse (uri); + treeUri = Uri.parse (uri); if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (directoryUri); + documentId = DocumentsContract.getTreeDocumentId (treeUri); /* And build a file URI referring to the directory. */ directoryUri - = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, + = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri, documentId); docUri = DocumentsContract.createDocument (resolver, @@ -1626,6 +1626,11 @@ public final class EmacsService extends Service if (docUri == null) return null; + /* Invalidate the file status of the containing directory. */ + + if (storageThread != null) + storageThread.postInvalidateStat (treeUri, documentId); + /* Return the ID of the new document. */ return DocumentsContract.getDocumentId (docUri); } @@ -1638,18 +1643,18 @@ public final class EmacsService extends Service throws FileNotFoundException { int index; - Uri directoryUri, docUri; + Uri treeUri, directoryUri, docUri; /* Now parse URI. */ - directoryUri = Uri.parse (uri); + treeUri = Uri.parse (uri); if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (directoryUri); + documentId = DocumentsContract.getTreeDocumentId (treeUri); /* And build a file URI referring to the directory. */ directoryUri - = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri, + = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri, documentId); /* If name ends with a directory separator character, delete @@ -1669,7 +1674,12 @@ public final class EmacsService extends Service if (docUri == null) return null; - /* Return the ID of the new document. */ + /* Return the ID of the new document, but first invalidate the + state of the containing directory. */ + + if (storageThread != null) + storageThread.postInvalidateStat (treeUri, documentId); + return DocumentsContract.getDocumentId (docUri); } @@ -1763,7 +1773,15 @@ public final class EmacsService extends Service /* Now invalidate the caches for both DIRNAME and DOCID. */ if (storageThread != null) - storageThread.postInvalidateCacheDir (uri1, docId, dirName); + { + storageThread.postInvalidateCacheDir (uri1, docId, dirName); + + /* Invalidate the stat cache entries for both the source and + destination directories, since their contents have + changed. */ + storageThread.postInvalidateStat (uri1, dstId); + storageThread.postInvalidateStat (uri1, srcId); + } return (name != null ? DocumentsContract.getDocumentId (name) -- cgit v1.2.1 From 709195fea6a082e3512c14fe16c4f9ea2f99824c Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 4 Aug 2023 08:32:05 +0800 Subject: Avoid encoding commonplace characters in tree names * java/org/gnu/emacs/EmacsService.java (getDocumentTrees): Don't encode some characters that need not be escaped within file names. --- java/org/gnu/emacs/EmacsService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index a3dea368272..036bc9cf098 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1294,8 +1294,12 @@ public final class EmacsService extends Service if (DocumentsContract.isTreeUri (uri) && uri.getAuthority ().equals (providerName) && permission.isReadPermission ()) - /* Make sure the tree document ID is encoded. */ - treeList.add (Uri.encode (DocumentsContract.getTreeDocumentId (uri))); + /* Make sure the tree document ID is encoded. Refrain from + encoding characters such as +:&?#, since they don't + conflict with file name separators or other special + characters. */ + treeList.add (Uri.encode (DocumentsContract.getTreeDocumentId (uri), + " +:&?#")); } return treeList.toArray (new String[0]); -- cgit v1.2.1 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. --- java/org/gnu/emacs/EmacsNative.java | 4 ++++ java/org/gnu/emacs/EmacsSafThread.java | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) (limited to 'java/org/gnu') 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); -- cgit v1.2.1 From e4f3a96709052731cc75de19cf7cab94d892bcea Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 6 Aug 2023 21:53:13 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsService.java (readDirectoryEntry): Fix potential NULL dereference. --- java/org/gnu/emacs/EmacsService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'java/org/gnu') diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 036bc9cf098..d91d8f66009 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -1522,7 +1522,8 @@ public final class EmacsService extends Service return entry; } - if (type.equals (Document.MIME_TYPE_DIR)) + if (type != null + && type.equals (Document.MIME_TYPE_DIR)) entry.d_type = 1; entry.d_name = name; return entry; -- cgit v1.2.1